1// Copyright (C) 2019 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 {binaryDecode, binaryEncode} from '../base/string_utils'; 16import {TRACE_SUFFIX} from '../common/constants'; 17 18import { 19 ConsumerPortResponse, 20 hasProperty, 21 isReadBuffersResponse, 22 Typed, 23} from './consumer_port_types'; 24import {Consumer, RpcConsumerPort} from './record_controller_interfaces'; 25 26export interface ChromeExtensionError extends Typed { 27 error: string; 28} 29 30export interface ChromeExtensionStatus extends Typed { 31 status: string; 32} 33 34export interface GetCategoriesResponse extends Typed { 35 categories: string[]; 36} 37 38export type ChromeExtensionMessage = ChromeExtensionError|ChromeExtensionStatus| 39 ConsumerPortResponse|GetCategoriesResponse; 40 41export function isChromeExtensionError(obj: Typed): 42 obj is ChromeExtensionError { 43 return obj.type === 'ChromeExtensionError'; 44} 45 46export function isChromeExtensionStatus(obj: Typed): 47 obj is ChromeExtensionStatus { 48 return obj.type === 'ChromeExtensionStatus'; 49} 50 51function isObject(obj: unknown): obj is object { 52 return typeof obj === 'object' && obj !== null; 53} 54 55export function isGetCategoriesResponse(obj: unknown): 56 obj is GetCategoriesResponse { 57 if (!(isObject(obj) && hasProperty(obj, 'type') && 58 obj.type === 'GetCategoriesResponse')) { 59 return false; 60 } 61 62 return hasProperty(obj, 'categories') && Array.isArray(obj.categories); 63} 64 65// This class acts as a proxy from the record controller (running in a worker), 66// to the frontend. This is needed because we can't directly talk with the 67// extension from a web-worker, so we use a MessagePort to communicate with the 68// frontend, that will consecutively forward it to the extension. 69 70// Rationale for the binaryEncode / binaryDecode calls below: 71// Messages to/from extensions need to be JSON serializable. ArrayBuffers are 72// not supported. For this reason here we use binaryEncode/Decode. 73// See https://developer.chrome.com/extensions/messaging#simple 74 75export class ChromeExtensionConsumerPort extends RpcConsumerPort { 76 private extensionPort: MessagePort; 77 78 constructor(extensionPort: MessagePort, consumer: Consumer) { 79 super(consumer); 80 this.extensionPort = extensionPort; 81 this.extensionPort.onmessage = this.onExtensionMessage.bind(this); 82 } 83 84 onExtensionMessage(message: {data: ChromeExtensionMessage}) { 85 if (isChromeExtensionError(message.data)) { 86 this.sendErrorMessage(message.data.error); 87 return; 88 } 89 if (isChromeExtensionStatus(message.data)) { 90 this.sendStatus(message.data.status); 91 return; 92 } 93 94 // In this else branch message.data will be a ConsumerPortResponse. 95 if (isReadBuffersResponse(message.data) && message.data.slices) { 96 const slice = message.data.slices[0].data as unknown as string; 97 message.data.slices[0].data = binaryDecode(slice); 98 } 99 this.sendMessage(message.data); 100 } 101 102 handleCommand(method: string, requestData: Uint8Array): void { 103 const reqEncoded = binaryEncode(requestData); 104 this.extensionPort.postMessage({method, requestData: reqEncoded}); 105 } 106 107 getRecordedTraceSuffix(): string { 108 return `${TRACE_SUFFIX}.gz`; 109 } 110} 111