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