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 * as 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