1 /*
<lambda>null2 * Copyright 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.benchmark
18
19 import android.annotation.SuppressLint
20 import android.os.Build
21 import android.os.Looper
22 import android.os.ParcelFileDescriptor
23 import android.os.ParcelFileDescriptor.AutoCloseInputStream
24 import android.os.SystemClock
25 import android.util.Log
26 import androidx.annotation.CheckResult
27 import androidx.annotation.RequiresApi
28 import androidx.annotation.RestrictTo
29 import androidx.test.platform.app.InstrumentationRegistry
30 import androidx.tracing.trace
31 import java.io.Closeable
32 import java.io.File
33 import java.io.InputStream
34 import java.nio.charset.Charset
35 import kotlin.random.Random
36 import kotlin.random.nextUInt
37
38 /**
39 * Wrappers for UiAutomation.executeShellCommand to handle compat behavior, and add additional
40 * features like script execution (with piping), stdin/stderr.
41 */
42 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
43 object Shell {
44
45 private const val COMPILATION_PROFILE_UNKNOWN = "unknown"
46
47 /**
48 * Returns true if the line from ps output contains the given process/package name.
49 *
50 * NOTE: On API 25 and earlier, the processName of unbundled executables will include the
51 * relative path they were invoked from:
52 * ```
53 * root 10065 10061 14848 3932 poll_sched 7bcaf1fc8c S /data/local/tmp/tracebox
54 * root 10109 1 11552 1140 poll_sched 78c86eac8c S ./tracebox
55 * ```
56 *
57 * On higher API levels, the process name will simply be e.g. "tracebox".
58 *
59 * As this function is also used for package names (which never have a leading `/`), we simply
60 * check for either.
61 */
62 internal fun psLineContainsProcess(psOutputLine: String, processName: String): Boolean {
63 val processLabel = psOutputLine.substringAfterLast(" ")
64 return processLabel == processName || // exact match
65 processLabel.startsWith("$processName:") || // app subprocess
66 processLabel.endsWith("/$processName") // executable with relative path
67 }
68
69 /**
70 * Equivalent of [psLineContainsProcess], but to be used with full process name string (e.g.
71 * from pgrep)
72 */
73 internal fun fullProcessNameMatchesProcess(
74 fullProcessName: String,
75 processName: String
76 ): Boolean {
77 return fullProcessName == processName || // exact match
78 fullProcessName.startsWith("$processName:") || // app subprocess
79 fullProcessName.endsWith("/$processName") // executable with relative path
80 }
81
82 fun connectUiAutomation() {
83 ShellImpl // force initialization
84 }
85
86 /**
87 * Function for reading shell-accessible proc files, like scaling_max_freq, which can't be read
88 * directly by the app process.
89 */
90 fun catProcFileLong(path: String): Long? {
91 return executeScriptCaptureStdoutStderr("cat $path").stdout.trim().run {
92 try {
93 toLong()
94 } catch (exception: NumberFormatException) {
95 // silently catch exception, as it may be not readable (e.g. due to offline)
96 null
97 }
98 }
99 }
100
101 /**
102 * Get a checksum for a given path
103 *
104 * Note: Does not check for stderr, as this method is used during ShellImpl init, so stderr not
105 * yet available
106 */
107 internal fun getChecksum(path: String): String {
108 val sum =
109 if (Build.VERSION.SDK_INT >= 23) {
110 md5sum(path)
111 } else {
112 // this isn't good, but it's good enough for API 22
113 return getFileSizeLsUnsafe(path) ?: ""
114 }
115 if (sum.isBlank()) {
116 if (!ShellImpl.isSessionRooted) {
117 val lsOutput = ShellImpl.executeCommandUnsafe("ls -l $path")
118 throw IllegalStateException(
119 "Checksum for $path was blank. Adb session is not rooted, if root owns file, " +
120 "you may need to \"adb root\" and delete the file: $lsOutput"
121 )
122 } else {
123 throw IllegalStateException("Checksum for $path was blank.")
124 }
125 }
126 return sum
127 }
128
129 /** Waits for the file size of the [path] to be table for at least [stableIterations]. */
130 @SuppressLint("BanThreadSleep") // Need polling to wait for file content to be flushed
131 fun waitForFileFlush(
132 path: String,
133 stableIterations: Int,
134 maxInitialFlushWaitIterations: Int,
135 maxStableFlushWaitIterations: Int,
136 pollDurationMs: Long,
137 triggerFileFlush: () -> Unit
138 ) {
139 var lastKnownSize = getFileSizeUnsafe(path)
140
141 triggerFileFlush()
142
143 // first, wait for initial dump from flush, which can be a long amount of time
144 var currentSize = getFileSizeUnsafe(path)
145 var iteration = 0
146 while (iteration < maxInitialFlushWaitIterations && currentSize == lastKnownSize) {
147 Thread.sleep(pollDurationMs)
148 currentSize = getFileSizeUnsafe(path)
149 iteration++
150 }
151
152 // wait for stabilization, which should take much less time and happen quickly
153 iteration = 0
154 lastKnownSize = 0
155 var stable = 0
156 while (iteration < maxStableFlushWaitIterations) {
157 currentSize = getFileSizeUnsafe(path)
158 if (currentSize > 0) {
159 if (currentSize == lastKnownSize) {
160 stable += 1
161 if (stable == stableIterations) {
162 break
163 }
164 } else {
165 // reset
166 stable = 0
167 lastKnownSize = currentSize
168 }
169 }
170 iteration += 1
171 Thread.sleep(pollDurationMs)
172 }
173 }
174
175 /** Gets the file size for a given path. */
176 internal fun getFileSizeUnsafe(path: String): Long {
177 // API 23 comes with the helpful stat command
178 val fileSize =
179 if (Build.VERSION.SDK_INT >= 23) {
180 // Using executeCommandUnsafe for perf reasons, but this API is still safe, given
181 // we validate the outputs.
182 ShellImpl.executeCommandUnsafe("stat -c %s $path").trim().toLongOrNull()
183 } else {
184 getFileSizeLsUnsafe(path)?.toLong()
185 }
186 require(fileSize != null) { "Unable to obtain file size for the file $path" }
187 return fileSize
188 }
189
190 /**
191 * Only use this API on API 22 or lower.
192 *
193 * This command uses [ShellImpl.executeCommandUnsafe] for performance reasons. The caller should
194 * always validate the outputs for a given invocation.
195 *
196 * @return `null` when the file [path] cannot be found.
197 */
198 private fun getFileSizeLsUnsafe(path: String): String? {
199 val result = ShellImpl.executeCommandUnsafe("ls -l $path")
200 return if (result.isBlank()) null else result.split(Regex("\\s+"))[3]
201 }
202
203 /**
204 * Copy file and make executable
205 *
206 * Note: this operation does checksum validation of dst, since it's used during setup of the
207 * shell script used to capture stderr, so stderr isn't available.
208 */
209 private fun moveToTmpAndMakeExecutable(src: String, dst: String) {
210 if (UserInfo.isAdditionalUser) {
211 val dstFile = ShellFile(dst).also { it.delete() }
212 val srcFile = UserFile(src)
213 srcFile.copyTo(dstFile)
214 } else {
215 ShellImpl.executeCommandUnsafe("cp $src $dst")
216 }
217
218 // Sets execution permissions on the script
219 if (Build.VERSION.SDK_INT >= 23) {
220 ShellImpl.executeCommandUnsafe("chmod +x $dst")
221 } else {
222 // chmod with support for +x only added in API 23
223 // While 777 is technically more permissive, this is only used for scripts and temporary
224 // files in tests, so we don't worry about permissions / access here
225 ShellImpl.executeCommandUnsafe("chmod 777 $dst")
226 }
227
228 // validate checksums instead of checking stderr, since it's not yet safe to
229 // read from stderr. This detects the problem where root left a stale executable
230 // that can't be modified by shell at the dst path
231 val srcSum = getChecksum(path = src)
232 val dstSum = getChecksum(path = dst)
233 if (srcSum != dstSum) {
234 throw IllegalStateException(
235 "Failed to verify copied executable $dst, " +
236 "md5 sums $srcSum, $dstSum don't match. Check if root owns" +
237 " $dst and if so, delete it with `adb root`-ed shell session."
238 )
239 }
240 }
241
242 /**
243 * Writes the inputStream to an executable file with the given name in `/data/local/tmp`
244 *
245 * Note: this operation does not validate command success, since it's used during setup of shell
246 * scripting code used to parse stderr. This means callers should validate.
247 */
248 fun createRunnableExecutable(name: String, inputStream: InputStream): String {
249 // dirUsableByAppAndShell is writable, but we can't execute there (as of Q),
250 // so we copy to /data/local/tmp
251 val writableExecutableFile =
252 File.createTempFile(
253 /* prefix */ "temporary_$name",
254 /* suffix */ null,
255 /* directory */ Outputs.dirUsableByAppAndShell
256 )
257 val runnableExecutablePath = "/data/local/tmp/$name"
258
259 try {
260 writableExecutableFile.outputStream().use { inputStream.copyTo(it) }
261 if (Outputs.forceFilesForShellAccessible) {
262 // executable must be readable by shell to be moved, and for some reason
263 // doesn't inherit shell readability from dirUsableByAppAndShell
264 writableExecutableFile.setReadable(true, false)
265 }
266 moveToTmpAndMakeExecutable(
267 src = writableExecutableFile.absolutePath,
268 dst = runnableExecutablePath
269 )
270 } finally {
271 writableExecutableFile.delete()
272 }
273
274 return runnableExecutablePath
275 }
276
277 /**
278 * Returns true if the shell session is rooted or su is usable, and thus root commands can be
279 * run (e.g. atrace commands with root-only tags)
280 */
281 fun isSessionRooted(): Boolean {
282 return ShellImpl.isSessionRooted || ShellImpl.isSuAvailable
283 }
284
285 fun getprop(propertyName: String): String {
286 return executeScriptCaptureStdout("getprop $propertyName").trim()
287 }
288
289 /**
290 * Convenience wrapper around [android.app.UiAutomation.executeShellCommand] which adds
291 * scripting functionality like piping and redirects, and which throws if stdout or stder was
292 * produced.
293 *
294 * Unlike `executeShellCommand()`, this method supports arbitrary multi-line shell expressions,
295 * as it creates and executes a shell script in `/data/local/tmp/`.
296 *
297 * Note that shell scripting capabilities differ based on device version. To see which utilities
298 * are available on which platform versions,see
299 * [Android's shell and utilities](https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md#)
300 *
301 * @param script Script content to run
302 * @param stdin String to pass in as stdin to first command in script
303 * @return Stdout string
304 */
305 fun executeScriptSilent(script: String, stdin: String? = null) {
306 val output = executeScriptCaptureStdoutStderr(script, stdin)
307 check(output.isBlank()) { "Expected no stdout/stderr from $script, saw $output" }
308 }
309
310 /**
311 * Convenience wrapper around [android.app.UiAutomation.executeShellCommand] which adds
312 * scripting functionality like piping and redirects, and which captures stdout and throws if
313 * stderr was produced.
314 *
315 * Unlike `executeShellCommand()`, this method supports arbitrary multi-line shell expressions,
316 * as it creates and executes a shell script in `/data/local/tmp/`.
317 *
318 * Note that shell scripting capabilities differ based on device version. To see which utilities
319 * are available on which platform versions,see
320 * [Android's shell and utilities](https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md#)
321 *
322 * @param script Script content to run
323 * @param stdin String to pass in as stdin to first command in script
324 * @return Stdout string
325 */
326 @CheckResult
327 fun executeScriptCaptureStdout(script: String, stdin: String? = null): String {
328 val output = executeScriptCaptureStdoutStderr(script, stdin)
329 check(output.stderr.isBlank()) { "Expected no stderr from $script, saw ${output.stderr}" }
330 return output.stdout
331 }
332
333 internal fun parseCompilationMode(apiLevel: Int, dump: String): String {
334 require(apiLevel >= 24)
335
336 /**
337 * Note that the actual string can take several forms, depending on API level and
338 * potentially ABI as well.
339 *
340 * Emulators are known to have different structure than physical devices on the same API
341 * level, this is potentially due to ABI.
342 *
343 * For this reason, we use a relatively lax matching system (only relying on prefix, equals,
344 * and trailing bracket), and rely on tests to validate.
345 */
346 val modePrefix =
347 when (apiLevel) {
348 // lower API levels will sometimes have newlines within the compilation_filter=...
349 // so we're happy to accept any whitespace within. whitespace in the capture is
350 // filtered below
351 in 24..27 -> ", compilation_filter=".toCharArray().joinToString("\\s*?")
352 // haven't observed this on higher APIs :shrug:
353 else -> "\\[status="
354 }
355 return "Dexopt state:.*?$modePrefix([^]]+?)]"
356 .toRegex(RegexOption.DOT_MATCHES_ALL)
357 .find(dump)
358 ?.groups
359 ?.get(1)
360 ?.value
361 ?.filter { !it.isWhitespace() } ?: COMPILATION_PROFILE_UNKNOWN
362 }
363
364 @CheckResult
365 fun getCompilationMode(packageName: String): String {
366 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) return "speed"
367 val dump = executeScriptCaptureStdout("cmd package dump $packageName").trim()
368 return parseCompilationMode(Build.VERSION.SDK_INT, dump)
369 }
370
371 /**
372 * Returns one apk (or more, if multi-apk/bundle) path for the given package
373 *
374 * The result of `pm path <package>` is one or more lines like:
375 * ```
376 * package: </path/to/apk1>
377 * package: </path/to/apk2>
378 * ```
379 *
380 * Note - to test multi-apk behavior locally, you can build and install a module like
381 * `benchmark:integration-tests:macrobenchmark-target` with the instructions below:
382 * ```
383 * ./gradlew benchmark:integ:macrobenchmark-target:bundleRelease
384 * java -jar bundletool.jar build-apks --local-testing --bundle=../../out/androidx/benchmark/integration-tests/macrobenchmark-target/build/outputs/bundle/release/macrobenchmark-target-release.aab --output=out.apks --overwrite --ks=/path/to/androidx/frameworks/support/development/keystore/debug.keystore --connected-device --ks-key-alias=AndroidDebugKey --ks-pass=pass:android
385 * java -jar bundletool.jar install-apks --apks=out.apks
386 * ```
387 */
388 @CheckResult
389 fun pmPath(packageName: String): List<String> {
390 return executeScriptCaptureStdout("pm path $packageName").split("\n").mapNotNull {
391 val delimiter = "package:"
392 val index = it.indexOf(delimiter)
393 if (index != -1) {
394 it.substring(index + delimiter.length).trim()
395 } else {
396 null
397 }
398 }
399 }
400
401 data class Output(val stdout: String, val stderr: String) {
402 /**
403 * Returns true if both stdout and stderr are blank
404 *
405 * This can be used with silent-if-successful shell commands:
406 * ```
407 * check(Shell.executeScriptWithStderr("mv $src $dest").isBlank()) { "Oh no mv failed!" }
408 * ```
409 */
410 fun isBlank(): Boolean = stdout.isBlank() && stderr.isBlank()
411 }
412
413 /**
414 * Convenience wrapper around [android.app.UiAutomation.executeShellCommand] which adds
415 * scripting functionality like piping and redirects, and which captures both stdout and stderr.
416 *
417 * Unlike `executeShellCommand()`, this method supports arbitrary multi-line shell expressions,
418 * as it creates and executes a shell script in `/data/local/tmp/`.
419 *
420 * Note that shell scripting capabilities differ based on device version. To see which utilities
421 * are available on which platform versions,see
422 * [Android's shell and utilities](https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md#)
423 *
424 * @param script Script content to run
425 * @param stdin String to pass in as stdin to first command in script
426 * @return Output object containing stdout and stderr of full script, and stderr of last command
427 */
428 @CheckResult
429 fun executeScriptCaptureStdoutStderr(script: String, stdin: String? = null): Output {
430 return trace("executeScript $script".take(127)) {
431 ShellImpl.createShellScript(script = script, stdin = stdin).start().getOutputAndClose()
432 }
433 }
434
435 /**
436 * Direct execution of executeShellCommand which doesn't account for scripting functionality,
437 * and doesn't capture stderr.
438 *
439 * Only use this function if you do not care about failure / errors.
440 */
441 @CheckResult
442 fun executeCommandCaptureStdoutOnly(command: String): String {
443 return ShellImpl.executeCommandUnsafe(command)
444 }
445
446 /**
447 * Creates a executable shell script that can be started. Similar to
448 * [executeScriptCaptureStdoutStderr] but allows deferring and caching script execution.
449 *
450 * @param script Script content to run
451 * @param stdin String to pass in as stdin to first command in script
452 * @return ShellScript that can be started.
453 */
454 fun createShellScript(script: String, stdin: String? = null): ShellScript {
455 return ShellImpl.createShellScript(script = script, stdin = stdin)
456 }
457
458 fun isPackageAlive(packageName: String): Boolean {
459 return getPidsForProcess(packageName).isNotEmpty()
460 }
461
462 fun getPidsForProcess(processName: String): List<Int> {
463 if (Build.VERSION.SDK_INT >= 23) {
464 return pgrepLF(pattern = processName).mapNotNull { runningProcess ->
465 // aggressive safety - ensure target isn't subset of another running package
466 if (fullProcessNameMatchesProcess(runningProcess.processName, processName)) {
467 runningProcess.pid
468 } else {
469 null
470 }
471 }
472 }
473
474 // NOTE: `pidof $processName` would work too, but filtering by process
475 // (the whole point of the command) doesn't work pre API 24
476
477 // Can't use ps -A pre API 26, arg isn't supported.
478 // Grep device side, since ps output by itself gets truncated
479 // NOTE: `ps | grep` is slow (multiple seconds), so avoid whenever possible!
480 return executeScriptCaptureStdout("ps | grep $processName")
481 .split(Regex("\r?\n"))
482 .map { it.trim() }
483 .filter { psLineContainsProcess(psOutputLine = it, processName = processName) }
484 .map {
485 // map to int - split, and take 2nd column (PID)
486 it.split(Regex("\\s+"))[1].toInt()
487 }
488 }
489
490 /**
491 * pgrep -l -f <pattern>
492 *
493 * pgrep is *fast*, way faster than ps | grep, but requires API 23
494 *
495 * -l, --list-name list PID and process name -f, --full use full process name to match
496 *
497 * @return List of processes - pid & full process name
498 */
499 @RequiresApi(23)
500 fun pgrepLF(pattern: String): List<ProcessPid> {
501 // Note: we use the unsafe variant for performance, since this is a
502 // common operation, and pgrep is stable after API 23 see [ShellBehaviorTest#pgrep]
503 val apiSpecificArgs =
504 setOfNotNull(
505 // aosp/3507001 -> needed to print full command line (so full package name)
506 if (Build.VERSION.SDK_INT >= 36) "-a" else null
507 )
508 .joinToString(" ")
509
510 return ShellImpl.executeCommandUnsafe("pgrep -l -f $apiSpecificArgs $pattern")
511 .split(Regex("\r?\n"))
512 .filter { it.isNotEmpty() }
513 .map {
514 val (pidString, process) = it.trim().split(" ")
515 ProcessPid(process, pidString.toInt())
516 }
517 }
518
519 @RequiresApi(23)
520 fun getRunningPidsAndProcessesForPackage(packageName: String): List<ProcessPid> {
521 require(!packageName.contains(":")) { "Package $packageName must not contain ':'" }
522 return pgrepLF(pattern = packageName.replace(".", "\\.")).filter {
523 it.processName == packageName || it.processName.startsWith("$packageName:")
524 }
525 }
526
527 fun getRunningProcessesForPackage(packageName: String): List<String> {
528 require(!packageName.contains(":")) { "Package $packageName must not contain ':'" }
529 if (Build.VERSION.SDK_INT >= 23) {
530 // uses pgrep which is nice and fast, but requires API 23
531 return getRunningPidsAndProcessesForPackage(packageName).map { it.processName }
532 }
533
534 // Grep device side, since ps output by itself gets truncated
535 // NOTE: Can't use ps -A pre API 26, arg isn't supported, but would need
536 // to pass it on 26 to see all processes.
537 // NOTE: `ps | grep` is slow (multiple seconds), so avoid whenever possible!
538 return executeScriptCaptureStdout("ps | grep $packageName")
539 .split(Regex("\r?\n"))
540 .map {
541 // get process name from end
542 it.substringAfterLast(" ")
543 }
544 .filter {
545 // allow primary or sub process
546 it == packageName || it.startsWith("$packageName:")
547 }
548 }
549
550 /**
551 * Checks if a process is alive, given a specified pid **and** process name.
552 *
553 * Both must match in order to return true.
554 */
555 fun isProcessAlive(pid: Int, processName: String): Boolean {
556 // unsafe, since this behavior is well tested, and performance here is important
557 // See [ShellBehaviorTest#ps]
558 return ShellImpl.executeCommandUnsafe("ps $pid").split(Regex("\r?\n")).any {
559 psLineContainsProcess(psOutputLine = it, processName = processName)
560 }
561 }
562
563 data class ProcessPid(val processName: String, val pid: Int) {
564 fun isAlive() = isProcessAlive(pid, processName)
565 }
566
567 fun killTerm(processes: List<ProcessPid>) {
568 processes.forEach {
569 // NOTE: we don't fail on stdout/stderr, since killing processes can be racy, and
570 // killing one can kill others. Instead, validation of process death happens below.
571 val stopOutput = executeScriptCaptureStdoutStderr("kill -TERM ${it.pid}")
572 Log.d(BenchmarkState.TAG, "kill -TERM command output - $stopOutput")
573 }
574 }
575
576 private const val DEFAULT_KILL_POLL_PERIOD_MS = 50L
577 private const val DEFAULT_KILL_POLL_MAX_COUNT = 100
578
579 fun killProcessesAndWait(
580 processName: String,
581 waitPollPeriodMs: Long = DEFAULT_KILL_POLL_PERIOD_MS,
582 waitPollMaxCount: Int = DEFAULT_KILL_POLL_MAX_COUNT,
583 onFailure: (String) -> Unit = { errorMessage -> throw IllegalStateException(errorMessage) },
584 processKiller: (List<ProcessPid>) -> Unit = ::killTerm,
585 ) {
586 val processes =
587 getPidsForProcess(processName).map { pid ->
588 ProcessPid(pid = pid, processName = processName)
589 }
590 if (!processes.isEmpty()) {
591 killProcessesAndWait(
592 processes,
593 waitPollPeriodMs = waitPollPeriodMs,
594 waitPollMaxCount = waitPollMaxCount,
595 onFailure,
596 processKiller
597 )
598 } else {
599 Log.d(BenchmarkState.TAG, "No processes for name $processName, skipping kill")
600 }
601 }
602
603 fun killProcessesAndWait(
604 processes: List<ProcessPid>,
605 waitPollPeriodMs: Long = DEFAULT_KILL_POLL_PERIOD_MS,
606 waitPollMaxCount: Int = DEFAULT_KILL_POLL_MAX_COUNT,
607 onFailure: (String) -> Unit = { errorMessage -> throw IllegalStateException(errorMessage) },
608 processKiller: (List<ProcessPid>) -> Unit = ::killTerm,
609 ) {
610 var runningProcesses = processes.toList()
611 processKiller(runningProcesses)
612 repeat(waitPollMaxCount) {
613 runningProcesses = runningProcesses.filter { isProcessAlive(it.pid, it.processName) }
614 if (runningProcesses.isEmpty()) {
615 return
616 }
617 inMemoryTrace("wait for $runningProcesses to die") {
618 SystemClock.sleep(waitPollPeriodMs)
619 }
620 Log.d(BenchmarkState.TAG, "Waiting $waitPollPeriodMs ms for $runningProcesses to die")
621 }
622 onFailure.invoke("Failed to stop $runningProcesses")
623 }
624
625 fun pathExists(absoluteFilePath: String) =
626 if (UserInfo.isAdditionalUser) {
627 VirtualFile.fromPath(absoluteFilePath).ls().first() == absoluteFilePath
628 } else {
629 ShellImpl.executeCommandUnsafe("ls $absoluteFilePath").trim() == absoluteFilePath
630 }
631
632 fun amBroadcast(broadcastArguments: String): Int? {
633 // unsafe here for perf, since we validate the return value so we don't need to check stderr
634 return ShellImpl.executeCommandUnsafe("am broadcast $broadcastArguments")
635 .substringAfter("Broadcast completed: result=")
636 .trim()
637 .toIntOrNull()
638 }
639
640 fun disablePackages(appPackages: List<String>) {
641 // Additionally use `am force-stop` to force JobScheduler to drop all jobs.
642 val command =
643 appPackages.joinToString(separator = "\n") { appPackage ->
644 """
645 am force-stop $appPackage
646 pm disable-user $appPackage
647 """
648 .trimIndent()
649 }
650 executeScriptCaptureStdoutStderr(command)
651 }
652
653 fun enablePackages(appPackages: List<String>) {
654 val command =
655 appPackages.joinToString(separator = "\n") { appPackage -> "pm enable $appPackage" }
656 executeScriptCaptureStdoutStderr(command)
657 }
658
659 @RequiresApi(24)
660 fun disableBackgroundDexOpt() {
661 // Cancels the active job if any
662 ShellImpl.executeCommandUnsafe("cmd package bg-dexopt-job --cancel")
663 ShellImpl.executeCommandUnsafe("cmd package bg-dexopt-job --disable")
664 }
665
666 @RequiresApi(24)
667 fun enableBackgroundDexOpt() {
668 ShellImpl.executeCommandUnsafe("cmd package bg-dexopt-job --enable")
669 }
670
671 fun isSELinuxEnforced(): Boolean {
672 return when (val value = executeScriptCaptureStdout("getenforce").trim()) {
673 "Permissive" -> false
674 "Disabled" -> false
675 "Enforcing" -> true
676 else -> throw IllegalStateException("unexpected result from getenforce: $value")
677 }
678 }
679
680 fun cp(from: String, to: String) {
681 if (UserInfo.isAdditionalUser) {
682 val fromFile = VirtualFile.fromPath(from)
683 val toFile = VirtualFile.fromPath(to)
684 toFile.delete()
685 fromFile.copyTo(toFile)
686 } else {
687 executeScriptSilent("cp $from $to")
688 }
689 }
690
691 fun mv(from: String, to: String) {
692 if (UserInfo.isAdditionalUser) {
693 val fromFile = VirtualFile.fromPath(from)
694 val toFile = VirtualFile.fromPath(to)
695 toFile.delete()
696 fromFile.moveTo(toFile)
697 } else {
698 executeScriptSilent("mv $from $to")
699 }
700 }
701
702 fun rm(path: String) {
703 if (UserInfo.isAdditionalUser) {
704 VirtualFile.fromPath(path).delete()
705 } else {
706 executeScriptSilent("rm -f $path")
707 }
708 }
709
710 fun chmod(path: String, args: String) {
711 if (UserInfo.isAdditionalUser) {
712 VirtualFile.fromPath(path).chmod(args)
713 } else {
714 executeScriptSilent("chmod $args $path")
715 }
716 }
717
718 fun mkdir(path: String) {
719 if (UserInfo.isAdditionalUser) {
720 VirtualFile.fromPath(path).mkdir()
721 } else {
722 executeScriptSilent("mkdir -p $path")
723 }
724 }
725
726 private fun md5sum(path: String): String {
727 return if (UserInfo.isAdditionalUser) {
728 VirtualFile.fromPath(path).md5sum()
729 } else {
730 ShellImpl.executeCommandUnsafe("md5sum $path").substringBefore(" ")
731 }
732 }
733 }
734
735 private object ShellImpl {
736 init {
<lambda>null737 require(Looper.getMainLooper().thread != Thread.currentThread()) {
738 "ShellImpl must not be initialized on the UI thread - UiAutomation must not be " +
739 "connected on the main thread!"
740 }
741 }
742
743 private val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
744
745 /** When true, the session is already rooted and all commands run as root by default. */
746 var isSessionRooted = false
747
748 /** When true, su is available for running commands and scripts as root. */
749 var isSuAvailable = false
750
751 init {
752 // b/268107648: UiAutomation always runs on user 0 so shell cannot access other user data.
753 // This behavior was introduced with FUSE on api 30. Before then, shell could access any
754 // user data.
755 if (UserInfo.currentUserId > 0 && Build.VERSION.SDK_INT in 30 until 31) {
756 throw IllegalStateException(
757 "Benchmark and Baseline Profile generation are not currently " +
758 "supported on AAOS and multiuser environment when a secondary user is " +
759 "selected, on api 30"
760 )
761 }
762 // These variables are used in executeCommand and executeScript, so we keep them as var
763 // instead of val and use a separate initializer
764 isSessionRooted = executeCommandUnsafe("id").contains("uid=0(root)")
765 // use a script below, since direct `su` command failure brings down this process
766 // on some API levels (and can fail even on userdebug builds)
767 isSuAvailable =
768 createShellScript(script = "su root id", stdin = null)
769 .start()
770 .getOutputAndClose()
771 .stdout
772 .contains("uid=0(root)")
773 }
774
775 /**
776 * Reimplementation of UiAutomator's Device.executeShellCommand, to avoid the UiAutomator
777 * dependency, and add tracing
778 *
779 * NOTE: this does not capture stderr, and is thus unsafe. Only use this when the more complex
780 * Shell.executeScript APIs aren't appropriate (such as in their implementation)
781 */
executeCommandUnsafenull782 fun executeCommandUnsafe(cmd: String): String =
783 trace("executeCommand $cmd".take(127)) {
784 return@trace executeCommandNonBlockingUnsafe(cmd).fullyReadInputStream()
785 }
786
executeCommandNonBlockingUnsafenull787 fun executeCommandNonBlockingUnsafe(cmd: String): ParcelFileDescriptor =
788 trace("executeCommandNonBlocking $cmd".take(127)) {
789 return@trace uiAutomation.executeShellCommand(
790 if (!isSessionRooted && isSuAvailable) {
791 "su root $cmd"
792 } else {
793 cmd
794 }
795 )
796 }
797
createShellScriptnull798 fun createShellScript(script: String, stdin: String?): ShellScript =
799 trace("createShellScript") {
800
801 // dirUsableByAppAndShell is writable, but we can't execute there (as of Q),
802 // so we copy to /data/local/tmp
803 val scriptName = "temporaryScript_${Random.nextUInt()}.sh"
804
805 val (scriptContentFile, stdInFile) =
806 if (UserInfo.isAdditionalUser) {
807 Pair(
808 ShellFile.inTempDir(scriptName).apply { writeText(script) },
809 stdin?.let {
810 ShellFile.inTempDir("${scriptName}_stdin").apply { writeText(it) }
811 }
812 )
813 } else {
814 Pair(
815 UserFile.inOutputsDir(scriptName).apply { writeText(script) },
816 stdin?.let { input ->
817 UserFile.inOutputsDir("${scriptName}_stdin").apply { writeText(input) }
818 }
819 )
820 }
821
822 // we use a path on /data/local/tmp (as opposed to externalDir) because some shell
823 // commands fail to redirect stderr to externalDir (notably, `am start`).
824 // This also means we need to `cat` the file to read it, and `rm` to remove it.
825 val stderrPath = "/data/local/tmp/${scriptName}_stderr"
826
827 try {
828 return@trace ShellScript(
829 stdinFile = stdInFile,
830 scriptContentFile = scriptContentFile,
831 stderrPath = stderrPath
832 )
833 } catch (e: Exception) {
834 throw Exception("Can't create shell script", e)
835 }
836 }
837 }
838
839 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
840 class ShellScript
841 internal constructor(
842 private val stdinFile: VirtualFile?,
843 private val scriptContentFile: VirtualFile,
844 private val stderrPath: String
845 ) {
846 private var cleanedUp: Boolean = false
847
848 /**
849 * Starts the shell script previously created.
850 *
851 * @return a [StartedShellScript] that contains streams to read output streams.
852 */
startnull853 fun start(): StartedShellScript =
854 trace("ShellScript#start") {
855 val stdoutDescriptor =
856 ShellImpl.executeCommandNonBlockingUnsafe(
857 scriptWrapperCommand(
858 scriptContentPath = scriptContentFile.absolutePath,
859 stderrPath = stderrPath,
860 stdinPath = stdinFile?.absolutePath
861 )
862 )
863 val stderrDescriptorFn =
864 stderrPath.run { { ShellImpl.executeCommandUnsafe("cat $stderrPath") } }
865
866 return@trace StartedShellScript(
867 stdoutDescriptor = stdoutDescriptor,
868 stderrDescriptorFn = stderrDescriptorFn,
869 cleanUpBlock = ::cleanUp
870 )
871 }
872
873 /** Manually clean up the shell script temporary files from the temp folder. */
cleanUpnull874 fun cleanUp() =
875 trace("ShellScript#cleanUp") {
876 if (cleanedUp) {
877 return@trace
878 }
879
880 // NOTE: while we could theoretically remove some of these files from the script, this
881 // isn't
882 // safe when the script is called multiple times, expecting the intermediates to remain.
883 // We need a rm to clean up the stderr file anyway (b/c it's not ready until stdout is
884 // complete), so we just delete everything here, all at once.
885 ShellImpl.executeCommandUnsafe(
886 "rm -f " +
887 listOfNotNull(
888 stderrPath,
889 scriptContentFile.absolutePath,
890 stdinFile?.absolutePath
891 )
892 .joinToString(" ")
893 )
894 cleanedUp = true
895 }
896
897 companion object {
898 /** Usage args: ```path/to/shellWrapper.sh <scriptFile> <stderrFile> [inputFile]``` */
899 private val scriptWrapperPath =
900 Shell.createRunnableExecutable(
901 // use separate paths to prevent access errors after `adb unroot`
902 if (ShellImpl.isSessionRooted) "shellWrapper_root.sh" else "shellWrapper.sh",
903 """
904 ### shell script which passes in stdin as needed, and captures stderr in a file
905 # $1 == script content (not executable)
906 # $2 == stderr
907 # $3 == stdin (optional)
908 if [[ $3 -eq "0" ]]; then
909 /system/bin/sh $1 2> $2
910 else
911 cat $3 | /system/bin/sh $1 2> $2
912 fi
913 """
914 .trimIndent()
915 .byteInputStream()
916 )
917
scriptWrapperCommandnull918 fun scriptWrapperCommand(
919 scriptContentPath: String,
920 stderrPath: String,
921 stdinPath: String?
922 ): String =
923 listOfNotNull(scriptWrapperPath, scriptContentPath, stderrPath, stdinPath)
924 .joinToString(" ")
925 }
926 }
927
928 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
929 class StartedShellScript
930 internal constructor(
931 private val stdoutDescriptor: ParcelFileDescriptor,
932 private val stderrDescriptorFn: (() -> (String)),
933 private val cleanUpBlock: () -> Unit
934 ) : Closeable {
935
936 /** Returns a [Sequence] of [String] containing the lines written by the process to stdOut. */
937 fun stdOutLineSequence(): Sequence<String> =
938 AutoCloseInputStream(stdoutDescriptor).bufferedReader().lineSequence()
939
940 /** Cleans up this shell script. */
941 override fun close() = cleanUpBlock()
942
943 /** Reads the full process output and cleans up the generated script */
944 fun getOutputAndClose(): Shell.Output {
945 val output =
946 Shell.Output(
947 stdout = stdoutDescriptor.fullyReadInputStream(),
948 stderr = stderrDescriptorFn.invoke()
949 )
950 close()
951 return output
952 }
953 }
954
fullyReadInputStreamnull955 internal fun ParcelFileDescriptor.fullyReadInputStream(): String {
956 AutoCloseInputStream(this).use { inputStream ->
957 return inputStream.readBytes().toString(Charset.defaultCharset())
958 }
959 }
960