bug 1523104: remote: upgrade to WebSocket on existing HTTPD; r=ochameau
authorAndreas Tolfsen <ato@sny.no>
Thu, 07 Mar 2019 22:18:45 +0000
changeset 521137 53947dc827ddfbf1eaab8aaa753824c4cffc9ffe
parent 521136 2dd67c30b7ae9994dadf32a0e2aa41f2268bd7f8
child 521138 9e035690f943de680442317056c18ce52167b903
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1523104
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 1523104: remote: upgrade to WebSocket on existing HTTPD; r=ochameau Rather than spinning up a separate WebSocket listener for each target, this enables us to serve WebSocket connections from the original HTTPD that we serve the rest of the JSON API from.
remote/Handler.jsm
remote/JSONHandler.jsm
remote/RemoteAgent.js
remote/Target.jsm
remote/TargetListener.jsm
remote/jar.mn
deleted file mode 100644
--- a/remote/Handler.jsm
+++ /dev/null
@@ -1,88 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-var EXPORTED_SYMBOLS = [
-  "TargetListHandler",
-  "ProtocolHandler",
-];
-
-const {Log} = ChromeUtils.import("chrome://remote/content/Log.jsm");
-const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-const {Protocol} = ChromeUtils.import("chrome://remote/content/Protocol.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "log", Log.get);
-
-class JSONHandler {
-  register(server) {
-    server.registerPathHandler(this.path, (req, resp) => {
-      resp.setHeader("content-type", "application/json");
-      this.rawHandle(req, new JSONWriter(resp));
-    });
-  }
-
-  rawHandle(request, response) {
-    log.trace(`(${request._scheme})-> ${request._method} ${request._path}`);
-
-    try {
-      this.handle(request, response);
-    } catch (e) {
-      log.warn(e);
-    }
-  }
-}
-
-class TargetListHandler extends JSONHandler {
-  constructor(targets) {
-    super();
-    this.targets = targets;
-  }
-
-  get path() {
-    return "/json/list";
-  }
-
-  handle(request, response) {
-    response.write([...this.targets]);
-  }
-}
-
-class ProtocolHandler extends JSONHandler {
-  get path() {
-    return "/json/protocol";
-  }
-
-  handle(request, response) {
-    response.write(Protocol.Description);
-  }
-}
-
-/**
- * Wraps an httpd.js response and serialises anything passed to
- * write() to JSON.
- */
-class JSONWriter {
-  constructor(response) {
-    this._response = response;
-  }
-
-  /** Filters out null and empty strings. */
-  _replacer(key, value) {
-    if (value === null || (typeof value == "string" && value.length == 0)) {
-      return undefined;
-    }
-    return value;
-  }
-
-  write(data) {
-    try {
-      const json = JSON.stringify(data, this._replacer, "\t");
-      this._response.write(json);
-    } catch (e) {
-      log.error(`Unable to serialise JSON: ${e.message}`, e);
-      this._response.setStatusLine("1.1", 500, "Internal Server Error");
-    }
-  }
-}
new file mode 100644
--- /dev/null
+++ b/remote/JSONHandler.jsm
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["JSONHandler"];
+
+const {HTTP_404} = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");
+const {Log} = ChromeUtils.import("chrome://remote/content/Log.jsm");
+const {Protocol} = ChromeUtils.import("chrome://remote/content/Protocol.jsm");
+
+class JSONHandler {
+  constructor(agent) {
+    this.agent = agent;
+    this.routes = {
+      "/json/version": this.getVersion.bind(this),
+      "/json/protocol": this.getProtocol.bind(this),
+      "/json/list": this.getTargetList.bind(this),
+    };
+  }
+
+  getVersion() {
+    return {};
+  }
+
+  getProtocol() {
+    return Protocol.Description;
+  }
+
+  getTargetList() {
+    return [...this.agent.targets];
+  }
+
+  // nsIHttpRequestHandler
+
+  handle(request, response) {
+    if (request.method != "GET") {
+      throw HTTP_404;
+    }
+
+    if (!(request.path in this.routes)) {
+      throw HTTP_404;
+    }
+
+    const body = this.routes[request.path]();
+    const payload = JSON.stringify(body, sanitise, Log.verbose ? "\t" : undefined);
+
+    response.setStatusLine(request.httpVersion, 200, "OK");
+    response.setHeader("Content-Type", "application/json");
+    response.write(payload);
+  }
+
+  // XPCOM
+
+  get QueryInterface() {
+    return ChromeUtils.generateQI([Ci.nsIHttpRequestHandler]);
+  }
+}
+
+// Filters out null and empty strings
+function sanitise(key, value) {
+  if (value === null || (typeof value == "string" && value.length == 0)) {
+    return undefined;
+  }
+  return value;
+}
--- a/remote/RemoteAgent.js
+++ b/remote/RemoteAgent.js
@@ -4,51 +4,61 @@
 
 "use strict";
 
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   ActorManagerParent: "resource://gre/modules/ActorManagerParent.jsm",
+  FatalError: "chrome://remote/content/Error.jsm",
   HttpServer: "chrome://remote/content/server/HTTPD.jsm",
+  JSONHandler: "chrome://remote/content/JSONHandler.jsm",
   Log: "chrome://remote/content/Log.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   Observer: "chrome://remote/content/Observer.jsm",
   Preferences: "resource://gre/modules/Preferences.jsm",
   ProtocolHandler: "chrome://remote/content/Handler.jsm",
   RecommendedPreferences: "chrome://remote/content/RecommendedPreferences.jsm",
   TabObserver: "chrome://remote/content/WindowManager.jsm",
   Targets: "chrome://remote/content/Targets.jsm",
-  TargetListHandler: "chrome://remote/content/Handler.jsm",
 });
 XPCOMUtils.defineLazyGetter(this, "log", Log.get);
 
 const ENABLED = "remote.enabled";
 const FORCE_LOCAL = "remote.force-local";
 
 const DEFAULT_HOST = "localhost";
 const DEFAULT_PORT = 9222;
 const LOOPBACKS = ["localhost", "127.0.0.1", "[::1]"];
 
 class ParentRemoteAgent {
   constructor() {
-    this.server = null;
+    this.server = new HttpServer();
     this.targets = new Targets();
 
+    this.server.registerPrefixHandler("/json/", new JSONHandler(this));
+
     this.tabs = new TabObserver({registerExisting: true});
     this.tabs.on("open", (eventName, tab) => {
       this.targets.connect(tab.linkedBrowser);
     });
     this.tabs.on("close", (eventName, tab) => {
       this.targets.disconnect(tab.linkedBrowser);
     });
 
+    this.targets.on("connect", (eventName, target) => {
+      this.server.registerPathHandler(`/devtools/page/${target.id}`, target);
+    });
+    this.targets.on("disconnect", (eventName, target) => {
+      // TODO(ato): removing a handler is currently not possible
+    });
+
     // This allows getting access to the underlying JS object
-    // of the '@mozilla.org/remote/agent' XPCOM components.
+    // of the @mozilla.org/remote/agent XPCOM components
     this.wrappedJSObject = this;
   }
 
   get listening() {
     return !!this.server && !this.server._socketClosed;
   }
 
   listen(address) {
@@ -65,22 +75,16 @@ class ParentRemoteAgent {
     if (port === 0) {
       port = -1;
     }
 
     if (this.listening) {
       return;
     }
 
-    this.server = new HttpServer();
-    const targetList = new TargetListHandler(this.targets);
-    const protocol = new ProtocolHandler();
-    targetList.register(this.server);
-    protocol.register(this.server);
-
     try {
       this.server._start(port, host);
       log.info(`Remote debugging agent listening on ${this.scheme}://${this.host}:${this.port}/`);
     } catch (e) {
       throw new Error(`Unable to start remote agent: ${e.message}`, e);
     }
 
     Preferences.set(RecommendedPreferences);
--- a/remote/Target.jsm
+++ b/remote/Target.jsm
@@ -1,44 +1,45 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["Target"];
 
+const {Connection} = ChromeUtils.import("chrome://remote/content/Connection.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const {TargetListener} = ChromeUtils.import("chrome://remote/content/TargetListener.jsm");
+const {Session} = ChromeUtils.import("chrome://remote/content/Session.jsm");
+const {WebSocketDebuggerTransport} = ChromeUtils.import("chrome://remote/content/server/WebSocketTransport.jsm");
+const {WebSocketServer} = ChromeUtils.import("chrome://remote/content/server/WebSocket.jsm");
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "Favicons",
     "@mozilla.org/browser/favicon-service;1", "nsIFaviconService");
 
 /**
  * A debugging target.
  *
  * Targets can be a document (page), an OOP frame, a background
  * document, or a worker.  They can all run in dedicated process or frame.
  */
 class Target {
   constructor(browser) {
     this.browser = browser;
-    this.debugger = new TargetListener(this);
+    this.sessions = new Map();
   }
 
   connect() {
     Services.obs.addObserver(this, "message-manager-disconnect");
-    this.debugger.listen();
   }
 
   disconnect() {
     Services.obs.removeObserver(this, "message-manager-disconnect");
     // TODO(ato): Disconnect existing client sockets
-    this.debugger.close();
   }
 
   get id() {
     return this.browsingContext.id;
   }
 
   get browsingContext() {
     return this.browser.browsingContext;
@@ -92,17 +93,20 @@ class Target {
     return "page";
   }
 
   get url() {
     return this.browser.currentURI.spec;
   }
 
   get wsDebuggerURL() {
-    return this.debugger.url;
+    const RemoteAgent = Cc["@mozilla.org/remote/agent"]
+        .getService(Ci.nsISupports).wrappedJSObject;
+    const {host, port} = RemoteAgent;
+    return `ws://${host}:${port}/devtools/page/${this.id}`;
   }
 
   toString() {
     return `[object Target ${this.id}]`;
   }
 
   toJSON() {
     return {
@@ -113,20 +117,38 @@ class Target {
       id: this.id,
       title: this.title,
       type: this.type,
       url: this.url,
       webSocketDebuggerUrl: this.wsDebuggerURL,
     };
   }
 
+  // nsIHttpRequestHandler
+
+  async handle(request, response) {
+    const so = await WebSocketServer.upgrade(request, response);
+    const transport = new WebSocketDebuggerTransport(so);
+    const conn = new Connection(transport);
+    this.sessions.set(conn, new Session(conn, this));
+  }
+
   // nsIObserver
 
   observe(subject, topic, data) {
     if (subject === this.mm && subject == "message-manager-disconnect") {
       // disconnect debugging target if <browser> is disconnected,
       // otherwise this is just a host process change
       if (this.closed) {
         this.disconnect();
       }
     }
   }
+
+  // XPCOM
+
+  get QueryInterface() {
+    return ChromeUtils.generateQI([
+      Ci.nsIHttpRequestHandler,
+      Ci.nsIObserver,
+    ]);
+  }
 }
deleted file mode 100644
--- a/remote/TargetListener.jsm
+++ /dev/null
@@ -1,98 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-var EXPORTED_SYMBOLS = ["TargetListener"];
-
-const {Connection} = ChromeUtils.import("chrome://remote/content/Connection.jsm");
-const {Session} = ChromeUtils.import("chrome://remote/content/Session.jsm");
-const {SocketListener} = ChromeUtils.import("chrome://remote/content/server/Socket.jsm");
-
-/**
- * Represents a debuggee target (a browsing context, typically a tab)
- * that clients can connect to and debug.
- *
- * Debugger#listen starts a WebSocket listener,
- * and for each accepted connection a new Session is created.
- * There can be multiple sessions per target.
- * The session's lifetime is equal to the lifetime of the debugger connection.
- */
-class TargetListener {
-  constructor(target) {
-    this.target = target;
-    this.listener = null;
-    this.sessions = new Map();
-    this.nextConnID = 0;
-    this.onConnectionAccepted = this.onConnectionAccepted.bind(this);
-  }
-
-  get listening() {
-    return !!this.listener && this.listener.listening;
-  }
-
-  listen() {
-    if (this.listener) {
-      return;
-    }
-
-    this.listener = new SocketListener();
-    this.listener.on("accepted", this.onConnectionAccepted);
-
-    this.listener.listen("ws", 0 /* atomically allocated port */);
-  }
-
-  close() {
-    this.listener.off("accepted", this.onConnectionAccepted);
-    for (const [conn, session] of this.sessions) {
-      session.destructor();
-      conn.close();
-    }
-    this.listener.close();
-    this.listener = null;
-    this.sessions.clear();
-  }
-
-  /**
-   * SocketListener's `accepted` listener. Called whenever a new WebSocket connection is
-   * established.
-   *
-   * @param String eventName
-   * @param WebSocketDebuggerTransport transport
-   * @param SocketListener listener
-   */
-  onConnectionAccepted(eventName, transport, listener) {
-    const conn = new Connection(this.nextConnID++, transport);
-    transport.ready();
-    transport.on("close", this.onConnectionClosed.bind(this, conn));
-    this.sessions.set(conn, new Session(conn, this.target));
-  }
-
-  /**
-   * WebSocketTransport's `close` listener. Called whenever a new WebSocket connection is
-   * closed.
-   *
-   * @param Connection conn
-   * @param String eventName
-   */
-  onConnectionClosed(conn, eventName) {
-    const session = this.sessions.get(conn);
-    if (!session) {
-      return;
-    }
-    session.destructor();
-  }
-
-  get url() {
-    if (this.listening) {
-      const {network, host, port} = this.listener;
-      return `${network}://${host}:${port}/`;
-    }
-    return null;
-  }
-
-  toString() {
-    return `[object TargetListener ${this.url || "disconnected"}]`;
-  }
-}
--- a/remote/jar.mn
+++ b/remote/jar.mn
@@ -2,25 +2,24 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 remote.jar:
 % content remote %content/
   content/Connection.jsm (Connection.jsm)
   content/ContentProcessSession.jsm (ContentProcessSession.jsm)
   content/Error.jsm (Error.jsm)
-  content/Handler.jsm (Handler.jsm)
+  content/JSONHandler.jsm (JSONHandler.jsm)
   content/Log.jsm (Log.jsm)
   content/Observer.jsm (Observer.jsm)
+  content/Protocol.jsm (Protocol.jsm)
   content/RecommendedPreferences.jsm (RecommendedPreferences.jsm)
-  content/Protocol.jsm (Protocol.jsm)
   content/Session.jsm (Session.jsm)
   content/Sync.jsm (Sync.jsm)
   content/Target.jsm (Target.jsm)
-  content/TargetListener.jsm (TargetListener.jsm)
   content/Targets.jsm (Targets.jsm)
   content/WindowManager.jsm (WindowManager.jsm)
 
   # Frame scripts
   content/frame-script.js (frame-script.js)
 
   # domains
   content/domains/Domain.jsm (domains/Domain.jsm)