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 {assertDefined} from 'common/assert_utils';
18import {FunctionUtils, OnProgressUpdateType} from 'common/function_utils';
19import {TimeUtils} from 'common/time_utils';
20import {
21  DeviceProperties,
22  proxyClient,
23  ProxyEndpoint,
24  proxyRequest,
25  ProxyState,
26} from 'trace_collection/proxy_client';
27import {Connection} from './connection';
28import {
29  ConfigMap,
30  TraceConfigurationMap,
31  TRACES,
32} from './trace_collection_utils';
33
34export class ProxyConnection implements Connection {
35  proxy = proxyClient;
36  keep_alive_worker: NodeJS.Timeout | undefined;
37  notConnected = [
38    ProxyState.NO_PROXY,
39    ProxyState.UNAUTH,
40    ProxyState.INVALID_VERSION,
41  ];
42
43  constructor(
44    private proxyStateChangeCallback: (state: ProxyState) => void,
45    private progressCallback: OnProgressUpdateType = FunctionUtils.DO_NOTHING,
46    private traceConfigChangeCallback: (
47      availableTracesConfig: TraceConfigurationMap,
48    ) => void,
49  ) {
50    this.proxy.setState(ProxyState.CONNECTING);
51    this.proxy.onProxyChange(
52      async (newState) => await this.onConnectChange(newState),
53    );
54    const urlParams = new URLSearchParams(window.location.search);
55    if (urlParams.has('token')) {
56      this.proxy.proxyKey = assertDefined(urlParams.get('token'));
57    } else if (this.proxy.store.get('adb.proxyKey')) {
58      this.proxy.proxyKey = assertDefined(this.proxy.store.get('adb.proxyKey'));
59    }
60    this.proxy.getDevices();
61  }
62
63  devices() {
64    return this.proxy.devices;
65  }
66
67  adbData() {
68    return this.proxy.adbData;
69  }
70
71  state() {
72    return this.proxy.state;
73  }
74
75  isDevicesState() {
76    return this.state() === ProxyState.DEVICES;
77  }
78
79  isStartTraceState() {
80    return this.state() === ProxyState.START_TRACE;
81  }
82
83  isErrorState() {
84    return this.state() === ProxyState.ERROR;
85  }
86
87  isStartingTraceState() {
88    return this.state() === ProxyState.STARTING_TRACE;
89  }
90
91  isEndTraceState() {
92    return this.state() === ProxyState.END_TRACE;
93  }
94
95  isLoadDataState() {
96    return this.state() === ProxyState.LOAD_DATA;
97  }
98
99  isConnectingState() {
100    return this.state() === ProxyState.CONNECTING;
101  }
102
103  throwNoTargetsError() {
104    this.proxy.setState(ProxyState.ERROR, 'No targets selected');
105  }
106
107  setProxyKey(key: string) {
108    this.proxy.proxyKey = key;
109    this.proxy.store.add('adb.proxyKey', key);
110  }
111
112  adbSuccess() {
113    return !this.notConnected.includes(this.proxy.state);
114  }
115
116  selectedDevice(): DeviceProperties {
117    return this.proxy.devices[this.proxy.selectedDevice];
118  }
119
120  selectedDeviceId(): string {
121    return this.proxy.selectedDevice;
122  }
123
124  restart() {
125    this.proxy.setState(ProxyState.CONNECTING);
126  }
127
128  resetLastDevice() {
129    this.proxy.store.add('adb.lastDevice', '');
130    this.restart();
131  }
132
133  selectDevice(id: string) {
134    this.proxy.selectDevice(id);
135  }
136
137  keepAliveTrace(view: ProxyConnection) {
138    if (!view.isStartingTraceState() && !view.isEndTraceState()) {
139      clearInterval(view.keep_alive_worker);
140      view.keep_alive_worker = undefined;
141      return;
142    }
143    proxyRequest.keepTraceAlive(view.proxy, (request: XMLHttpRequest) => {
144      if (request.responseText !== 'True') {
145        view.endTrace();
146      } else if (view.keep_alive_worker === undefined) {
147        view.keep_alive_worker = setInterval(view.keepAliveTrace, 1000, view);
148      }
149    });
150  }
151
152  async startTrace(
153    requestedTraces: string[],
154    reqEnableConfig?: string[],
155    reqSelectedSfConfig?: ConfigMap,
156    reqSelectedWmConfig?: ConfigMap,
157  ) {
158    if (reqEnableConfig) {
159      proxyRequest.setEnabledConfig(this.proxy, reqEnableConfig);
160    }
161    if (reqSelectedSfConfig) {
162      proxyRequest.setSelectedConfig(
163        ProxyEndpoint.SELECTED_SF_CONFIG_TRACE,
164        this.proxy,
165        reqSelectedSfConfig,
166      );
167    }
168    if (reqSelectedWmConfig) {
169      proxyRequest.setSelectedConfig(
170        ProxyEndpoint.SELECTED_WM_CONFIG_TRACE,
171        this.proxy,
172        reqSelectedWmConfig,
173      );
174    }
175    await proxyClient.setState(ProxyState.STARTING_TRACE);
176    await proxyRequest.startTrace(
177      this.proxy,
178      requestedTraces,
179      (request: XMLHttpRequest) => this.keepAliveTrace(this),
180    );
181    // TODO(b/330118129): identify source of additional start latency that affects some traces
182    await TimeUtils.sleepMs(1000); // 1s timeout ensures SR fully started
183    proxyClient.setState(ProxyState.END_TRACE);
184  }
185
186  async endTrace() {
187    this.progressCallback(0);
188    await this.proxy.setState(ProxyState.LOAD_DATA);
189    await proxyRequest.endTrace(this.proxy, this.progressCallback);
190  }
191
192  async dumpState(requestedDumps: string[]): Promise<boolean> {
193    this.progressCallback(0);
194    if (requestedDumps.length < 1) {
195      console.error('No targets selected');
196      await this.proxy.setState(ProxyState.ERROR, 'No targets selected');
197      return false;
198    }
199    await this.proxy.setState(ProxyState.LOAD_DATA);
200    await proxyRequest.dumpState(
201      this.proxy,
202      requestedDumps,
203      this.progressCallback,
204    );
205    return true;
206  }
207
208  isWaylandAvailable(): Promise<boolean> {
209    return new Promise((resolve, reject) => {
210      proxyRequest.call(
211        'GET',
212        ProxyEndpoint.CHECK_WAYLAND,
213        (request: XMLHttpRequest) => {
214          resolve(request.responseText === 'true');
215        },
216      );
217    });
218  }
219
220  async onConnectChange(newState: ProxyState) {
221    if (newState === ProxyState.CONNECTING) {
222      proxyClient.getDevices();
223    }
224    if (newState === ProxyState.START_TRACE) {
225      const isWaylandAvailable = await this.isWaylandAvailable();
226      if (isWaylandAvailable) {
227        const availableTracesConfig = TRACES['default'];
228        if (isWaylandAvailable) {
229          Object.assign(availableTracesConfig, TRACES['arc']);
230        }
231        this.traceConfigChangeCallback(availableTracesConfig);
232      }
233    }
234    this.proxyStateChangeCallback(newState);
235  }
236}
237