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