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 {extractTraceConfig} from '../base/extract_utils';
18import {base64Encode} from '../base/string_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