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 {fetchWithTimeout} from '../base/http_utils'; 16import {assertExists} from '../base/logging'; 17import {StatusResult} from '../protos'; 18import {EngineBase, LoadingTracker} from '../trace_processor/engine'; 19 20const RPC_CONNECT_TIMEOUT_MS = 2000; 21 22export interface HttpRpcState { 23 connected: boolean; 24 status?: StatusResult; 25 failure?: string; 26} 27 28export class HttpRpcEngine extends EngineBase { 29 readonly id: string; 30 errorHandler: (err: string) => void = () => {}; 31 private requestQueue = new Array<Uint8Array>(); 32 private websocket?: WebSocket; 33 private connected = false; 34 35 // Can be changed by frontend/index.ts when passing ?rpc_port=1234 . 36 static rpcPort = '9001'; 37 38 constructor(id: string, loadingTracker?: LoadingTracker) { 39 super(loadingTracker); 40 this.id = id; 41 } 42 43 rpcSendRequestBytes(data: Uint8Array): void { 44 if (this.websocket === undefined) { 45 const wsUrl = `ws://${HttpRpcEngine.hostAndPort}/websocket`; 46 this.websocket = new WebSocket(wsUrl); 47 this.websocket.onopen = () => this.onWebsocketConnected(); 48 this.websocket.onmessage = (e) => this.onWebsocketMessage(e); 49 this.websocket.onclose = (e) => 50 this.errorHandler(`Websocket closed (${e.code}: ${e.reason})`); 51 this.websocket.onerror = (e) => 52 this.errorHandler(`WebSocket error: ${e}`); 53 } 54 55 if (this.connected) { 56 this.websocket.send(data); 57 } else { 58 this.requestQueue.push(data); // onWebsocketConnected() will flush this. 59 } 60 } 61 62 private onWebsocketConnected() { 63 for (;;) { 64 const queuedMsg = this.requestQueue.shift(); 65 if (queuedMsg === undefined) break; 66 assertExists(this.websocket).send(queuedMsg); 67 } 68 this.connected = true; 69 } 70 71 private onWebsocketMessage(e: MessageEvent) { 72 assertExists(e.data as Blob) 73 .arrayBuffer() 74 .then((buf) => { 75 super.onRpcResponseBytes(new Uint8Array(buf)); 76 }); 77 } 78 79 static async checkConnection(): Promise<HttpRpcState> { 80 const RPC_URL = `http://${HttpRpcEngine.hostAndPort}/`; 81 const httpRpcState: HttpRpcState = {connected: false}; 82 console.info( 83 `It's safe to ignore the ERR_CONNECTION_REFUSED on ${RPC_URL} below. ` + 84 `That might happen while probing the external native accelerator. The ` + 85 `error is non-fatal and unlikely to be the culprit for any UI bug.`, 86 ); 87 try { 88 const resp = await fetchWithTimeout( 89 RPC_URL + 'status', 90 {method: 'post', cache: 'no-cache'}, 91 RPC_CONNECT_TIMEOUT_MS, 92 ); 93 if (resp.status !== 200) { 94 httpRpcState.failure = `${resp.status} - ${resp.statusText}`; 95 } else { 96 const buf = new Uint8Array(await resp.arrayBuffer()); 97 httpRpcState.connected = true; 98 httpRpcState.status = StatusResult.decode(buf); 99 } 100 } catch (err) { 101 httpRpcState.failure = `${err}`; 102 } 103 return httpRpcState; 104 } 105 106 static get hostAndPort() { 107 return `127.0.0.1:${HttpRpcEngine.rpcPort}`; 108 } 109} 110