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 {JsonRpc2, LikeSocket} from 'noice-json-rpc'; 16 17// To really understand how this works it is useful to see the implementation 18// of noice-json-rpc. 19export class DevToolsSocket implements LikeSocket { 20 private messageCallback: Function = (_: string) => {}; 21 private openCallback: Function = () => {}; 22 private closeCallback: Function = () => {}; 23 private target: chrome.debugger.Debuggee | undefined; 24 25 constructor() { 26 chrome.debugger.onDetach.addListener(this.onDetach.bind(this)); 27 chrome.debugger.onEvent.addListener((_source, method, params) => { 28 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 29 if (this.messageCallback) { 30 const msg: JsonRpc2.Notification = {method, params}; 31 this.messageCallback(JSON.stringify(msg)); 32 } 33 }); 34 } 35 36 send(message: string): void { 37 if (this.target === undefined) return; 38 39 const msg: JsonRpc2.Request = JSON.parse(message); 40 chrome.debugger.sendCommand( 41 this.target, 42 msg.method, 43 msg.params, 44 (result) => { 45 if (result === undefined) result = {}; 46 const response: JsonRpc2.Response = {id: msg.id, result}; 47 this.messageCallback(JSON.stringify(response)); 48 }, 49 ); 50 } 51 52 // This method will be called once for each event soon after the creation of 53 // this object. To understand better what happens, checking the implementation 54 // of noice-json-rpc is very useful. 55 // While the events "message" and "open" are for implementing the LikeSocket, 56 // "close" is a callback set from ChromeTracingController, to reset the state 57 // after a detach. 58 on(event: string, cb: Function) { 59 if (event === 'message') { 60 this.messageCallback = cb; 61 } else if (event === 'open') { 62 this.openCallback = cb; 63 } else if (event === 'close') { 64 this.closeCallback = cb; 65 } 66 } 67 68 removeListener(_event: string, _cb: Function) { 69 throw new Error('Call unexpected'); 70 } 71 72 attachToBrowser(then: (error?: string) => void) { 73 this.attachToTarget({targetId: 'browser'}, then); 74 } 75 76 private attachToTarget( 77 target: chrome.debugger.Debuggee, 78 then: (error?: string) => void, 79 ) { 80 chrome.debugger.attach(target, /* requiredVersion=*/ '1.3', () => { 81 if (chrome.runtime.lastError) { 82 then(chrome.runtime.lastError.message); 83 return; 84 } 85 this.target = target; 86 this.openCallback(); 87 then(); 88 }); 89 } 90 91 detach() { 92 if (this.target === undefined) return; 93 94 chrome.debugger.detach(this.target, () => { 95 this.target = undefined; 96 }); 97 } 98 99 onDetach(_source: chrome.debugger.Debuggee, _reason: string) { 100 if (_source === this.target) { 101 this.target = undefined; 102 this.closeCallback(); 103 } 104 } 105 106 isAttached(): boolean { 107 return this.target !== undefined; 108 } 109} 110