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 {_TextDecoder} from 'custom_utils'; 16 17import {base64Encode} from '../base/string_utils'; 18import {extractTraceConfig} from '../base/trace_config_utils'; 19 20import {AdbAuthState, AdbBaseConsumerPort} from './adb_base_controller'; 21import {Adb, AdbStream} from './adb_interfaces'; 22import {ReadBuffersResponse} from './consumer_port_types'; 23import {Consumer} from './record_controller_interfaces'; 24 25enum AdbShellState { 26 READY, 27 RECORDING, 28 FETCHING 29} 30const DEFAULT_DESTINATION_FILE = '/data/misc/perfetto-traces/trace-by-ui'; 31const textDecoder = new _TextDecoder(); 32 33export class AdbConsumerPort extends AdbBaseConsumerPort { 34 traceDestFile = DEFAULT_DESTINATION_FILE; 35 shellState: AdbShellState = AdbShellState.READY; 36 private recordShell?: AdbStream; 37 38 constructor(adb: Adb, consumer: Consumer) { 39 super(adb, consumer); 40 this.adb = adb; 41 } 42 43 async invoke(method: string, params: Uint8Array) { 44 // ADB connection & authentication is handled by the superclass. 45 console.assert(this.state === AdbAuthState.CONNECTED); 46 47 switch (method) { 48 case 'EnableTracing': 49 this.enableTracing(params); 50 break; 51 case 'ReadBuffers': 52 this.readBuffers(); 53 break; 54 case 'DisableTracing': 55 this.disableTracing(); 56 break; 57 case 'FreeBuffers': 58 this.freeBuffers(); 59 break; 60 case 'GetTraceStats': 61 break; 62 default: 63 this.sendErrorMessage(`Method not recognized: ${method}`); 64 break; 65 } 66 } 67 68 async enableTracing(enableTracingProto: Uint8Array) { 69 try { 70 const traceConfigProto = extractTraceConfig(enableTracingProto); 71 if (!traceConfigProto) { 72 this.sendErrorMessage('Invalid config.'); 73 return; 74 } 75 76 await this.startRecording(traceConfigProto); 77 this.setDurationStatus(enableTracingProto); 78 } catch (e) { 79 this.sendErrorMessage(e.message); 80 } 81 } 82 83 async startRecording(configProto: Uint8Array) { 84 this.shellState = AdbShellState.RECORDING; 85 const recordCommand = this.generateStartTracingCommand(configProto); 86 this.recordShell = await this.adb.shell(recordCommand); 87 const output: string[] = []; 88 this.recordShell.onData = raw => output.push(textDecoder.decode(raw)); 89 this.recordShell.onClose = () => { 90 const response = output.join(); 91 if (!this.tracingEndedSuccessfully(response)) { 92 this.sendErrorMessage(response); 93 this.shellState = AdbShellState.READY; 94 return; 95 } 96 this.sendStatus('Recording ended successfully. Fetching the trace..'); 97 this.sendMessage({type: 'EnableTracingResponse'}); 98 this.recordShell = undefined; 99 }; 100 } 101 102 tracingEndedSuccessfully(response: string): boolean { 103 return !response.includes(' 0 ms') && response.includes('Wrote '); 104 } 105 106 async readBuffers() { 107 console.assert(this.shellState === AdbShellState.RECORDING); 108 this.shellState = AdbShellState.FETCHING; 109 110 const readTraceShell = 111 await this.adb.shell(this.generateReadTraceCommand()); 112 readTraceShell.onData = raw => 113 this.sendMessage(this.generateChunkReadResponse(raw)); 114 115 readTraceShell.onClose = () => { 116 this.sendMessage( 117 this.generateChunkReadResponse(new Uint8Array(), /* last */ true)); 118 }; 119 } 120 121 async getPidFromShellAsString() { 122 const pidStr = 123 await this.adb.shellOutputAsString(`ps -u shell | grep perfetto`); 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 = 148 await this.adb.shellOutputAsString(`kill -SIGINT ${pid}`); 149 150 if (killOutput.length !== 0) { 151 throw Error(`Unable to kill perfetto: ${killOutput}`); 152 } 153 } catch (e) { 154 this.sendErrorMessage(e.message); 155 } 156 } 157 158 freeBuffers() { 159 this.shellState = AdbShellState.READY; 160 if (this.recordShell) { 161 this.recordShell.close(); 162 this.recordShell = undefined; 163 } 164 } 165 166 generateChunkReadResponse(data: Uint8Array, last = false): 167 ReadBuffersResponse { 168 return { 169 type: 'ReadBuffersResponse', 170 slices: [{data, lastSliceForPacket: last}] 171 }; 172 } 173 174 generateReadTraceCommand(): string { 175 // We attempt to delete the trace file after tracing. On a non-root shell, 176 // this will fail (due to selinux denial), but perfetto cmd will be able to 177 // override the file later. However, on a root shell, we need to clean up 178 // the file since perfetto cmd might otherwise fail to override it in a 179 // future session. 180 return `gzip -c ${this.traceDestFile} && rm -f ${this.traceDestFile}`; 181 } 182 183 generateStartTracingCommand(tracingConfig: Uint8Array) { 184 const configBase64 = base64Encode(tracingConfig); 185 const perfettoCmd = `perfetto -c - -o ${this.traceDestFile}`; 186 return `echo '${configBase64}' | base64 -d | ${perfettoCmd}`; 187 } 188} 189