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