1 /*
2  * Copyright 2019 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.gradle
18 
19 import java.util.concurrent.TimeUnit
20 import org.gradle.api.GradleException
21 import org.gradle.api.logging.LogLevel
22 import org.gradle.api.logging.Logger
23 
24 /**
25  * Helper class wrapping the adb cli tool.
26  *
27  * Provides an interface to execute adb commands in a way that automatically handles directing
28  * stdout and stderr to gradle output. Typical usage of this class is as input into gradle tasks or
29  * plugins that need to interact with adb.
30  */
31 class Adb {
32     data class ProcessResult(val exitValue: Int, val stdout: String, val stderr: String)
33 
34     private val adbPath: String
35     private val logger: Logger
36 
37     constructor(adbPath: String, logger: Logger) {
38         this.adbPath = adbPath
39         this.logger = logger
40     }
41 
42     /**
43      * Check if adb shell runs as root by default.
44      *
45      * The most common case for this is when adbd is running as root.
46      *
47      * @return `true` if adb shell runs as root by default, `false` otherwise.
48      */
isAdbdRootnull49     fun isAdbdRoot(): Boolean {
50         val defaultUser = execSync("shell id").stdout
51         return defaultUser.contains("uid=0(root)")
52     }
53 
54     /** Check if the `su` binary is installed. */
isSuInstallednull55     fun isSuInstalled(): Boolean {
56         // Not all devices / methods of rooting support su -c, but sh -c is usually supported.
57         // Although the root group is su's default, using syntax different from "su gid cmd", can
58         // cause the adb shell command to hang on some devices.
59         return execSync("shell su 0 sh -c exit", shouldThrow = false).exitValue == 0
60     }
61 
execSyncnull62     fun execSync(
63         adbCmd: String,
64         deviceId: String? = null,
65         shouldThrow: Boolean = true,
66         silent: Boolean = false
67     ): ProcessResult {
68         val subCmd = adbCmd.trim().split(Regex("\\s+")).toTypedArray()
69         val adbArgs = if (!deviceId.isNullOrEmpty()) arrayOf("-s", deviceId) else emptyArray()
70         val cmd = arrayOf(adbPath, *adbArgs, *subCmd)
71 
72         if (!silent) {
73             logger.log(LogLevel.INFO, cmd.joinToString(" "))
74         }
75         val process = Runtime.getRuntime().exec(cmd)
76 
77         if (!process.waitFor(5, TimeUnit.SECONDS)) {
78             throw GradleException("Timeout waiting for ${cmd.joinToString(" ")}")
79         }
80 
81         val stdout = process.inputStream.bufferedReader().use { it.readText() }
82         val stderr = process.errorStream.bufferedReader().use { it.readText() }
83 
84         if (!stdout.isBlank() && !silent) {
85             logger.log(LogLevel.QUIET, stdout.trim())
86         }
87 
88         if (!stderr.isBlank() && shouldThrow && !silent) {
89             logger.log(LogLevel.ERROR, stderr.trim())
90         }
91 
92         if (shouldThrow && process.exitValue() != 0) {
93             throw GradleException(stderr)
94         }
95 
96         return ProcessResult(process.exitValue(), stdout, stderr)
97     }
98 }
99