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