1/* 2 * Copyright (C) 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, assertUnreachable} from 'common/assert_utils'; 18import {FunctionUtils} from 'common/function_utils'; 19import {Timestamp} from 'common/time'; 20import {RemoteToolTimestampConverter} from 'common/timestamp_converter'; 21import { 22 RemoteToolFilesReceived, 23 RemoteToolTimestampReceived, 24 WinscopeEvent, 25 WinscopeEventType, 26} from 'messaging/winscope_event'; 27import { 28 EmitEvent, 29 WinscopeEventEmitter, 30} from 'messaging/winscope_event_emitter'; 31import {WinscopeEventListener} from 'messaging/winscope_event_listener'; 32import { 33 Message, 34 MessageBugReport, 35 MessageFiles, 36 MessagePong, 37 MessageTimestamp, 38 MessageType, 39 TimestampType, 40} from './messages'; 41import {OriginAllowList} from './origin_allow_list'; 42 43class RemoteTool { 44 timestampType?: TimestampType; 45 46 constructor(readonly window: Window, readonly origin: string) {} 47} 48 49export class CrossToolProtocol 50 implements WinscopeEventEmitter, WinscopeEventListener 51{ 52 private remoteTool?: RemoteTool; 53 private emitEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC; 54 private timestampConverter: RemoteToolTimestampConverter; 55 private allowTimestampSync = true; 56 57 constructor(timestampConverter: RemoteToolTimestampConverter) { 58 this.timestampConverter = timestampConverter; 59 60 window.addEventListener('message', async (event) => { 61 await this.onMessageReceived(event); 62 }); 63 } 64 65 setEmitEvent(callback: EmitEvent) { 66 this.emitEvent = callback; 67 } 68 69 async onWinscopeEvent(event: WinscopeEvent) { 70 await event.visit( 71 WinscopeEventType.TRACE_POSITION_UPDATE, 72 async (event) => { 73 if ( 74 !this.remoteTool || 75 !this.remoteTool.timestampType || 76 !this.allowTimestampSync 77 ) { 78 return; 79 } 80 81 const timestampNs = this.getTimestampNsForRemoteTool( 82 event.position.timestamp, 83 ); 84 if (timestampNs === undefined) { 85 return; 86 } 87 88 const message = new MessageTimestamp( 89 timestampNs, 90 this.remoteTool.timestampType, 91 ); 92 this.remoteTool.window.postMessage(message, this.remoteTool.origin); 93 console.log('Cross-tool protocol sent timestamp message:', message); 94 }, 95 ); 96 } 97 98 isConnected() { 99 return this.remoteTool !== undefined; 100 } 101 102 setAllowTimestampSync(value: boolean) { 103 this.allowTimestampSync = value; 104 } 105 106 getAllowTimestampSync() { 107 return this.allowTimestampSync; 108 } 109 110 private async onMessageReceived(event: MessageEvent) { 111 if (!OriginAllowList.isAllowed(event.origin)) { 112 return; 113 } 114 115 const message = event.data as Message; 116 if (message.type === undefined) { 117 return; 118 } 119 120 if (!this.remoteTool) { 121 this.remoteTool = new RemoteTool(event.source as Window, event.origin); 122 } 123 124 switch (message.type) { 125 case MessageType.PING: 126 console.log('Cross-tool protocol received ping message:', message); 127 (event.source as Window).postMessage(new MessagePong(), event.origin); 128 break; 129 case MessageType.PONG: 130 console.log( 131 'Cross-tool protocol received unexpected pong message:', 132 message, 133 ); 134 break; 135 case MessageType.BUGREPORT: 136 console.log('Cross-tool protocol received bugreport message:', message); 137 await this.onMessageBugreportReceived(message as MessageBugReport); 138 console.log( 139 'Cross-tool protocol processed bugreport message:', 140 message, 141 ); 142 break; 143 case MessageType.TIMESTAMP: 144 console.log('Cross-tool protocol received timestamp message:', message); 145 await this.onMessageTimestampReceived(message as MessageTimestamp); 146 console.log( 147 'Cross-tool protocol processed timestamp message:', 148 message, 149 ); 150 break; 151 case MessageType.FILES: 152 console.log('Cross-tool protocol received files message:', message); 153 await this.onMessageFilesReceived(message as MessageFiles); 154 console.log('Cross-tool protocol processed files message:', message); 155 console.log( 156 'Cross-tool protocol received unexpected files message', 157 message, 158 ); 159 break; 160 default: 161 console.log( 162 'Cross-tool protocol received unsupported message type:', 163 message, 164 ); 165 break; 166 } 167 } 168 169 private async onMessageBugreportReceived(message: MessageBugReport) { 170 this.setRemoteToolTimestampTypeIfNeeded(message.timestampType); 171 const deferredTimestamp = this.makeDeferredTimestampForWinscope( 172 message.timestampNs, 173 ); 174 await this.emitEvent( 175 new RemoteToolFilesReceived([message.file], deferredTimestamp), 176 ); 177 } 178 179 private async onMessageFilesReceived(message: MessageFiles) { 180 this.setRemoteToolTimestampTypeIfNeeded(message.timestampType); 181 const deferredTimestamp = this.makeDeferredTimestampForWinscope( 182 message.timestampNs, 183 ); 184 await this.emitEvent( 185 new RemoteToolFilesReceived(message.files, deferredTimestamp), 186 ); 187 } 188 189 private async onMessageTimestampReceived(message: MessageTimestamp) { 190 if (!this.allowTimestampSync) { 191 return; 192 } 193 this.setRemoteToolTimestampTypeIfNeeded(message.timestampType); 194 const deferredTimestamp = this.makeDeferredTimestampForWinscope( 195 message.timestampNs, 196 ); 197 await this.emitEvent( 198 new RemoteToolTimestampReceived(assertDefined(deferredTimestamp)), 199 ); 200 } 201 202 private setRemoteToolTimestampTypeIfNeeded(type: TimestampType | undefined) { 203 const remoteTool = assertDefined(this.remoteTool); 204 205 if (remoteTool.timestampType !== undefined) { 206 return; 207 } 208 209 // Default to CLOCK_REALTIME for backward compatibility. 210 // The initial protocol's version didn't provide an explicit timestamp type 211 // and all timestamps were supposed to be CLOCK_REALTIME. 212 remoteTool.timestampType = type ?? TimestampType.CLOCK_REALTIME; 213 } 214 215 private getTimestampNsForRemoteTool( 216 timestamp: Timestamp, 217 ): bigint | undefined { 218 const timestampType = this.remoteTool?.timestampType; 219 switch (timestampType) { 220 case undefined: 221 return undefined; 222 case TimestampType.UNKNOWN: 223 return undefined; 224 case TimestampType.CLOCK_BOOTTIME: 225 return this.timestampConverter.tryGetBootTimeNs(timestamp); 226 case TimestampType.CLOCK_REALTIME: 227 return this.timestampConverter.tryGetRealTimeNs(timestamp); 228 default: 229 assertUnreachable(timestampType); 230 } 231 } 232 233 // Make a deferred timestamp: a lambda meant to be executed at a later point to create a 234 // timestamp. The lambda is needed to defer timestamp creation to the point where traces 235 // are loaded into TracePipeline and TimestampConverter is properly initialized and ready 236 // to instantiate timestamps. 237 private makeDeferredTimestampForWinscope( 238 timestampNs: bigint | undefined, 239 ): (() => Timestamp | undefined) | undefined { 240 const timestampType = assertDefined(this.remoteTool?.timestampType); 241 242 if (timestampNs === undefined || timestampType === undefined) { 243 return undefined; 244 } 245 246 switch (timestampType) { 247 case TimestampType.UNKNOWN: 248 return undefined; 249 case TimestampType.CLOCK_BOOTTIME: 250 return () => { 251 try { 252 return this.timestampConverter.makeTimestampFromBootTimeNs( 253 timestampNs, 254 ); 255 } catch (error) { 256 return undefined; 257 } 258 }; 259 case TimestampType.CLOCK_REALTIME: 260 return () => { 261 try { 262 return this.timestampConverter.makeTimestampFromRealNs(timestampNs); 263 } catch (error) { 264 return undefined; 265 } 266 }; 267 default: 268 assertUnreachable(timestampType); 269 } 270 } 271} 272