xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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