1// Copyright (C) 2022 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 {HostOsByteStream} from '../host_os_byte_stream';
16import {RecordingError} from '../recording_error_handling';
17import {
18  DataSource,
19  HostOsTargetInfo,
20  OnDisconnectCallback,
21  OnTargetChangeCallback,
22  RecordingTargetV2,
23  TracingSession,
24  TracingSessionListener,
25} from '../recording_interfaces_v2';
26import {
27  isLinux,
28  isMacOs,
29  WEBSOCKET_CLOSED_ABNORMALLY_CODE,
30} from '../recording_utils';
31import {TracedTracingSession} from '../traced_tracing_session';
32
33export class HostOsTarget implements RecordingTargetV2 {
34  private readonly targetType: 'LINUX' | 'MACOS';
35  private readonly name: string;
36  private websocket: WebSocket;
37  private streams = new Set<HostOsByteStream>();
38  private dataSources?: DataSource[];
39  private onDisconnect: OnDisconnectCallback = (_) => {};
40
41  constructor(
42    websocketUrl: string,
43    private maybeClearTarget: (target: HostOsTarget) => void,
44    private onTargetChange: OnTargetChangeCallback,
45  ) {
46    if (isMacOs(navigator.userAgent)) {
47      this.name = 'MacOS';
48      this.targetType = 'MACOS';
49    } else if (isLinux(navigator.userAgent)) {
50      this.name = 'Linux';
51      this.targetType = 'LINUX';
52    } else {
53      throw new RecordingError(
54        'Host OS target created on an unsupported operating system.',
55      );
56    }
57
58    this.websocket = new WebSocket(websocketUrl);
59    this.websocket.onclose = this.onClose.bind(this);
60    // 'onError' gets called when the websocketURL where the UI tries to connect
61    // is disallowed by the Content Security Policy. In this case, we disconnect
62    // the target.
63    this.websocket.onerror = this.disconnect.bind(this);
64  }
65
66  getInfo(): HostOsTargetInfo {
67    return {
68      targetType: this.targetType,
69      name: this.name,
70      dataSources: this.dataSources || [],
71    };
72  }
73
74  canCreateTracingSession(): boolean {
75    return true;
76  }
77
78  async createTracingSession(
79    tracingSessionListener: TracingSessionListener,
80  ): Promise<TracingSession> {
81    this.onDisconnect = tracingSessionListener.onDisconnect;
82
83    const osStream = await HostOsByteStream.create(this.getUrl());
84    this.streams.add(osStream);
85    const tracingSession = new TracedTracingSession(
86      osStream,
87      tracingSessionListener,
88    );
89    await tracingSession.initConnection();
90
91    if (!this.dataSources) {
92      this.dataSources = await tracingSession.queryServiceState();
93      this.onTargetChange();
94    }
95    return tracingSession;
96  }
97
98  // Starts a tracing session in order to fetch data sources from the
99  // device. Then, it cancels the session.
100  async fetchTargetInfo(
101    tracingSessionListener: TracingSessionListener,
102  ): Promise<void> {
103    const tracingSession = await this.createTracingSession(
104      tracingSessionListener,
105    );
106    tracingSession.cancel();
107  }
108
109  async disconnect(): Promise<void> {
110    if (this.websocket.readyState === this.websocket.OPEN) {
111      this.websocket.close();
112      // We remove the 'onclose' callback so the 'disconnect' method doesn't get
113      // executed twice.
114      this.websocket.onclose = null;
115    }
116    for (const stream of this.streams) {
117      stream.close();
118    }
119    // We remove the existing target from the factory if present.
120    this.maybeClearTarget(this);
121    // We run the onDisconnect callback in case this target is used for tracing.
122    this.onDisconnect();
123  }
124
125  // We can connect to the Host OS without taking the connection away from
126  // another process.
127  async canConnectWithoutContention(): Promise<boolean> {
128    return true;
129  }
130
131  getUrl() {
132    return this.websocket.url;
133  }
134
135  private onClose(ev: CloseEvent): void {
136    if (ev.code === WEBSOCKET_CLOSED_ABNORMALLY_CODE) {
137      console.info(
138        `It's safe to ignore the 'WebSocket connection to ${this.getUrl()} error above, if present. It occurs when ` +
139          'checking the connection to the local Websocket server.',
140      );
141    }
142    this.disconnect();
143  }
144}
145