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 {defer, Deferred} from '../base/deferred';
16import {fetchWithTimeout} from '../base/http_utils';
17import {assertExists, assertTrue} from '../base/logging';
18import {StatusResult} from '../common/protos';
19
20import {Engine, LoadingTracker} from './engine';
21
22export const RPC_URL = 'http://127.0.0.1:9001/';
23const RPC_CONNECT_TIMEOUT_MS = 2000;
24
25export interface HttpRpcState {
26  connected: boolean;
27  loadedTraceName?: string;
28  failure?: string;
29}
30
31interface QueuedRequest {
32  methodName: string;
33  reqData?: Uint8Array;
34  resp: Deferred<Uint8Array>;
35  id: number;
36}
37
38export class HttpRpcEngine extends Engine {
39  readonly id: string;
40  private nextReqId = 0;
41  private requestQueue = new Array<QueuedRequest>();
42  private pendingRequest?: QueuedRequest = undefined;
43  errorHandler: (err: string) => void = () => {};
44
45  constructor(id: string, loadingTracker?: LoadingTracker) {
46    super(loadingTracker);
47    this.id = id;
48  }
49
50  async parse(data: Uint8Array): Promise<void> {
51    await this.enqueueRequest('parse', data);
52  }
53
54  async notifyEof(): Promise<void> {
55    await this.enqueueRequest('notify_eof');
56  }
57
58  async restoreInitialTables(): Promise<void> {
59    await this.enqueueRequest('restore_initial_tables');
60  }
61
62  rawQuery(rawQueryArgs: Uint8Array): Promise<Uint8Array> {
63    return this.enqueueRequest('raw_query', rawQueryArgs);
64  }
65
66  rawComputeMetric(rawComputeMetricArgs: Uint8Array): Promise<Uint8Array> {
67    return this.enqueueRequest('compute_metric', rawComputeMetricArgs);
68  }
69
70  async enableMetatrace(): Promise<void> {
71    await this.enqueueRequest('enable_metatrace');
72  }
73
74  disableAndReadMetatrace(): Promise<Uint8Array> {
75    return this.enqueueRequest('disable_and_read_metatrace');
76  }
77
78  enqueueRequest(methodName: string, data?: Uint8Array): Promise<Uint8Array> {
79    const resp = defer<Uint8Array>();
80    const req:
81        QueuedRequest = {methodName, reqData: data, resp, id: this.nextReqId++};
82    if (this.pendingRequest === undefined) {
83      this.beginFetch(req);
84    } else {
85      this.requestQueue.push(req);
86    }
87    return resp;
88  }
89
90  private beginFetch(req: QueuedRequest) {
91    assertTrue(this.pendingRequest === undefined);
92    this.pendingRequest = req;
93    const methodName = req.methodName.toLowerCase();
94    // Deliberately not using fetchWithTimeout() here. These queries can be
95    // arbitrarily long.
96    // Deliberately not setting cache: no-cache. Doing so invalidates also the
97    // CORS pre-flight responses, causing one OPTIONS request for each POST.
98    // no-cache is also useless because trace-processor's replies are already
99    // marked as no-cache and browsers generally already assume that POST
100    // requests are not idempotent.
101    fetch(RPC_URL + methodName, {
102      method: 'post',
103      headers: {
104        'Content-Type': 'application/x-protobuf',
105        'X-Seq-Id': `${req.id}`,  // Used only for debugging.
106      },
107      body: req.reqData || new Uint8Array(),
108    })
109        .then(resp => this.endFetch(resp, req.id))
110        .catch(err => this.errorHandler(err));
111  }
112
113  private endFetch(resp: Response, expectedReqId: number) {
114    const req = assertExists(this.pendingRequest);
115    this.pendingRequest = undefined;
116    assertTrue(expectedReqId === req.id);
117    if (resp.status !== 200) {
118      req.resp.reject(`HTTP ${resp.status} - ${resp.statusText}`);
119      return;
120    }
121    resp.arrayBuffer().then(arrBuf => {
122      // Note: another request can sneak in via enqueueRequest() between the
123      // arrayBuffer() call and this continuation. At this point
124      // this.pendingRequest might be set again.
125      // If not (the most common case) submit the next queued request, if any.
126      this.maybeSubmitNextQueuedRequest();
127      req.resp.resolve(new Uint8Array(arrBuf));
128    });
129  }
130
131  private maybeSubmitNextQueuedRequest() {
132    if (this.pendingRequest === undefined && this.requestQueue.length > 0) {
133      this.beginFetch(this.requestQueue.shift()!);
134    }
135  }
136
137  static async checkConnection(): Promise<HttpRpcState> {
138    const httpRpcState: HttpRpcState = {connected: false};
139    console.info(
140        `It's safe to ignore the ERR_CONNECTION_REFUSED on ${RPC_URL} below. ` +
141        `That might happen while probing the exernal native accelerator. The ` +
142        `error is non-fatal and unlikely to be the culprit for any UI bug.`);
143    try {
144      const resp = await fetchWithTimeout(
145          RPC_URL + 'status',
146          {method: 'post', cache: 'no-cache'},
147          RPC_CONNECT_TIMEOUT_MS);
148      if (resp.status !== 200) {
149        httpRpcState.failure = `${resp.status} - ${resp.statusText}`;
150      } else {
151        const buf = new Uint8Array(await resp.arrayBuffer());
152        const status = StatusResult.decode(buf);
153        httpRpcState.connected = true;
154        if (status.loadedTraceName) {
155          httpRpcState.loadedTraceName = status.loadedTraceName;
156        }
157      }
158    } catch (err) {
159      httpRpcState.failure = `${err}`;
160    }
161    return httpRpcState;
162  }
163}
164