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