Skip to content

Commit 6869725

Browse files
committed
Add convenience script to deploy coverage agent through adb
1 parent f85d489 commit 6869725

File tree

10 files changed

+166
-3
lines changed

10 files changed

+166
-3
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
.externalNativeBuild
1414
.cxx
1515
local.properties
16+
deployer/build

.idea/.name

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/compiler.xml

+4-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/gradle.xml

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,21 @@ gradlew.bat assemble
2727
### Linux
2828

2929
```bash
30-
ANDROID_SDK_ROOT=/path/to/sdk gradlew.bat assemble
30+
ANDROID_SDK_ROOT=/path/to/sdk ./gradlew assemble
3131
```
32+
33+
34+
## Pushing to Device
35+
36+
The `deployer` folder in this project contains a convenience application to push the CoverageAgent
37+
to an Android device using `adb`.
38+
39+
In order to instrument apps that don't have the `android:debuggable` attribute set, you must ensure
40+
you have root access on the device and `ro.debuggable` is set.
41+
42+
```bash
43+
gradle run --args="your.android.package.name"
44+
```
45+
46+
It will locate the app's data directory and push the coverage agent into the
47+
`DATA_DIR/code_cache/startup_agents` directory.

deployer/build.gradle

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
plugins {
2+
id 'org.jetbrains.kotlin.jvm' version '1.5.32'
3+
id 'application'
4+
}
5+
6+
repositories {
7+
mavenCentral()
8+
}
9+
10+
dependencies {
11+
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1'
12+
}
13+
14+
java {
15+
sourceCompatibility = JavaVersion.VERSION_1_8
16+
targetCompatibility = JavaVersion.VERSION_1_8
17+
}
18+
19+
application {
20+
mainClass.set('com.ammaraskar.coverageagent.DeployerKt')
21+
}
22+
23+
tasks.named('test') {
24+
useJUnitPlatform()
25+
}
26+
27+
// Depend on runtime_cpp so that the jvmti agent is built when we are invoked.
28+
tasks.named('run') {
29+
dependsOn(':runtime_cpp:externalNativeBuildDebug')
30+
workingDir = rootProject.projectDir
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
}

settings.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
rootProject.name = "CoverageAgent"
22
include ':runtime_java'
33
include ':runtime_cpp'
4+
include ':deployer'

0 commit comments

Comments
 (0)