1 /* 2 * Copyright 2023 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.tracing.perfetto.handshake 18 19 import androidx.tracing.perfetto.handshake.protocol.RequestKeys.ACTION_DISABLE_TRACING_COLD_START 20 import androidx.tracing.perfetto.handshake.protocol.RequestKeys.ACTION_ENABLE_TRACING 21 import androidx.tracing.perfetto.handshake.protocol.RequestKeys.ACTION_ENABLE_TRACING_COLD_START 22 import androidx.tracing.perfetto.handshake.protocol.RequestKeys.KEY_PATH 23 import androidx.tracing.perfetto.handshake.protocol.RequestKeys.KEY_PERSISTENT 24 import androidx.tracing.perfetto.handshake.protocol.RequestKeys.RECEIVER_CLASS_NAME 25 import androidx.tracing.perfetto.handshake.protocol.Response 26 import androidx.tracing.perfetto.handshake.protocol.ResponseKeys.KEY_MESSAGE 27 import androidx.tracing.perfetto.handshake.protocol.ResponseKeys.KEY_REQUIRED_VERSION 28 import androidx.tracing.perfetto.handshake.protocol.ResponseKeys.KEY_RESULT_CODE 29 import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes 30 import java.io.File 31 32 /** 33 * Handshake implementation allowing to enable Perfetto SDK tracing in an app that enables it. 34 * 35 * @param targetPackage package name of the target app 36 * @param parseJsonMap function parsing a flat map in a JSON format into a `Map<String, String>` 37 * e.g. `"{ 'key 1': 'value 1', 'key 2': 'value 2' }"` -> `mapOf("key 1" to "value 1", "key 2" to 38 * "value 2")` 39 * @param executeShellCommand function allowing to execute `adb shell` commands on the target device 40 * 41 * For error handling, note that [parseJsonMap] and [executeShellCommand] will be called on the same 42 * thread as [enableTracingImmediate] and [enableTracingColdStart]. 43 */ 44 public class PerfettoSdkHandshake( 45 private val targetPackage: String, 46 private val parseJsonMap: (jsonString: String) -> Map<String, String>, 47 private val executeShellCommand: ShellCommandExecutor 48 ) { 49 /** 50 * Attempts to enable tracing in the app. It will wake up (or start) the app process, so it will 51 * act as warm/hot tracing. For cold tracing see [enableTracingColdStart] 52 * 53 * Note: if the app process is not running, it will be launched making the method a bad choice 54 * for cold tracing (use [enableTracingColdStart] instead. 55 * 56 * @param librarySource optional AAR or an APK containing `libtracing_perfetto.so` 57 */ enableTracingImmediatenull58 public fun enableTracingImmediate(librarySource: LibrarySource? = null): Response = 59 safeExecute { 60 val libPath = 61 librarySource?.run { 62 when (this) { 63 is LibrarySource.ZipLibrarySource -> { 64 PerfettoSdkSideloader(targetPackage) 65 .sideloadFromZipFile( 66 libraryZip, 67 tempDirectory, 68 executeShellCommand, 69 moveLibFileFromTmpDirToAppDir 70 ) 71 } 72 } 73 } 74 sendTracingBroadcast(ACTION_ENABLE_TRACING, libPath) 75 } 76 77 /** 78 * Attempts to prepare cold startup tracing in the app. 79 * 80 * Leaves the app process in a terminated state. 81 * 82 * @param persistent if set to true, cold start tracing mode is persisted between app runs and 83 * must be cleared using [disableTracingColdStart]. Otherwise, cold start tracing is enabled 84 * only for the first app start since enabling. While persistent mode reduces some overhead of 85 * setting up tracing, it recommended to use non-persistent mode as it does not pose the risk 86 * of leaving cold start tracing persistently enabled in case of a failure to clean-up with 87 * [disableTracingColdStart]. 88 * @param librarySource optional AAR or an APK containing `libtracing_perfetto.so` 89 */ 90 @JvmOverloads enableTracingColdStartnull91 public fun enableTracingColdStart( 92 persistent: Boolean = false, 93 librarySource: LibrarySource? = null 94 ): Response = safeExecute { 95 // sideload the `libtracing_perfetto.so` file if applicable 96 val libPath = 97 librarySource?.run { 98 when (this) { 99 is LibrarySource.ZipLibrarySource -> { 100 PerfettoSdkSideloader(targetPackage) 101 .sideloadFromZipFile( 102 libraryZip, 103 tempDirectory, 104 executeShellCommand, 105 moveLibFileFromTmpDirToAppDir 106 ) 107 } 108 } 109 } 110 111 // ensure a clean start (e.g. in case tracing is already enabled) 112 killAppProcess() 113 114 // verify (by performing a regular handshake) that we can enable tracing at app startup 115 val response = 116 sendTracingBroadcast(ACTION_ENABLE_TRACING_COLD_START, libPath, persistent = persistent) 117 118 // Terminate the app process regardless of the response: 119 // - if enabling tracing is successful, the process needs to be terminated for cold tracing 120 // - if enabling tracing is unsuccessful, we still want to terminate the app process to 121 // achieve deterministic behaviour of this method 122 killAppProcess() 123 124 response 125 } 126 127 /** 128 * Disables cold start tracing in the app if previously enabled by [enableTracingColdStart]. 129 * 130 * No-op if cold start tracing was not enabled in the app, or if it was enabled in the 131 * non-`persistent` mode and the app has already been started at least once. 132 * 133 * The function initially enables the app process (if not already enabled), but leaves it in a 134 * terminated state after executing. 135 * 136 * @see [enableTracingColdStart] 137 */ <lambda>null138 public fun disableTracingColdStart(): Response = safeExecute { 139 sendTracingBroadcast(ACTION_DISABLE_TRACING_COLD_START).also { killAppProcess() } 140 } 141 sendTracingBroadcastnull142 private fun sendTracingBroadcast( 143 action: String, 144 libPath: File? = null, 145 persistent: Boolean? = null 146 ): Response { 147 val commandBuilder = StringBuilder("am broadcast -a $action") 148 if (persistent != null) commandBuilder.append(" --es $KEY_PERSISTENT $persistent") 149 if (libPath != null) commandBuilder.append(" --es $KEY_PATH $libPath") 150 commandBuilder.append(" $targetPackage/$RECEIVER_CLASS_NAME") 151 152 val rawResponse = executeShellCommand(commandBuilder.toString()) 153 return try { 154 parseResponse(rawResponse) 155 } catch (e: Exception) { 156 throw PerfettoSdkHandshakeException( 157 "Exception occurred while trying to parse a response." + 158 " Error: ${e.message}. Raw response: $rawResponse." 159 ) 160 } 161 } 162 parseResponsenull163 private fun parseResponse(rawResponse: String): Response { 164 val line = 165 rawResponse.split(Regex("\r?\n")).firstOrNull { 166 it.contains("Broadcast completed: result=") 167 } ?: throw PerfettoSdkHandshakeException("Cannot parse: $rawResponse") 168 169 if (line == "Broadcast completed: result=0") 170 return Response(ResponseResultCodes.RESULT_CODE_CANCELLED, null, null) 171 172 val matchResult = 173 Regex("Broadcast completed: (result=.*?)(, data=\".*?\")?(, extras: .*)?") 174 .matchEntire(line) 175 ?: throw PerfettoSdkHandshakeException("Cannot parse: $rawResponse") 176 177 val broadcastResponseCode = 178 matchResult.groups[1]?.value?.substringAfter("result=")?.toIntOrNull() 179 180 val dataString = 181 matchResult.groups 182 .firstOrNull { it?.value?.startsWith(", data=") ?: false } 183 ?.value 184 ?.substringAfter(", data=\"") 185 ?.dropLast(1) 186 ?: throw PerfettoSdkHandshakeException( 187 "Cannot parse: $rawResponse. " + "Unable to detect 'data=' section." 188 ) 189 190 val dataMap = parseJsonMap(dataString) 191 val response = 192 Response( 193 dataMap[KEY_RESULT_CODE]?.toInt() 194 ?: throw PerfettoSdkHandshakeException( 195 "Response missing $KEY_RESULT_CODE value" 196 ), 197 dataMap[KEY_REQUIRED_VERSION] 198 ?: throw PerfettoSdkHandshakeException( 199 "Response missing $KEY_REQUIRED_VERSION" + " value" 200 ), 201 dataMap[KEY_MESSAGE] 202 ) 203 204 if (broadcastResponseCode != response.resultCode) { 205 throw PerfettoSdkHandshakeException( 206 "Cannot parse: $rawResponse. Result code not matching broadcast result code." 207 ) 208 } 209 210 return response 211 } 212 213 /** Executes provided [block] and wraps exceptions in an appropriate [Response] */ safeExecutenull214 private fun safeExecute(block: () -> Response): Response = 215 try { 216 block() 217 } catch (exception: Exception) { 218 Response(ResponseResultCodes.RESULT_CODE_ERROR_OTHER, null, exception.message) 219 } 220 killAppProcessnull221 private fun killAppProcess() { 222 // on a root session we can use `killall` which works on both system and user apps 223 // `am force-stop` only works on user apps 224 val isRootSession = executeShellCommand("id").contains("uid=0(root)") 225 val result = 226 when (isRootSession) { 227 true -> executeShellCommand("killall $targetPackage") 228 else -> executeShellCommand("am force-stop $targetPackage") 229 } 230 if (result.isNotBlank() && !result.contains("No such process")) { 231 throw PerfettoSdkHandshakeException("Issue while trying to kill app process: $result") 232 } 233 } 234 235 /** Provides means to sideload Perfetto SDK native binaries */ 236 public sealed class LibrarySource { 237 internal class ZipLibrarySource 238 @Suppress("StreamFiles") 239 constructor( 240 internal val libraryZip: File, 241 internal val tempDirectory: File, 242 internal val moveLibFileFromTmpDirToAppDir: FileMover 243 ) : LibrarySource() 244 245 public companion object { 246 /** 247 * Provides means to sideload Perfetto SDK native binaries with a library AAR used as a 248 * source 249 * 250 * @param aarFile an AAR file containing `libtracing_perfetto.so` 251 * @param tempDirectory a directory directly accessible to the caller process (used for 252 * extraction of the binaries from the zip) 253 * @param moveLibFileFromTmpDirToAppDir a function capable of moving the binary file 254 * from the [tempDirectory] to an app accessible folder 255 */ 256 @Suppress("StreamFiles") 257 @JvmStatic aarLibrarySourcenull258 public fun aarLibrarySource( 259 aarFile: File, 260 tempDirectory: File, 261 moveLibFileFromTmpDirToAppDir: FileMover 262 ): LibrarySource = 263 ZipLibrarySource(aarFile, tempDirectory, moveLibFileFromTmpDirToAppDir) 264 265 /** 266 * Provides means to sideload Perfetto SDK native binaries with an APK containing the 267 * library used as a source 268 * 269 * @param apkFile an APK file containing `libtracing_perfetto.so` 270 * @param tempDirectory a directory directly accessible to the caller process (used for 271 * extraction of the binaries from the zip) 272 * @param moveLibFileFromTmpDirToAppDir a function capable of moving the binary file 273 * from the [tempDirectory] to an app accessible folder 274 */ 275 @Suppress("StreamFiles") 276 @JvmStatic 277 public fun apkLibrarySource( 278 apkFile: File, 279 tempDirectory: File, 280 moveLibFileFromTmpDirToAppDir: FileMover 281 ): LibrarySource = 282 ZipLibrarySource(apkFile, tempDirectory, moveLibFileFromTmpDirToAppDir) 283 } 284 } 285 } 286 287 /** Internal exception class for issues specific to [PerfettoSdkHandshake] */ 288 private class PerfettoSdkHandshakeException(message: String) : Exception(message) 289