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