1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {base64Encode, utf8Decode} from '../../base/string_utils'; 16import {RecordingState} from './state'; 17import {extractTraceConfig} from './trace_config_utils'; 18import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller'; 19import {Adb, AdbStream} from './adb_interfaces'; 20import {ReadBuffersResponse} from './consumer_port_types'; 21import {Consumer} from './record_controller_interfaces'; 22 23enum AdbShellState { 24 READY, 25 RECORDING, 26 FETCHING, 27} 28const DEFAULT_DESTINATION_FILE = '/data/misc/perfetto-traces/trace-by-ui'; 29 30export class AdbConsumerPort extends AdbBaseConsumerPort { 31 traceDestFile = DEFAULT_DESTINATION_FILE; 32 shellState: AdbShellState = AdbShellState.READY; 33 private recordShell?: AdbStream; 34 35 constructor(adb: Adb, consumer: Consumer, recState: RecordingState) { 36 super(adb, consumer, recState); 37 this.adb = adb; 38 } 39 40 async invoke(method: string, params: Uint8Array) { 41 // ADB connection & authentication is handled by the superclass. 42 console.assert(this.state === AdbConnectionState.CONNECTED); 43 44 switch (method) { 45 case 'EnableTracing': 46 this.enableTracing(params); 47 break; 48 case 'ReadBuffers': 49 this.readBuffers(); 50 break; 51 case 'DisableTracing': 52 this.disableTracing(); 53 break; 54 case 'FreeBuffers': 55 this.freeBuffers(); 56 break; 57 case 'GetTraceStats': 58 break; 59 default: 60 this.sendErrorMessage(`Method not recognized: ${method}`); 61 break; 62 } 63 } 64 65 async enableTracing(enableTracingProto: Uint8Array) { 66 try { 67 const traceConfigProto = extractTraceConfig(enableTracingProto); 68 if (!traceConfigProto) { 69 this.sendErrorMessage('Invalid config.'); 70 return; 71 } 72 73 await this.startRecording(traceConfigProto); 74 this.setDurationStatus(enableTracingProto); 75 } catch (e) { 76 this.sendErrorMessage(e.message); 77 } 78 } 79 80 async startRecording(configProto: Uint8Array) { 81 this.shellState = AdbShellState.RECORDING; 82 const recordCommand = this.generateStartTracingCommand(configProto); 83 this.recordShell = await this.adb.shell(recordCommand); 84 const output: string[] = []; 85 this.recordShell.onData = (raw) => output.push(utf8Decode(raw)); 86 this.recordShell.onClose = () => { 87 const response = output.join(); 88 if (!this.tracingEndedSuccessfully(response)) { 89 this.sendErrorMessage(response); 90 this.shellState = AdbShellState.READY; 91 return; 92 } 93 this.sendStatus('Recording ended successfully. Fetching the trace..'); 94 this.sendMessage({type: 'EnableTracingResponse'}); 95 this.recordShell = undefined; 96 }; 97 } 98 99 tracingEndedSuccessfully(response: string): boolean { 100 return !response.includes(' 0 ms') && response.includes('Wrote '); 101 } 102 103 async readBuffers() { 104 console.assert(this.shellState === AdbShellState.RECORDING); 105 this.shellState = AdbShellState.FETCHING; 106 107 const readTraceShell = await this.adb.shell( 108 this.generateReadTraceCommand(), 109 ); 110 readTraceShell.onData = (raw) => 111 this.sendMessage(this.generateChunkReadResponse(raw)); 112 113 readTraceShell.onClose = () => { 114 this.sendMessage( 115 this.generateChunkReadResponse(new Uint8Array(), /* last */ true), 116 ); 117 }; 118 } 119 120 async getPidFromShellAsString() { 121 const pidStr = await this.adb.shellOutputAsString( 122 `ps -u shell | grep perfetto`, 123 ); 124 // We used to use awk '{print $2}' but older phones/Go phones don't have 125 // awk installed. Instead we implement similar functionality here. 126 const awk = pidStr.split(' ').filter((str) => str !== ''); 127 if (awk.length < 1) { 128 throw Error(`Unabled to find perfetto pid in string "${pidStr}"`); 129 } 130 return awk[1]; 131 } 132 133 async disableTracing() { 134 if (!this.recordShell) return; 135 try { 136 // We are not using 'pidof perfetto' so that we can use more filters. 'ps 137 // -u shell' is meant to catch processes started from shell, so if there 138 // are other ongoing tracing sessions started by others, we are not 139 // killing them. 140 const pid = await this.getPidFromShellAsString(); 141 142 if (pid.length === 0 || isNaN(Number(pid))) { 143 throw Error(`Perfetto pid not found. Impossible to stop/cancel the 144 recording. Command output: ${pid}`); 145 } 146 // Perfetto stops and finalizes the tracing session on SIGINT. 147 const killOutput = await this.adb.shellOutputAsString( 148 `kill -SIGINT ${pid}`, 149 ); 150 151 if (killOutput.length !== 0) { 152 throw Error(`Unable to kill perfetto: ${killOutput}`); 153 } 154 } catch (e) { 155 this.sendErrorMessage(e.message); 156 } 157 } 158 159 freeBuffers() { 160 this.shellState = AdbShellState.READY; 161 if (this.recordShell) { 162 this.recordShell.close(); 163 this.recordShell = undefined; 164 } 165 } 166 167 generateChunkReadResponse( 168 data: Uint8Array, 169 last = false, 170 ): ReadBuffersResponse { 171 return { 172 type: 'ReadBuffersResponse', 173 slices: [{data, lastSliceForPacket: last}], 174 }; 175 } 176 177 generateReadTraceCommand(): string { 178 // We attempt to delete the trace file after tracing. On a non-root shell, 179 // this will fail (due to selinux denial), but perfetto cmd will be able to 180 // override the file later. However, on a root shell, we need to clean up 181 // the file since perfetto cmd might otherwise fail to override it in a 182 // future session. 183 return `gzip -c ${this.traceDestFile} && rm -f ${this.traceDestFile}`; 184 } 185 186 generateStartTracingCommand(tracingConfig: Uint8Array) { 187 const configBase64 = base64Encode(tracingConfig); 188 const perfettoCmd = `perfetto -c - -o ${this.traceDestFile}`; 189 return `echo '${configBase64}' | base64 -d | ${perfettoCmd}`; 190 } 191} 192