xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.RecordTrace/adb_base_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 {exists} from '../../base/utils';
16import {RecordingState, RecordingTarget, isAdbTarget} from './state';
17import {
18  extractDurationFromTraceConfig,
19  extractTraceConfig,
20} from './trace_config_utils';
21import {Adb} from './adb_interfaces';
22import {ReadBuffersResponse} from './consumer_port_types';
23import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
24
25export enum AdbConnectionState {
26  READY_TO_CONNECT,
27  AUTH_IN_PROGRESS,
28  CONNECTED,
29  CLOSED,
30}
31
32interface Command {
33  method: string;
34  params: Uint8Array;
35}
36
37export abstract class AdbBaseConsumerPort extends RpcConsumerPort {
38  // Contains the commands sent while the authentication is in progress. They
39  // will all be executed afterwards. If the device disconnects, they are
40  // removed.
41  private commandQueue: Command[] = [];
42
43  protected adb: Adb;
44  protected state = AdbConnectionState.READY_TO_CONNECT;
45  protected device?: USBDevice;
46  protected recState: RecordingState;
47
48  protected constructor(
49    adb: Adb,
50    consumer: Consumer,
51    recState: RecordingState,
52  ) {
53    super(consumer);
54    this.adb = adb;
55    this.recState = recState;
56  }
57
58  async handleCommand(method: string, params: Uint8Array) {
59    try {
60      if (method === 'FreeBuffers') {
61        // When we finish tracing, we disconnect the adb device interface.
62        // Otherwise, we will keep holding the device interface and won't allow
63        // adb to access it. https://wicg.github.io/webusb/#abusing-a-device
64        // "Lastly, since USB devices are unable to distinguish requests from
65        // multiple sources, operating systems only allow a USB interface to
66        // have a single owning user-space or kernel-space driver."
67        this.state = AdbConnectionState.CLOSED;
68        await this.adb.disconnect();
69      } else if (method === 'EnableTracing') {
70        this.state = AdbConnectionState.READY_TO_CONNECT;
71      }
72
73      if (this.state === AdbConnectionState.CLOSED) return;
74
75      this.commandQueue.push({method, params});
76
77      if (
78        this.state === AdbConnectionState.READY_TO_CONNECT ||
79        this.deviceDisconnected()
80      ) {
81        this.state = AdbConnectionState.AUTH_IN_PROGRESS;
82        this.device = await this.findDevice(this.recState.recordingTarget);
83        if (!this.device) {
84          this.state = AdbConnectionState.READY_TO_CONNECT;
85          const target = this.recState.recordingTarget;
86          throw Error(
87            `Device with serial ${
88              isAdbTarget(target) ? target.serial : 'n/a'
89            } not found.`,
90          );
91        }
92
93        this.sendStatus(`Please allow USB debugging on device.
94          If you press cancel, reload the page.`);
95
96        await this.adb.connect(this.device);
97
98        // During the authentication the device may have been disconnected.
99        if (!this.recState.recordingInProgress || this.deviceDisconnected()) {
100          throw Error('Recording not in progress after adb authorization.');
101        }
102
103        this.state = AdbConnectionState.CONNECTED;
104        this.sendStatus('Device connected.');
105      }
106
107      if (this.state === AdbConnectionState.AUTH_IN_PROGRESS) return;
108
109      console.assert(this.state === AdbConnectionState.CONNECTED);
110
111      for (const cmd of this.commandQueue) this.invoke(cmd.method, cmd.params);
112
113      this.commandQueue = [];
114    } catch (e) {
115      this.commandQueue = [];
116      this.state = AdbConnectionState.READY_TO_CONNECT;
117      this.sendErrorMessage(e.message);
118    }
119  }
120
121  private deviceDisconnected() {
122    return !this.device || !this.device.opened;
123  }
124
125  setDurationStatus(enableTracingProto: Uint8Array) {
126    const traceConfigProto = extractTraceConfig(enableTracingProto);
127    if (!traceConfigProto) return;
128    const duration = extractDurationFromTraceConfig(traceConfigProto);
129    this.sendStatus(
130      `Recording in progress${
131        exists(duration) ? ' for ' + duration.toString() + ' ms' : ''
132      }...`,
133    );
134  }
135
136  abstract invoke(method: string, argsProto: Uint8Array): void;
137
138  generateChunkReadResponse(
139    data: Uint8Array,
140    last = false,
141  ): ReadBuffersResponse {
142    return {
143      type: 'ReadBuffersResponse',
144      slices: [{data, lastSliceForPacket: last}],
145    };
146  }
147
148  async findDevice(
149    connectedDevice: RecordingTarget,
150  ): Promise<USBDevice | undefined> {
151    if (!('usb' in navigator)) return undefined;
152    if (!isAdbTarget(connectedDevice)) return undefined;
153    const devices = await navigator.usb.getDevices();
154    return devices.find((d) => d.serialNumber === connectedDevice.serial);
155  }
156}
157