• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2022 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 {defer, Deferred} from '../../base/deferred';
18
19import {AdbConnectionImpl} from './adb_connection_impl';
20import {RecordingError} from './recording_error_handling';
21import {
22  ByteStream,
23  OnDisconnectCallback,
24  OnStreamCloseCallback,
25  OnStreamDataCallback,
26} from './recording_interfaces_v2';
27import {
28  ALLOW_USB_DEBUGGING,
29  buildAbdWebsocketCommand,
30  WEBSOCKET_UNABLE_TO_CONNECT,
31} from './recording_utils';
32
33const textDecoder = new _TextDecoder();
34
35export class AdbConnectionOverWebsocket extends AdbConnectionImpl {
36  private streams = new Set<AdbOverWebsocketStream>();
37
38  onDisconnect: OnDisconnectCallback = (_) => {};
39
40  constructor(
41    private deviceSerialNumber: string,
42    private websocketUrl: string,
43  ) {
44    super();
45  }
46
47  shell(cmd: string): Promise<AdbOverWebsocketStream> {
48    return this.openStream('shell:' + cmd);
49  }
50
51  connectSocket(path: string): Promise<AdbOverWebsocketStream> {
52    return this.openStream(path);
53  }
54
55  protected async openStream(
56    destination: string,
57  ): Promise<AdbOverWebsocketStream> {
58    return AdbOverWebsocketStream.create(
59      this.websocketUrl,
60      destination,
61      this.deviceSerialNumber,
62      this.closeStream.bind(this),
63    );
64  }
65
66  // The disconnection for AdbConnectionOverWebsocket is synchronous, but this
67  // method is async to have a common interface with other types of connections
68  // which are async.
69  async disconnect(disconnectMessage?: string): Promise<void> {
70    for (const stream of this.streams) {
71      stream.close();
72    }
73    this.onDisconnect(disconnectMessage);
74  }
75
76  closeStream(stream: AdbOverWebsocketStream): void {
77    if (this.streams.has(stream)) {
78      this.streams.delete(stream);
79    }
80  }
81
82  // There will be no contention for the websocket connection, because it will
83  // communicate with the 'adb server' running on the computer which opened
84  // Perfetto.
85  canConnectWithoutContention(): Promise<boolean> {
86    return Promise.resolve(true);
87  }
88}
89
90// An AdbOverWebsocketStream instantiates a websocket connection to the device.
91// It exposes an API to write commands to this websocket and read its output.
92export class AdbOverWebsocketStream implements ByteStream {
93  private websocket: WebSocket;
94  // commandSentSignal gets resolved if we successfully connect to the device
95  // and send the command this socket wraps. commandSentSignal gets rejected if
96  // we fail to connect to the device.
97  private commandSentSignal = defer<AdbOverWebsocketStream>();
98  // We store a promise for each messge while the message is processed.
99  // This way, if the websocket server closes the connection, we first process
100  // all previously received messages and only afterwards disconnect.
101  // An application is when the stream wraps a shell command. The websocket
102  // server will reply and then immediately disconnect.
103  private messageProcessedSignals: Set<Deferred<void>> = new Set();
104
105  private _isConnected = false;
106  private onStreamDataCallbacks: OnStreamDataCallback[] = [];
107  private onStreamCloseCallbacks: OnStreamCloseCallback[] = [];
108
109  private constructor(
110    websocketUrl: string,
111    destination: string,
112    deviceSerialNumber: string,
113    private removeFromConnection: (stream: AdbOverWebsocketStream) => void,
114  ) {
115    this.websocket = new WebSocket(websocketUrl);
116
117    this.websocket.onopen = this.onOpen.bind(this, deviceSerialNumber);
118    this.websocket.onmessage = this.onMessage.bind(this, destination);
119    // The websocket may be closed by the websocket server. This happens
120    // for instance when we get the full result of a shell command.
121    this.websocket.onclose = this.onClose.bind(this);
122  }
123
124  addOnStreamDataCallback(onStreamData: OnStreamDataCallback) {
125    this.onStreamDataCallbacks.push(onStreamData);
126  }
127
128  addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback) {
129    this.onStreamCloseCallbacks.push(onStreamClose);
130  }
131
132  // Used by the connection object to signal newly received data, not exposed
133  // in the interface.
134  signalStreamData(data: Uint8Array): void {
135    for (const onStreamData of this.onStreamDataCallbacks) {
136      onStreamData(data);
137    }
138  }
139
140  // Used by the connection object to signal the stream is closed, not exposed
141  // in the interface.
142  signalStreamClosed(): void {
143    for (const onStreamClose of this.onStreamCloseCallbacks) {
144      onStreamClose();
145    }
146    this.onStreamDataCallbacks = [];
147    this.onStreamCloseCallbacks = [];
148  }
149
150  // We close the websocket and notify the AdbConnection to remove this stream.
151  close(): void {
152    // If the websocket connection is still open (ie. the close did not
153    // originate from the server), we close the websocket connection.
154    if (this.websocket.readyState === this.websocket.OPEN) {
155      this.websocket.close();
156      // We remove the 'onclose' callback so the 'close' method doesn't get
157      // executed twice.
158      this.websocket.onclose = null;
159    }
160    this._isConnected = false;
161    this.removeFromConnection(this);
162    this.signalStreamClosed();
163  }
164
165  // For websocket, the teardown happens synchronously.
166  async closeAndWaitForTeardown(): Promise<void> {
167    this.close();
168  }
169
170  write(msg: string | Uint8Array): void {
171    this.websocket.send(msg);
172  }
173
174  isConnected(): boolean {
175    return this._isConnected;
176  }
177
178  private async onOpen(deviceSerialNumber: string): Promise<void> {
179    this.websocket.send(
180      buildAbdWebsocketCommand(`host:transport:${deviceSerialNumber}`),
181    );
182  }
183
184  private async onMessage(
185    destination: string,
186    evt: MessageEvent,
187  ): Promise<void> {
188    const messageProcessed = defer<void>();
189    this.messageProcessedSignals.add(messageProcessed);
190    try {
191      if (!this._isConnected) {
192        const txt = await evt.data.text();
193        const prefix = txt.substr(0, 4);
194        if (prefix === 'OKAY') {
195          this._isConnected = true;
196          this.websocket.send(buildAbdWebsocketCommand(destination));
197          this.commandSentSignal.resolve(this);
198        } else if (prefix === 'FAIL' && txt.includes('device unauthorized')) {
199          this.commandSentSignal.reject(
200            new RecordingError(ALLOW_USB_DEBUGGING),
201          );
202          this.close();
203        } else {
204          this.commandSentSignal.reject(
205            new RecordingError(WEBSOCKET_UNABLE_TO_CONNECT),
206          );
207          this.close();
208        }
209      } else {
210        // Upon a successful connection we first receive an 'OKAY' message.
211        // After that, we receive messages with traced binary payloads.
212        const arrayBufferResponse = await evt.data.arrayBuffer();
213        if (textDecoder.decode(arrayBufferResponse) !== 'OKAY') {
214          this.signalStreamData(new Uint8Array(arrayBufferResponse));
215        }
216      }
217      messageProcessed.resolve();
218    } finally {
219      this.messageProcessedSignals.delete(messageProcessed);
220    }
221  }
222
223  private async onClose(): Promise<void> {
224    // Wait for all messages to be processed before closing the connection.
225    await Promise.allSettled(this.messageProcessedSignals);
226    this.close();
227  }
228
229  static create(
230    websocketUrl: string,
231    destination: string,
232    deviceSerialNumber: string,
233    removeFromConnection: (stream: AdbOverWebsocketStream) => void,
234  ): Promise<AdbOverWebsocketStream> {
235    return new AdbOverWebsocketStream(
236      websocketUrl,
237      destination,
238      deviceSerialNumber,
239      removeFromConnection,
240    ).commandSentSignal;
241  }
242}
243