1/* 2 * Copyright 2022, The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {OnProgressUpdateType} from 'common/function_utils'; 18import {PersistentStore} from 'common/persistent_store'; 19import {OnRequestSuccessCallback} from './on_request_success_callback'; 20import {ConfigMap} from './trace_collection_utils'; 21 22export interface Device { 23 [key: string]: DeviceProperties; 24} 25 26export interface DeviceProperties { 27 authorised: boolean; 28 model: string; 29} 30 31export enum ProxyState { 32 ERROR, 33 CONNECTING, 34 NO_PROXY, 35 INVALID_VERSION, 36 UNAUTH, 37 DEVICES, 38 START_TRACE, 39 END_TRACE, 40 LOAD_DATA, 41 STARTING_TRACE, 42} 43 44export enum ProxyEndpoint { 45 DEVICES = '/devices/', 46 START_TRACE = '/start/', 47 END_TRACE = '/end/', 48 ENABLE_CONFIG_TRACE = '/configtrace/', 49 SELECTED_WM_CONFIG_TRACE = '/selectedwmconfigtrace/', 50 SELECTED_SF_CONFIG_TRACE = '/selectedsfconfigtrace/', 51 DUMP = '/dump/', 52 FETCH = '/fetch/', 53 STATUS = '/status/', 54 CHECK_WAYLAND = '/checkwayland/', 55} 56 57// from here, all requests to the proxy are made 58class ProxyRequest { 59 // List of trace we are actively tracing 60 private tracingTraces: string[] | undefined; 61 62 async call( 63 method: string, 64 path: string, 65 onSuccess: OnRequestSuccessCallback | undefined, 66 type?: XMLHttpRequest['responseType'], 67 jsonRequest?: object, 68 ): Promise<void> { 69 return new Promise((resolve) => { 70 const request = new XMLHttpRequest(); 71 const client = proxyClient; 72 request.onreadystatechange = async function () { 73 if (this.readyState !== XMLHttpRequest.DONE) { 74 return; 75 } 76 if (this.status === XMLHttpRequest.UNSENT) { 77 client.setState(ProxyState.NO_PROXY); 78 resolve(); 79 } else if (this.status === 200) { 80 if ( 81 !client.areVersionsCompatible( 82 this.getResponseHeader('Winscope-Proxy-Version'), 83 ) 84 ) { 85 client.setState(ProxyState.INVALID_VERSION); 86 resolve(); 87 } else if (onSuccess) { 88 try { 89 await onSuccess(this); 90 } catch (err) { 91 console.error(err); 92 proxyClient.setState( 93 ProxyState.ERROR, 94 `Error handling request response:\n${err}\n\n` + 95 `Request:\n ${request.responseText}`, 96 ); 97 resolve(); 98 } 99 } 100 resolve(); 101 } else if (this.status === 403) { 102 client.setState(ProxyState.UNAUTH); 103 resolve(); 104 } else { 105 if (this.responseType === 'text' || !this.responseType) { 106 client.errorText = this.responseText; 107 } else if (this.responseType === 'arraybuffer') { 108 client.errorText = String.fromCharCode.apply( 109 null, 110 new Array(this.response), 111 ); 112 } 113 client.setState(ProxyState.ERROR, client.errorText); 114 resolve(); 115 } 116 }; 117 request.responseType = type || ''; 118 request.open(method, client.WINSCOPE_PROXY_URL + path); 119 const lastKey = client.store.get('adb.proxyKey'); 120 if (lastKey !== undefined) { 121 client.proxyKey = lastKey; 122 } 123 request.setRequestHeader('Winscope-Token', client.proxyKey); 124 if (jsonRequest) { 125 const json = JSON.stringify(jsonRequest); 126 request.setRequestHeader( 127 'Content-Type', 128 'application/json;charset=UTF-8', 129 ); 130 request.send(json); 131 } else { 132 request.send(); 133 } 134 }); 135 } 136 137 async getDevices() { 138 await proxyRequest.call( 139 'GET', 140 ProxyEndpoint.DEVICES, 141 proxyRequest.onSuccessGetDevices, 142 ); 143 } 144 145 async setEnabledConfig(view: ProxyClient, req: string[]) { 146 await proxyRequest.call( 147 'POST', 148 `${ProxyEndpoint.ENABLE_CONFIG_TRACE}${view.selectedDevice}/`, 149 undefined, 150 undefined, 151 req, 152 ); 153 } 154 155 async setSelectedConfig( 156 endpoint: ProxyEndpoint, 157 view: ProxyClient, 158 req: ConfigMap, 159 ) { 160 await proxyRequest.call( 161 'POST', 162 `${endpoint}${view.selectedDevice}/`, 163 undefined, 164 undefined, 165 req, 166 ); 167 } 168 169 async startTrace( 170 view: ProxyClient, 171 requestedTraces: string[], 172 onSuccessStartTrace: OnRequestSuccessCallback, 173 ) { 174 this.tracingTraces = requestedTraces; 175 await proxyRequest.call( 176 'POST', 177 `${ProxyEndpoint.START_TRACE}${view.selectedDevice}/`, 178 onSuccessStartTrace, 179 undefined, 180 requestedTraces, 181 ); 182 } 183 184 async endTrace( 185 view: ProxyClient, 186 progressCallback: OnProgressUpdateType, 187 ): Promise<void> { 188 const requestedTraces = this.tracingTraces; 189 this.tracingTraces = undefined; 190 if (requestedTraces === undefined) { 191 throw Error('Trace not started before stopping'); 192 } 193 await proxyRequest.call( 194 'POST', 195 `${ProxyEndpoint.END_TRACE}${view.selectedDevice}/`, 196 async (request: XMLHttpRequest) => { 197 await proxyClient.updateAdbData( 198 requestedTraces, 199 'trace', 200 progressCallback, 201 ); 202 }, 203 ); 204 } 205 206 async keepTraceAlive( 207 view: ProxyClient, 208 onSuccessKeepTraceAlive: OnRequestSuccessCallback, 209 ) { 210 await proxyRequest.call( 211 'GET', 212 `${ProxyEndpoint.STATUS}${view.selectedDevice}/`, 213 onSuccessKeepTraceAlive, 214 ); 215 } 216 217 async dumpState( 218 view: ProxyClient, 219 requestedDumps: string[], 220 progressCallback: OnProgressUpdateType, 221 ) { 222 await proxyRequest.call( 223 'POST', 224 `${ProxyEndpoint.DUMP}${view.selectedDevice}/`, 225 async (request: XMLHttpRequest) => { 226 await proxyClient.updateAdbData( 227 requestedDumps, 228 'dump', 229 progressCallback, 230 ); 231 }, 232 undefined, 233 requestedDumps, 234 ); 235 } 236 237 async fetchFiles(dev: string, adbParams: AdbParams): Promise<void> { 238 const files = adbParams.files; 239 const idx = adbParams.idx; 240 241 await proxyRequest.call( 242 'GET', 243 `${ProxyEndpoint.FETCH}${dev}/${files[idx]}/`, 244 this.onSuccessFetchFiles, 245 'arraybuffer', 246 ); 247 } 248 249 private onSuccessFetchFiles: OnRequestSuccessCallback = async ( 250 request: XMLHttpRequest, 251 ) => { 252 try { 253 const enc = new TextDecoder('utf-8'); 254 const resp = enc.decode(request.response); 255 const filesByType = JSON.parse(resp); 256 257 for (const filetype of Object.keys(filesByType)) { 258 const files = filesByType[filetype]; 259 for (const encodedFileBuffer of files) { 260 const buffer = Uint8Array.from(atob(encodedFileBuffer), (c) => 261 c.charCodeAt(0), 262 ); 263 const blob = new Blob([buffer]); 264 const newFile = new File([blob], filetype); 265 proxyClient.adbData.push(newFile); 266 } 267 } 268 } catch (error) { 269 proxyClient.setState(ProxyState.ERROR, request.responseText); 270 throw error; 271 } 272 }; 273 274 private onSuccessGetDevices: OnRequestSuccessCallback = async ( 275 request: XMLHttpRequest, 276 ) => { 277 const client = proxyClient; 278 try { 279 client.devices = JSON.parse(request.responseText); 280 const last = client.store.get('adb.lastDevice'); 281 if (last && client.devices[last] && client.devices[last].authorised) { 282 await client.selectDevice(last); 283 } else { 284 client.setState(ProxyState.DEVICES); 285 } 286 if (client.refresh_worker === undefined) { 287 client.refresh_worker = setInterval(client.getDevices, 1000); 288 } 289 } catch (err) { 290 console.error(err); 291 client.errorText = request.responseText; 292 client.setState(ProxyState.ERROR, client.errorText); 293 } 294 }; 295} 296export const proxyRequest = new ProxyRequest(); 297 298interface AdbParams { 299 files: string[]; 300 idx: number; 301 traceType: string; 302} 303 304// stores all the changing variables from proxy and sets up calls from ProxyRequest 305export class ProxyClient { 306 readonly WINSCOPE_PROXY_URL = 'http://localhost:5544'; 307 readonly VERSION = '2.1.1'; 308 state: ProxyState = ProxyState.CONNECTING; 309 stateChangeListeners: Array<{ 310 (param: ProxyState, errorText: string): Promise<void>; 311 }> = []; 312 refresh_worker: NodeJS.Timeout | undefined; 313 devices: Device = {}; 314 selectedDevice = ''; 315 errorText = ''; 316 adbData: File[] = []; 317 proxyKey = ''; 318 lastDevice = ''; 319 store = new PersistentStore(); 320 321 async setState(state: ProxyState, errorText = '') { 322 this.state = state; 323 this.errorText = errorText; 324 for (const listener of this.stateChangeListeners) { 325 await listener(state, errorText); 326 } 327 } 328 329 onProxyChange(fn: (state: ProxyState, errorText: string) => Promise<void>) { 330 this.removeOnProxyChange(fn); 331 this.stateChangeListeners.push(fn); 332 } 333 334 removeOnProxyChange( 335 removeFn: (state: ProxyState, errorText: string) => Promise<void>, 336 ) { 337 this.stateChangeListeners = this.stateChangeListeners.filter( 338 (fn) => fn !== removeFn, 339 ); 340 } 341 342 async getDevices() { 343 if ( 344 proxyClient.state !== ProxyState.DEVICES && 345 proxyClient.state !== ProxyState.CONNECTING && 346 proxyClient.state !== ProxyState.START_TRACE 347 ) { 348 if (proxyClient.refresh_worker !== undefined) { 349 clearInterval(proxyClient.refresh_worker); 350 proxyClient.refresh_worker = undefined; 351 } 352 return; 353 } 354 proxyRequest.getDevices(); 355 } 356 357 async selectDevice(device_id: string) { 358 this.selectedDevice = device_id; 359 this.store.add('adb.lastDevice', device_id); 360 this.setState(ProxyState.START_TRACE); 361 } 362 363 async updateAdbData( 364 files: string[], 365 traceType: string, 366 progressCallback: OnProgressUpdateType, 367 ) { 368 for (let idx = 0; idx < files.length; idx++) { 369 const adbParams = { 370 files, 371 idx, 372 traceType, 373 }; 374 await proxyRequest.fetchFiles(this.selectedDevice, adbParams); 375 progressCallback((100 * (idx + 1)) / files.length); 376 } 377 } 378 379 areVersionsCompatible(proxyVersion: string | null): boolean { 380 if (!proxyVersion) return false; 381 const [proxyMajor, proxyMinor, proxyPatch] = proxyVersion 382 .split('.') 383 .map((s) => Number(s)); 384 const [clientMajor, clientMinor, clientPatch] = this.VERSION.split('.').map( 385 (s) => Number(s), 386 ); 387 388 if (proxyMajor !== clientMajor) { 389 return false; 390 } 391 392 if (proxyMinor === clientMinor) { 393 // Check patch number to ensure user has deployed latest bug fixes 394 return proxyPatch >= clientPatch; 395 } 396 397 return proxyMinor > clientMinor; 398 } 399} 400 401export const proxyClient = new ProxyClient(); 402