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