|
| 1 | +package com.ammaraskar.coverageagent |
| 2 | + |
| 3 | +import java.io.File |
| 4 | +import java.util.* |
| 5 | +import java.util.concurrent.TimeUnit |
| 6 | + |
| 7 | +class Deployer { |
| 8 | + |
| 9 | + private val soName = "libcoverage_instrumenting_agent.so" |
| 10 | + |
| 11 | + fun deploy(packageName: String, adbDeviceName: String?) { |
| 12 | + println("Instrumenting app $packageName with coverage agent.") |
| 13 | + // Get the architecture of the device. |
| 14 | + val architecture = getDeviceArchitecture(adbDeviceName) |
| 15 | + println("[i] device architecture=${architecture}") |
| 16 | + |
| 17 | + |
| 18 | + val library = File("runtime_cpp/build/intermediates/merged_native_libs/debug/out/lib/${architecture}/${soName}") |
| 19 | + println("[i] Using library: ${library.absolutePath}") |
| 20 | + |
| 21 | + runAdbCommand(adbDeviceName, "push", library.absolutePath, "/data/local/tmp/") |
| 22 | + println("[+] Pushed library to /data/local/tmp/${soName}") |
| 23 | + |
| 24 | + println("[i] Trying to use run-as to copy to startup_agents") |
| 25 | + val copyDestinationWithRunAs = tryToCopyLibraryWithRunAs(packageName, adbDeviceName) |
| 26 | + if (copyDestinationWithRunAs.isPresent) { |
| 27 | + println("[+] Library copied to ${copyDestinationWithRunAs.get()}") |
| 28 | + return |
| 29 | + } |
| 30 | + |
| 31 | + println("[x] run-as failed, using su permissions instead.") |
| 32 | + |
| 33 | + // Use dumpsys package to figure out the data directory and user id of the application. |
| 34 | + val dumpSysOutput = runAdbCommand(adbDeviceName, "shell", "dumpsys", "package", packageName) |
| 35 | + |
| 36 | + var dataDir: String? = null |
| 37 | + var userId: String? = null |
| 38 | + for (line in dumpSysOutput.lines()) { |
| 39 | + if (line.contains("dataDir=")) dataDir = line.split("=")[1].trim() |
| 40 | + if (line.contains("userId=")) userId = line.split("=")[1].trim() |
| 41 | + } |
| 42 | + |
| 43 | + if (dataDir == null || userId == null) { |
| 44 | + println("[!] UNABLE to find app's dataDir or userId. (dataDir=$dataDir, userId=$userId)") |
| 45 | + return |
| 46 | + } |
| 47 | + println("[i] Grabbed app's dataDir=$dataDir and userId=$userId") |
| 48 | + |
| 49 | + runAdbCommand(adbDeviceName, |
| 50 | + "shell", "su", userId, "\"mkdir -p $dataDir/code_cache/startup_agents/\"") |
| 51 | + runAdbCommand(adbDeviceName, |
| 52 | + "shell", "su", userId, "\"cp /data/local/tmp/${soName} $dataDir/code_cache/startup_agents/\"") |
| 53 | + println("[+] Library copied to $dataDir/code_cache/startup_agents/") |
| 54 | + } |
| 55 | + |
| 56 | + private fun getDeviceArchitecture(adbDeviceName: String?): String { |
| 57 | + return runAdbCommand(adbDeviceName, "shell", "getprop", "ro.product.cpu.abi").trim() |
| 58 | + } |
| 59 | + |
| 60 | + private fun tryToCopyLibraryWithRunAs(packageName: String, adbDeviceName: String?): Optional<String> { |
| 61 | + return try { |
| 62 | + runAdbCommand(adbDeviceName, "shell", "run-as", packageName, "mkdir -p code_cache/startup_agents/") |
| 63 | + runAdbCommand(adbDeviceName, "shell", "run-as", packageName, "cp /data/local/tmp/${soName} code_cache/startup_agents/") |
| 64 | + |
| 65 | + Optional.of(runAdbCommand(adbDeviceName, "shell", "run-as", packageName, "pwd")) |
| 66 | + } catch (e: RuntimeException) { |
| 67 | + Optional.empty() |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + private fun runAdbCommand(adbDeviceName: String?, vararg command: String): String { |
| 72 | + val adbCommand = mutableListOf("adb") |
| 73 | + if (adbDeviceName != null) { |
| 74 | + adbCommand.add("-s") |
| 75 | + adbCommand.add(adbDeviceName) |
| 76 | + } |
| 77 | + adbCommand.addAll(command) |
| 78 | + return runCommandAndGetOutput(adbCommand) |
| 79 | + } |
| 80 | + |
| 81 | + private fun runCommandAndGetOutput(command: List<String>): String { |
| 82 | + println("> ${command.joinToString(" ")}") |
| 83 | + val proc = ProcessBuilder(command) |
| 84 | + .redirectError(ProcessBuilder.Redirect.PIPE) |
| 85 | + .redirectOutput(ProcessBuilder.Redirect.PIPE) |
| 86 | + .start() |
| 87 | + |
| 88 | + val output = proc.inputStream.bufferedReader().readText() |
| 89 | + proc.waitFor(20, TimeUnit.SECONDS) |
| 90 | + |
| 91 | + if (proc.exitValue() != 0) { |
| 92 | + print(output) |
| 93 | + print(proc.errorStream.bufferedReader().readText()) |
| 94 | + throw RuntimeException("${command.joinToString(" ")} returned exit code ${proc.exitValue()}") |
| 95 | + } |
| 96 | + return output |
| 97 | + } |
| 98 | + |
| 99 | +} |
| 100 | + |
| 101 | +fun main(args: Array<String>) { |
| 102 | + if (args.isEmpty()) { |
| 103 | + println("Usage: Deployer <android-package-name> [adb-device-name]") |
| 104 | + return |
| 105 | + } |
| 106 | + |
| 107 | + Deployer().deploy(packageName = args[0], adbDeviceName = args.getOrNull(1)) |
| 108 | +} |
0 commit comments