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 {Device} from './connection'; 20import {ConfigMap} from './trace_collection_utils'; 21 22export enum ProxyState { 23 ERROR = 0, 24 CONNECTING = 1, 25 NO_PROXY = 2, 26 INVALID_VERSION = 3, 27 UNAUTH = 4, 28 DEVICES = 5, 29 START_TRACE = 6, 30 END_TRACE = 7, 31 LOAD_DATA = 8, 32} 33 34export enum ProxyEndpoint { 35 DEVICES = '/devices/', 36 START_TRACE = '/start/', 37 END_TRACE = '/end/', 38 ENABLE_CONFIG_TRACE = '/configtrace/', 39 SELECTED_WM_CONFIG_TRACE = '/selectedwmconfigtrace/', 40 SELECTED_SF_CONFIG_TRACE = '/selectedsfconfigtrace/', 41 DUMP = '/dump/', 42 FETCH = '/fetch/', 43 STATUS = '/status/', 44 CHECK_WAYLAND = '/checkwayland/', 45} 46 47// from here, all requests to the proxy are made 48class ProxyRequest { 49 // List of trace we are actively tracing 50 private tracingTraces: string[] | undefined; 51 52 async call( 53 method: string, 54 path: string, 55 onSuccess: ((request: XMLHttpRequest) => void | Promise<void>) | undefined, 56 type?: XMLHttpRequest['responseType'], 57 jsonRequest: any = null 58 ): Promise<void> { 59 return new Promise((resolve) => { 60 const request = new XMLHttpRequest(); 61 const client = proxyClient; 62 request.onreadystatechange = async function () { 63 if (this.readyState !== XMLHttpRequest.DONE) { 64 return; 65 } 66 if (this.status === XMLHttpRequest.UNSENT) { 67 client.setState(ProxyState.NO_PROXY); 68 resolve(); 69 } else if (this.status === 200) { 70 if (this.getResponseHeader('Winscope-Proxy-Version') !== client.VERSION) { 71 client.setState(ProxyState.INVALID_VERSION); 72 resolve(); 73 } else if (onSuccess) { 74 try { 75 await onSuccess(this); 76 } catch (err) { 77 console.error(err); 78 proxyClient.setState( 79 ProxyState.ERROR, 80 `Error handling request response:\n${err}\n\n` + 81 `Request:\n ${request.responseText}` 82 ); 83 resolve(); 84 } 85 } 86 resolve(); 87 } else if (this.status === 403) { 88 client.setState(ProxyState.UNAUTH); 89 resolve(); 90 } else { 91 if (this.responseType === 'text' || !this.responseType) { 92 client.errorText = this.responseText; 93 } else if (this.responseType === 'arraybuffer') { 94 client.errorText = String.fromCharCode.apply(null, new Array(this.response)); 95 } 96 client.setState(ProxyState.ERROR, client.errorText); 97 resolve(); 98 } 99 }; 100 request.responseType = type || ''; 101 request.open(method, client.WINSCOPE_PROXY_URL + path); 102 const lastKey = client.store.get('adb.proxyKey'); 103 if (lastKey !== null) { 104 client.proxyKey = lastKey; 105 } 106 request.setRequestHeader('Winscope-Token', client.proxyKey); 107 if (jsonRequest) { 108 const json = JSON.stringify(jsonRequest); 109 request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); 110 request.send(json); 111 } else { 112 request.send(); 113 } 114 }); 115 } 116 117 async getDevices(view: any) { 118 await proxyRequest.call('GET', ProxyEndpoint.DEVICES, proxyRequest.onSuccessGetDevices); 119 } 120 121 async setEnabledConfig(view: any, req: string[]) { 122 await proxyRequest.call( 123 'POST', 124 `${ProxyEndpoint.ENABLE_CONFIG_TRACE}${view.proxy.selectedDevice}/`, 125 undefined, 126 undefined, 127 req 128 ); 129 } 130 131 async setSelectedConfig(endpoint: ProxyEndpoint, view: any, req: ConfigMap) { 132 await proxyRequest.call( 133 'POST', 134 `${endpoint}${view.proxy.selectedDevice}/`, 135 undefined, 136 undefined, 137 req 138 ); 139 } 140 141 async startTrace(view: any, requestedTraces: string[]) { 142 this.tracingTraces = requestedTraces; 143 await proxyRequest.call( 144 'POST', 145 `${ProxyEndpoint.START_TRACE}${view.proxy.selectedDevice}/`, 146 (request: XMLHttpRequest) => { 147 view.keepAliveTrace(view); 148 }, 149 undefined, 150 requestedTraces 151 ); 152 } 153 154 async endTrace(view: any, progressCallback: OnProgressUpdateType): Promise<void> { 155 const requestedTraces = this.tracingTraces; 156 this.tracingTraces = undefined; 157 if (requestedTraces === undefined) { 158 throw Error('Trace no started before stopping'); 159 } 160 await proxyRequest.call( 161 'POST', 162 `${ProxyEndpoint.END_TRACE}${view.proxy.selectedDevice}/`, 163 async (request: XMLHttpRequest) => { 164 await proxyClient.updateAdbData(requestedTraces, 'trace', progressCallback); 165 } 166 ); 167 } 168 169 async keepTraceAlive(view: any) { 170 await this.call( 171 'GET', 172 `${ProxyEndpoint.STATUS}${view.proxy.selectedDevice}/`, 173 (request: XMLHttpRequest) => { 174 if (request.responseText !== 'True') { 175 view.endTrace(); 176 } else if (view.keep_alive_worker === null) { 177 view.keep_alive_worker = setInterval(view.keepAliveTrace, 1000, view); 178 } 179 } 180 ); 181 } 182 183 async dumpState(view: any, requestedDumps: string[], progressCallback: OnProgressUpdateType) { 184 await proxyRequest.call( 185 'POST', 186 `${ProxyEndpoint.DUMP}${view.proxy.selectedDevice}/`, 187 async (request: XMLHttpRequest) => { 188 await proxyClient.updateAdbData(requestedDumps, 'dump', progressCallback); 189 }, 190 undefined, 191 requestedDumps 192 ); 193 } 194 195 onSuccessGetDevices = (request: XMLHttpRequest) => { 196 const client = proxyClient; 197 try { 198 client.devices = JSON.parse(request.responseText); 199 const last = client.store.get('adb.lastDevice'); 200 if (last && client.devices[last] && client.devices[last].authorised) { 201 client.selectDevice(last); 202 } else { 203 if (client.refresh_worker === null) { 204 client.refresh_worker = setInterval(client.getDevices, 1000); 205 } 206 client.setState(ProxyState.DEVICES); 207 } 208 } catch (err) { 209 console.error(err); 210 client.errorText = request.responseText; 211 client.setState(ProxyState.ERROR, client.errorText); 212 } 213 }; 214 215 async fetchFiles(dev: string, adbParams: AdbParams): Promise<void> { 216 const files = adbParams.files; 217 const idx = adbParams.idx; 218 219 await proxyRequest.call( 220 'GET', 221 `${ProxyEndpoint.FETCH}${dev}/${files[idx]}/`, 222 async (request: XMLHttpRequest) => { 223 try { 224 const enc = new TextDecoder('utf-8'); 225 const resp = enc.decode(request.response); 226 const filesByType = JSON.parse(resp); 227 228 for (const filetype of Object.keys(filesByType)) { 229 const files = filesByType[filetype]; 230 for (const encodedFileBuffer of files) { 231 const buffer = Uint8Array.from(atob(encodedFileBuffer), (c) => c.charCodeAt(0)); 232 const blob = new Blob([buffer]); 233 const newFile = new File([blob], filetype); 234 proxyClient.adbData.push(newFile); 235 } 236 } 237 } catch (error) { 238 proxyClient.setState(ProxyState.ERROR, request.responseText); 239 throw error; 240 } 241 }, 242 'arraybuffer' 243 ); 244 } 245} 246export const proxyRequest = new ProxyRequest(); 247 248interface AdbParams { 249 files: string[]; 250 idx: number; 251 traceType: string; 252} 253 254// stores all the changing variables from proxy and sets up calls from ProxyRequest 255export class ProxyClient { 256 readonly WINSCOPE_PROXY_URL = 'http://localhost:5544'; 257 readonly VERSION = '1.0'; 258 state: ProxyState = ProxyState.CONNECTING; 259 stateChangeListeners: Array<{(param: ProxyState, errorText: string): void}> = []; 260 refresh_worker: NodeJS.Timer | null = null; 261 devices: Device = {}; 262 selectedDevice = ''; 263 errorText = ''; 264 adbData: File[] = []; 265 proxyKey = ''; 266 lastDevice = ''; 267 store = new PersistentStore(); 268 269 setState(state: ProxyState, errorText = '') { 270 this.state = state; 271 this.errorText = errorText; 272 for (const listener of this.stateChangeListeners) { 273 listener(state, errorText); 274 } 275 } 276 277 onProxyChange(fn: (state: ProxyState, errorText: string) => void) { 278 this.removeOnProxyChange(fn); 279 this.stateChangeListeners.push(fn); 280 } 281 282 removeOnProxyChange(removeFn: (state: ProxyState, errorText: string) => void) { 283 this.stateChangeListeners = this.stateChangeListeners.filter((fn) => fn !== removeFn); 284 } 285 286 getDevices() { 287 if (this.state !== ProxyState.DEVICES && this.state !== ProxyState.CONNECTING) { 288 clearInterval(this.refresh_worker!); 289 this.refresh_worker = null; 290 return; 291 } 292 proxyRequest.getDevices(this); 293 } 294 295 selectDevice(device_id: string) { 296 this.selectedDevice = device_id; 297 this.store.add('adb.lastDevice', device_id); 298 this.setState(ProxyState.START_TRACE); 299 } 300 301 async updateAdbData(files: string[], traceType: string, progressCallback: OnProgressUpdateType) { 302 for (let idx = 0; idx < files.length; idx++) { 303 const adbParams = { 304 files, 305 idx, 306 traceType, 307 }; 308 await proxyRequest.fetchFiles(this.selectedDevice, adbParams); 309 progressCallback((100 * (idx + 1)) / files.length); 310 } 311 } 312} 313 314export const proxyClient = new ProxyClient(); 315