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 LocalStore from '../localstore.js'; 18import {FILE_DECODERS, FILE_TYPES} from '../decode.js'; 19 20export enum ProxyState { 21 ERROR = 0, 22 CONNECTING = 1, 23 NO_PROXY = 2, 24 INVALID_VERSION = 3, 25 UNAUTH = 4, 26 DEVICES = 5, 27 START_TRACE = 6, 28 END_TRACE = 7, 29 LOAD_DATA = 8, 30}; 31 32export enum ProxyEndpoint { 33 DEVICES = '/devices/', 34 START_TRACE = '/start/', 35 END_TRACE = '/end/', 36 CONFIG_TRACE = '/configtrace/', 37 SELECTED_WM_CONFIG_TRACE = '/selectedwmconfigtrace/', 38 SELECTED_SF_CONFIG_TRACE = '/selectedsfconfigtrace/', 39 DUMP = '/dump/', 40 FETCH = '/fetch/', 41 STATUS = '/status/', 42 CHECK_WAYLAND = '/checkwayland/', 43}; 44 45const proxyFileTypeAdapter = { 46 'window_trace': FILE_TYPES.WINDOW_MANAGER_TRACE, 47 'accessibility_trace': FILE_TYPES.ACCESSIBILITY_TRACE, 48 'layers_trace': FILE_TYPES.SURFACE_FLINGER_TRACE, 49 'wl_trace': FILE_TYPES.WAYLAND_TRACE, 50 'layers_dump': FILE_TYPES.SURFACE_FLINGER_DUMP, 51 'window_dump': FILE_TYPES.WINDOW_MANAGER_DUMP, 52 'wl_dump': FILE_TYPES.WAYLAND_DUMP, 53 'screen_recording': FILE_TYPES.SCREEN_RECORDING, 54 'transactions': FILE_TYPES.TRANSACTIONS_TRACE, 55 'transactions_legacy': FILE_TYPES.TRANSACTIONS_TRACE_LEGACY, 56 'proto_log': FILE_TYPES.PROTO_LOG, 57 'system_ui_trace': FILE_TYPES.SYSTEM_UI, 58 'launcher_trace': FILE_TYPES.LAUNCHER, 59 'ime_trace_clients': FILE_TYPES.IME_TRACE_CLIENTS, 60 'ime_trace_service': FILE_TYPES.IME_TRACE_SERVICE, 61 'ime_trace_managerservice': FILE_TYPES.IME_TRACE_MANAGERSERVICE, 62}; 63 64class ProxyClient { 65 readonly WINSCOPE_PROXY_URL = 'http://localhost:5544'; 66 readonly VERSION = '0.8'; 67 68 store:LocalStore = LocalStore('adb', { 69 proxyKey: '', 70 lastDevice: ''}); 71 72 state:ProxyState = ProxyState.CONNECTING; 73 stateChangeListeners:{(param:ProxyState, errorText:String): void;}[] = []; 74 refresh_worker: NodeJS.Timer = null; 75 devices = {}; 76 selectedDevice = "" 77 errorText:String = "" 78 79 call(method, path, view, onSuccess, type = null, jsonRequest = null) { 80 const request = new XMLHttpRequest(); 81 let client = this; 82 request.onreadystatechange = function() { 83 if (this.readyState !== 4) { 84 return; 85 } 86 if (this.status === 0) { 87 client.setState(ProxyState.NO_PROXY); 88 } else if (this.status === 200) { 89 if (this.getResponseHeader('Winscope-Proxy-Version') !== client.VERSION) { 90 client.setState(ProxyState.INVALID_VERSION); 91 } else if (onSuccess) { 92 onSuccess(this, view); 93 } 94 } else if (this.status === 403) { 95 client.setState(ProxyState.UNAUTH); 96 } else { 97 if (this.responseType === 'text' || !this.responseType) { 98 client.errorText = this.responseText; 99 } else if (this.responseType === 'arraybuffer') { 100 client.errorText = String.fromCharCode.apply(null, new Uint8Array(this.response)); 101 } 102 client.setState(ProxyState.ERROR); 103 } 104 }; 105 request.responseType = type || ""; 106 request.open(method, client.WINSCOPE_PROXY_URL + path); 107 request.setRequestHeader('Winscope-Token', client.store.proxyKey); 108 if (jsonRequest) { 109 const json = JSON.stringify(jsonRequest); 110 request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); 111 request.send(json); 112 } else { 113 request.send(); 114 } 115 } 116 117 setState(state:ProxyState, errorText:String = "") { 118 this.state = state; 119 this.errorText = errorText; 120 for (let listener of this.stateChangeListeners) { 121 listener(state, errorText); 122 } 123 } 124 125 onStateChange(fn: (state:ProxyState, errorText:String) => void) { 126 this.removeOnStateChange(fn); 127 this.stateChangeListeners.push(fn); 128 } 129 130 removeOnStateChange(removeFn: (state:ProxyState, errorText:String) => void) { 131 this.stateChangeListeners = this.stateChangeListeners.filter(fn => fn !== removeFn); 132 } 133 134 getDevices() { 135 if (this.state !== ProxyState.DEVICES && this.state !== ProxyState.CONNECTING) { 136 clearInterval(this.refresh_worker); 137 this.refresh_worker = null; 138 return; 139 } 140 let client = this; 141 this.call('GET', ProxyEndpoint.DEVICES, this, function(request, view) { 142 try { 143 client.devices = JSON.parse(request.responseText); 144 if (client.store.lastDevice && client.devices[client.store.lastDevice] && 145 client.devices[client.store.lastDevice].authorised) { 146 client.selectDevice(client.store.lastDevice); 147 } else { 148 if (client.refresh_worker === null) { 149 client.refresh_worker = setInterval(client.getDevices, 1000); 150 } 151 client.setState(ProxyState.DEVICES); 152 } 153 } catch (err) { 154 console.error(err); 155 client.errorText = request.responseText; 156 client.setState(ProxyState.ERROR); 157 } 158 }); 159 } 160 161 selectDevice(device_id) { 162 this.selectedDevice = device_id; 163 this.store.lastDevice = device_id; 164 this.setState(ProxyState.START_TRACE); 165 } 166 167 deviceId() { 168 return this.selectedDevice; 169 } 170 171 resetLastDevice() { 172 this.store.lastDevice = ''; 173 } 174 175 loadFile(files, idx, traceType, view) { 176 let client = this; 177 this.call('GET', `${ProxyEndpoint.FETCH}${proxyClient.deviceId()}/${files[idx]}/`, view, 178 (request, view) => { 179 try { 180 const enc = new TextDecoder('utf-8'); 181 const resp = enc.decode(request.response); 182 const filesByType = JSON.parse(resp); 183 184 for (const filetype in filesByType) { 185 if (filesByType.hasOwnProperty(filetype)) { 186 const files = filesByType[filetype]; 187 const fileDecoder = FILE_DECODERS[proxyFileTypeAdapter[filetype]]; 188 189 for (const encodedFileBuffer of files) { 190 const buffer = Uint8Array.from(atob(encodedFileBuffer), (c) => c.charCodeAt(0)); 191 const data = fileDecoder.decoder(buffer, fileDecoder.decoderParams, 192 fileDecoder.name, view.store); 193 view.dataFiles.push(data); 194 view.loadProgress = 100 * (idx + 1) / files.length; // TODO: Update this 195 } 196 } 197 } 198 199 if (idx < files.length - 1) { 200 client.loadFile(files, idx + 1, traceType, view); 201 } else { 202 const currentDate = new Date().toISOString(); 203 view.$emit('dataReady', 204 `winscope-${traceType}-${currentDate}`, 205 view.dataFiles); 206 } 207 } catch (err) { 208 console.error(err); 209 client.setState(ProxyState.ERROR, err); 210 } 211 }, 'arraybuffer'); 212 } 213 214} 215 216export const proxyClient = new ProxyClient(); 217