1// Copyright (C) 2019 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 protos from '../protos'; 16import {fetchWithTimeout} from '../base/http_utils'; 17import {assertExists} from '../base/logging'; 18import {EngineBase} from '../trace_processor/engine'; 19 20const RPC_CONNECT_TIMEOUT_MS = 2000; 21 22export interface HttpRpcState { 23 connected: boolean; 24 status?: protos.StatusResult; 25 failure?: string; 26} 27 28export class HttpRpcEngine extends EngineBase { 29 readonly mode = 'HTTP_RPC'; 30 readonly id: string; 31 private requestQueue = new Array<Uint8Array>(); 32 private websocket?: WebSocket; 33 private connected = false; 34 private disposed = false; 35 36 // Can be changed by frontend/index.ts when passing ?rpc_port=1234 . 37 static rpcPort = '9001'; 38 39 constructor(id: string) { 40 super(); 41 this.id = id; 42 } 43 44 rpcSendRequestBytes(data: Uint8Array): void { 45 if (this.websocket === undefined) { 46 if (this.disposed) return; 47 const wsUrl = `ws://${HttpRpcEngine.hostAndPort}/websocket`; 48 this.websocket = new WebSocket(wsUrl); 49 this.websocket.onopen = () => this.onWebsocketConnected(); 50 this.websocket.onmessage = (e) => this.onWebsocketMessage(e); 51 this.websocket.onclose = (e) => this.onWebsocketClosed(e); 52 this.websocket.onerror = (e) => 53 super.fail( 54 `WebSocket error rs=${(e.target as WebSocket)?.readyState} (ERR:ws)`, 55 ); 56 } 57 58 if (this.connected) { 59 this.websocket.send(data); 60 } else { 61 this.requestQueue.push(data); // onWebsocketConnected() will flush this. 62 } 63 } 64 65 private onWebsocketConnected() { 66 for (;;) { 67 const queuedMsg = this.requestQueue.shift(); 68 if (queuedMsg === undefined) break; 69 assertExists(this.websocket).send(queuedMsg); 70 } 71 this.connected = true; 72 } 73 74 private onWebsocketClosed(e: CloseEvent) { 75 if (this.disposed) return; 76 if (e.code === 1006 && this.connected) { 77 // On macbooks the act of closing the lid / suspending often causes socket 78 // disconnections. Try to gracefully re-connect. 79 console.log('Websocket closed, reconnecting'); 80 this.websocket = undefined; 81 this.connected = false; 82 this.rpcSendRequestBytes(new Uint8Array()); // Triggers a reconnection. 83 } else { 84 super.fail(`Websocket closed (${e.code}: ${e.reason}) (ERR:ws)`); 85 } 86 } 87 88 private onWebsocketMessage(e: MessageEvent) { 89 assertExists(e.data as Blob) 90 .arrayBuffer() 91 .then((buf) => { 92 super.onRpcResponseBytes(new Uint8Array(buf)); 93 }); 94 } 95 96 static async checkConnection(): Promise<HttpRpcState> { 97 const RPC_URL = `http://${HttpRpcEngine.hostAndPort}/`; 98 const httpRpcState: HttpRpcState = {connected: false}; 99 console.info( 100 `It's safe to ignore the ERR_CONNECTION_REFUSED on ${RPC_URL} below. ` + 101 `That might happen while probing the external native accelerator. The ` + 102 `error is non-fatal and unlikely to be the culprit for any UI bug.`, 103 ); 104 try { 105 const resp = await fetchWithTimeout( 106 RPC_URL + 'status', 107 {method: 'post', cache: 'no-cache'}, 108 RPC_CONNECT_TIMEOUT_MS, 109 ); 110 if (resp.status !== 200) { 111 httpRpcState.failure = `${resp.status} - ${resp.statusText}`; 112 } else { 113 const buf = new Uint8Array(await resp.arrayBuffer()); 114 httpRpcState.connected = true; 115 httpRpcState.status = protos.StatusResult.decode(buf); 116 } 117 } catch (err) { 118 httpRpcState.failure = `${err}`; 119 } 120 return httpRpcState; 121 } 122 123 static get hostAndPort() { 124 return `127.0.0.1:${HttpRpcEngine.rpcPort}`; 125 } 126 127 [Symbol.dispose]() { 128 this.disposed = true; 129 const websocket = this.websocket; 130 this.websocket = undefined; 131 websocket?.close(); 132 } 133} 134