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