bug 1523104: remote: execute the domains in the content process by piping all WebSocket request, response and events via message manager API; r=ato
authorAlexandre Poirot <poirot.alex@gmail.com>
Mon, 11 Feb 2019 10:05:12 -0800
changeset 521076 f73d7c3bced49eb09f5f3cf4a0dc6088f931b143
parent 521075 bea61357248a8b30038e45bcd03ff65cb78bba1c
child 521077 ac7c2e018cb60c5061254fb475f5dbc86ae9a29b
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)
reviewersato
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: execute the domains in the content process by piping all WebSocket request, response and events via message manager API; r=ato
remote/ConsoleServiceObserver.jsm
remote/ContentProcessSession.jsm
remote/Domain.jsm
remote/Domains.jsm
remote/RemoteAgent.js
remote/Session.jsm
remote/actor/DOMChild.jsm
remote/actor/LogChild.jsm
remote/actor/moz.build
remote/domain/Log.jsm
remote/domain/Page.jsm
remote/frame-script.js
remote/jar.mn
remote/moz.build
deleted file mode 100644
--- a/remote/ConsoleServiceObserver.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 = ["ConsoleServiceObserver"];
-
-const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-class ConsoleServiceObserver {
-  constructor() {
-    this.running = false;
-  }
-
-  start() {
-    if (!this.running) {
-      Services.console.registerListener(this);
-      this.running = true;
-    }
-  }
-
-  stop() {
-    if (this.running) {
-      Services.console.unregisterListener(this);
-    }
-  }
-
-  // nsIConsoleListener
-
-  /**
-   * Takes all script error messages belonging to the current window
-   * and emits a "console-service-message" event.
-   *
-   * @param {nsIConsoleMessage} message
-   *     Message originating from the nsIConsoleService.
-   */
-  observe(message) {
-    let entry;
-    if (message instanceof Ci.nsIScriptError) {
-      entry = fromScriptError(message);
-    } else {
-      entry = fromConsoleMessage(message);
-    }
-
-    // TODO(ato): This doesn't work for some reason:
-    Services.mm.broadcastAsyncMessage("RemoteAgent:Log:OnConsoleServiceMessage", entry);
-  }
-
-  // XPCOM
-
-  get QueryInterface() {
-    return ChromeUtils.generateQI([Ci.nsIConsoleListener]);
-  }
-};
-
-function fromConsoleMessage(message) {
-  const levels = {
-    [Ci.nsIConsoleMessage.debug]: "verbose",
-    [Ci.nsIConsoleMessage.info]: "info",
-    [Ci.nsIConsoleMessage.warn]: "warning",
-    [Ci.nsIConsoleMessage.error]: "error",
-  };
-  const level = levels[message.logLevel];
-
-  return {
-    source: "javascript",
-    level,
-    text: message.message,
-    timestamp: Date.now(),
-  };
-}
-
-function fromScriptError(error) {
-  const {flags, errorMessage, sourceName, lineNumber, stack} = error;
-
-  // lossy reduction from bitmask to CDP string level
-  let level = "verbose";
-  if ((flags & Ci.nsIScriptError.exceptionFlag) ||
-      (flags & Ci.nsIScriptError.errorFlag)) {
-    level = "error";
-  } else if ((flags & Ci.nsIScriptError.warningFlag) ||
-      (flags & Ci.nsIScriptError.strictFlag)) {
-    level = "warning";
-  } else if (flags & Ci.nsIScriptError.infoFlag) {
-    level = "info";
-  }
-
-  return {
-    source: "javascript",
-    level,
-    text: errorMessage,
-    timestamp: Date.now(),
-    url: sourceName,
-    lineNumber,
-    stackTrace: stack,
-  };
-}
new file mode 100644
--- /dev/null
+++ b/remote/ContentProcessSession.jsm
@@ -0,0 +1,93 @@
+/* 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 = ["ContentProcessSession"];
+
+const {Domains} = ChromeUtils.import("chrome://remote/content/Domains.jsm");
+
+class ContentProcessSession {
+  constructor(messageManager, browsingContext, content, docShell) {
+    this.messageManager = messageManager;
+    this.browsingContext = browsingContext;
+    this.content = content;
+    this.docShell = docShell;
+
+    this.domains = new Domains(this);
+    this.messageManager.addMessageListener("remote-protocol:request", this);
+    this.messageManager.addMessageListener("remote-protocol:destroy", this);
+
+    this.destroy = this.destroy.bind(this);
+    this.content.addEventListener("unload", this.destroy);
+  }
+
+  destroy() {
+    this.content.addEventListener("unload", this.destroy);
+    this.messageManager.removeMessageListener("remote-protocol:request", this);
+    this.messageManager.removeMessageListener("remote-protocol:destroy", this);
+  }
+
+  async receiveMessage({ name, data }) {
+    const { browsingContextId } = data;
+
+    // We may have more than one tab loaded in the same process,
+    // and debug the two at the same time. We want to ensure not
+    // mixing up the requests made against two such tabs.
+    // Each tab is going to have its own frame script instance
+    // and two communication channels are going to be set up via
+    // the two message managers.
+    if (browsingContextId != this.browsingContext.id) {
+      return;
+    }
+
+    switch (name) {
+      case "remote-protocol:request":
+        try {
+          const { id, domainName, methodName, params } = data.request;
+          const inst = this.domains.get(domainName);
+          const methodFn = inst[methodName];
+          if (!methodFn || typeof methodFn != "function") {
+            throw new Error(`Method implementation of ${methodName} missing`);
+          }
+
+          const result = await methodFn.call(inst, params);
+
+          this.messageManager.sendAsyncMessage("remote-protocol:result", {
+            browsingContextId,
+            id,
+            result,
+          });
+        } catch (e) {
+          this.messageManager.sendAsyncMessage("remote-protocol:error", {
+            browsingContextId,
+            id: data.request.id,
+            error: {
+              name: e.name || "exception",
+              message: e.message || String(e),
+              stack: e.stack,
+            },
+          });
+        }
+        break;
+      case "remote-protocol:destroy":
+        this.destroy();
+        break;
+    }
+  }
+
+  // EventEmitter
+
+  // This method is called when any Domain emit any event
+  // Domains register "*" listener on each instantiated Domain.
+  onevent(eventName, params) {
+    this.messageManager.sendAsyncMessage("remote-protocol:event", {
+      browsingContextId: this.browsingContext.id,
+      event: {
+        method: eventName,
+        params,
+      },
+    });
+  }
+}
--- a/remote/Domain.jsm
+++ b/remote/Domain.jsm
@@ -15,22 +15,26 @@ class Domain {
     this.target = target;
     this.name = this.constructor.name;
 
     EventEmitter.decorate(this);
   }
 
   destructor() {}
 
-  get browser() {
-    return this.target.browser;
+  get content() {
+    return this.session.content;
   }
 
-  get mm() {
-    return this.browser.mm;
+  get docShell() {
+    return this.session.docShell;
+  }
+
+  get chromeEventHandler() {
+    return this.docShell.chromeEventHandler;
   }
 };
 
 XPCOMUtils.defineLazyModuleGetters(Domain, {
   Log: "chrome://remote/content/domain/Log.jsm",
   Network: "chrome://remote/content/domain/Network.jsm",
   Page: "chrome://remote/content/domain/Page.jsm",
   Runtime: "chrome://remote/content/domain/Runtime.jsm",
new file mode 100644
--- /dev/null
+++ b/remote/Domains.jsm
@@ -0,0 +1,56 @@
+/* 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 = ["Domains"];
+
+const {Domain} = ChromeUtils.import("chrome://remote/content/Domain.jsm");
+
+class Domains extends Map {
+  constructor(session) {
+    super();
+    this.session = session;
+  }
+
+  get(name) {
+    let inst = super.get(name);
+    if (!inst) {
+      inst = this.new(name);
+      this.set(inst);
+    }
+    return inst;
+  }
+
+  set(domain) {
+    super.set(domain.name, domain);
+  }
+
+  new(name) {
+    const Cls = Domain[name];
+    if (!Cls) {
+      throw new Error("No such domain: " + name);
+    }
+
+    const inst = new Cls(this.session, this.session.target);
+    inst.on("*", this.session);
+
+    return inst;
+  }
+
+  delete(name) {
+    const inst = super.get(name);
+    if (inst) {
+      inst.off("*");
+      inst.destructor();
+      super.delete(inst.name);
+    }
+  }
+
+  clear() {
+    for (const domainName of this.keys()) {
+      this.delete(domainName);
+    }
+  }
+}
--- a/remote/RemoteAgent.js
+++ b/remote/RemoteAgent.js
@@ -4,17 +4,16 @@
 
 "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",
-  ConsoleServiceObserver: "chrome://remote/content/ConsoleServiceObserver.jsm",
   HttpServer: "chrome://remote/content/server/HTTPD.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/Prefs.jsm",
   TabObserver: "chrome://remote/content/WindowManager.jsm",
@@ -29,52 +28,21 @@ const HTTPD = "remote.httpd";
 const SCHEME = `${HTTPD}.scheme`;
 const HOST = `${HTTPD}.host`;
 const PORT = `${HTTPD}.port`;
 
 const DEFAULT_HOST = "localhost";
 const DEFAULT_PORT = 9222;
 const LOOPBACKS = ["localhost", "127.0.0.1", "::1"];
 
-const ACTORS = {
-  DOM: {
-    child: {
-      module: "chrome://remote/content/actor/DOMChild.jsm",
-      events: {
-        DOMContentLoaded: {},
-        DOMWindowCreated: {once: true},
-        pageshow: {mozSystemGroup: true},
-        pagehide: {mozSystemGroup: true},
-      },
-    },
-  },
-
-  Log: {
-    child: {
-      module: "chrome://remote/content/actor/LogChild.jsm",
-      observers: [
-        "console-api-log-event",
-      ],
-    },
-  },
-};
-
 class ParentRemoteAgent {
   constructor() {
     this.server = null;
     this.targets = new Targets();
 
-    // TODO(ato): We need a way to dynamically load actors,
-    // otherwise these actors may risk being instantiated
-    // without the remote agent running!
-    ActorManagerParent.addActors(ACTORS);
-    ActorManagerParent.flush();
-
-    this.consoleService = new ConsoleServiceObserver();
-
     this.tabs = new TabObserver({registerExisting: true});
     this.tabs.on("open", tab => this.targets.connect(tab.linkedBrowser));
     this.tabs.on("close", tab => this.targets.disconnect(tab.linkedBrowser));
   }
 
   // nsIRemoteAgent
 
   get listening() {
@@ -122,17 +90,16 @@ class ParentRemoteAgent {
   async close() {
     if (this.listening) {
       try {
         await this.server.stop();
 
         Preferences.resetBranch(HTTPD);
         Preferences.reset(Object.keys(RecommendedPreferences));
 
-        this.consoleService.stop();
         this.tabs.stop();
         this.targets.clear();
       } catch (e) {
         throw new Error(`Unable to stop agent: ${e.message}`, e);
       }
 
       log.info("Stopped listening");
     }
@@ -185,18 +152,16 @@ class ParentRemoteAgent {
       log.fatal(`Expected address syntax [<host>]:<port>: ${flag}`);
       cmdLine.preventDefault = true;
       return;
     }
 
     await Observer.once("sessionstore-windows-restored");
     await this.tabs.start();
 
-    this.consoleService.start();
-
     try {
       this.listen(addr);
     } catch ({message}) {
       this.close();
       log.fatal(`Unable to start remote agent on ${addr.spec}: ${message}`);
       cmdLine.preventDefault = true;
     }
   }
--- a/remote/Session.jsm
+++ b/remote/Session.jsm
@@ -10,56 +10,75 @@ const {Domain} = ChromeUtils.import("chr
 const {formatError} = ChromeUtils.import("chrome://remote/content/Error.jsm");
 const {Protocol} = ChromeUtils.import("chrome://remote/content/Protocol.jsm");
 
 class Session {
   constructor(connection, target) {
     this.connection = connection;
     this.target = target;
 
-    this.domains = new Domains(this);
+    this.browsingContext = target.browser.browsingContext;
+    this.messageManager = target.browser.messageManager;
+    this.messageManager.loadFrameScript("chrome://remote/content/frame-script.js", false);
+    this.messageManager.addMessageListener("remote-protocol:event", this);
+    this.messageManager.addMessageListener("remote-protocol:result", this);
+    this.messageManager.addMessageListener("remote-protocol:error", this);
 
     this.connection.onmessage = this.dispatch.bind(this);
   }
 
   destructor() {
     this.connection.onmessage = null;
-    this.domains.clear();
+
+    this.messageManager.sendAsyncMessage("remote-protocol:destroy", {
+      browsingContextId: this.browsingContext.id,
+    });
+    this.messageManager.removeMessageListener("remote-protocol:event", this);
+    this.messageManager.removeMessageListener("remote-protocol:result", this);
+    this.messageManager.removeMessageListener("remote-protocol:error", this);
   }
 
   async dispatch({id, method, params}) {
     try {
       if (typeof id == "undefined") {
         throw new TypeError("Message missing 'id' field");
       }
       if (typeof method == "undefined") {
         throw new TypeError("Message missing 'method' field");
       }
 
       const [domainName, methodName] = split(method, ".", 1);
       assertSchema(domainName, methodName, params);
 
-      const inst = this.domains.get(domainName);
-      const methodFn = inst[methodName];
-      if (!methodFn || typeof methodFn != "function") {
-        throw new Error(`Method implementation of ${method} missing`);
-      }
-
-      const result = await methodFn.call(inst, params);
-      this.connection.send({id, result});
+      this.messageManager.sendAsyncMessage("remote-protocol:request", {
+        browsingContextId: this.browsingContext.id,
+        request: { id, domainName, methodName, params },
+      });
     } catch (e) {
       const error = formatError(e, {stack: true});
       this.connection.send({id, error});
     }
   }
 
-  // EventEmitter
-
-  onevent(eventName, params) {
-    this.connection.send({method: eventName, params});
+  receiveMessage({ name, data }) {
+    switch (name) {
+      case "remote-protocol:result":
+        this.connection.send({
+          id: data.id,
+          result: data.result,
+        });
+        break;
+      case "remote-protocol:event":
+        this.connection.send(data.event);
+        break;
+      case "remote-protocol:error":
+        const error = formatError(data.error, {stack: true});
+        this.connection.send({id: data.id, error});
+        break;
+    }
   }
 };
 
 function assertSchema(domainName, methodName, params) {
   const domain = Domain[domainName];
   if (!domain) {
     throw new TypeError("No such domain: " + domainName);
   }
@@ -71,63 +90,16 @@ function assertSchema(domainName, method
   const descriptor = (domain.schema.methods || {})[methodName];
   if (!Protocol.checkSchema(descriptor.params || {}, params, details)) {
     const {errorType, propertyName, propertyValue} = details;
     throw new TypeError(`${domainName}.${methodName} called ` +
         `with ${errorType} ${propertyName}: ${propertyValue}`);
   }
 }
 
-class Domains extends Map {
-  constructor(session) {
-    super();
-    this.session = session;
-  }
-
-  get(name) {
-    let inst = super.get(name);
-    if (!inst) {
-      inst = this.new(name);
-      this.set(inst);
-    }
-    return inst;
-  }
-
-  set(domain) {
-    super.set(domain.name, domain);
-  }
-
-  new(name) {
-    const Cls = Domain[name];
-    if (!Cls) {
-      throw new Error("No such domain: " + name);
-    }
-
-    const inst = new Cls(this.session, this.session.target);
-    inst.on("*", this.session);
-
-    return inst;
-  }
-
-  delete(name) {
-    const inst = super.get(name);
-    if (inst) {
-      inst.off("*");
-      inst.destructor();
-      super.delete(inst.name);
-    }
-  }
-
-  clear() {
-    for (const domainName of this.keys()) {
-      this.delete(domainName);
-    }
-  }
-}
-
 /**
  * Split s by sep, returning list of substrings.
  * If max is given, at most max splits are done.
  * If max is 0, there is no limit on the number of splits.
  */
 function split(s, sep, max = 0) {
   if (typeof s != "string" ||
       typeof sep != "string" ||
deleted file mode 100644
--- a/remote/actor/DOMChild.jsm
+++ /dev/null
@@ -1,19 +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 = ["DOMChild"];
-
-const {RemoteAgentActorChild} = ChromeUtils.import("chrome://remote/content/Actor.jsm");
-
-class DOMChild extends RemoteAgentActorChild {
-  handleEvent({type}) {
-    const event = {
-      type,
-      timestamp: Date.now(),
-    };
-    this.sendAsyncMessage("RemoteAgent:DOM:OnEvent", event);
-  }
-};
deleted file mode 100644
--- a/remote/actor/LogChild.jsm
+++ /dev/null
@@ -1,51 +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 = ["LogChild"];
-
-const {RemoteAgentActorChild} = ChromeUtils.import("chrome://remote/content/Actor.jsm");
-const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const {WindowManager} = ChromeUtils.import("chrome://remote/content/WindowManager.jsm");
-
-class LogChild extends RemoteAgentActorChild {
-  observe(subject, topic) {
-    const event = subject.wrappedJSObject;
-
-    if (this.isEventRelevant(event)) {
-      const entry = {
-        source: "javascript",
-        level: reduceLevel(event.level),
-        text: event.arguments.join(" "),
-        timestamp: event.timeStamp,
-        url: event.fileName,
-        lineNumber: event.lineNumber,
-      };
-      this.sendAsyncMessage("RemoteAgent:Log:OnConsoleAPIEvent", entry);
-    }
-  }
-
-  isEventRelevant({innerID}) {
-    const eventWin = Services.wm.getCurrentInnerWindowWithId(innerID);
-    if (!eventWin || !WindowManager.isWindowIncluded(this.content, eventWin)) {
-      return false;
-    }
-    return true;
-  }
-};
-
-function reduceLevel(level) {
-  switch (level) {
-  case "exception":
-    return "error";
-  case "warn":
-    return "warning";
-  case "debug":
-    return "verbose";
-  case "log":
-  default:
-    return "info";
-  }
-}
deleted file mode 100644
--- a/remote/actor/moz.build
+++ /dev/null
@@ -1,8 +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/.
-
-FINAL_TARGET_FILES.actors += [
-    "DOMChild.jsm",
-    "LogChild.jsm",
-]
--- a/remote/domain/Log.jsm
+++ b/remote/domain/Log.jsm
@@ -44,45 +44,53 @@ class Log extends Domain {
   destructor() {
     this.disable();
   }
 
   enable() {
     if (!this.enabled) {
       this.enabled = true;
 
-      // TODO(ato): Using the target content browser's MM here does not work
-      // because it disconnects when it suffers a host process change.
-      // That forces us to listen for Everything
-      // and do a target check in receiveMessage() below.
-      // Perhaps we can solve reattaching message listeners in a ParentActor?
-      Services.mm.addMessageListener("RemoteAgent:Log:OnConsoleAPIEvent", this);
-      Services.mm.addMessageListener("RemoteAgent:Log:OnConsoleServiceMessage", this);
+      Services.console.registerListener(this);
+      Services.obs.addObserver(this, "console-api-log-event");
     }
   }
 
   disable() {
     if (this.enabled) {
-      Services.mm.removeMessageListener("RemoteAgent:Log:OnConsoleAPIEvent", this);
-      Services.mm.removeMessageListener("RemoteAgent:Log:OnConsoleServiceMessage", this);
       this.enabled = false;
+      Services.console.unregisterListener(this);
+      Services.obs.removeObserver(this, "console-api-log-event");
     }
   }
 
-  // nsIMessageListener
+  // nsIObserver
 
-  receiveMessage({target, name, data}) {
-    // filter out Console API events that do not belong
-    // to the browsing context we are operating on
-    if (name == "RemoteAgent:Log:OnConsoleAPIEvent" &&
-        this.target.id !== data.browsingContextId) {
-      return;
+  /**
+   * Takes all script error messages belonging to the current window
+   * and emits a "console-service-message" event.
+   *
+   * @param {nsIConsoleMessage} message
+   *     Message originating from the nsIConsoleService.
+   */
+  observe(message, topic) {
+    let entry;
+    if (message instanceof Ci.nsIScriptError) {
+      entry = fromScriptError(message);
+    } else if (message instanceof Ci.nsIConsoleMessage) {
+      entry = fromConsoleMessage(message);
+    } else if (topic == "console-api-log-event") {
+      entry = fromConsoleAPI(message.wrappedJSObject);
     }
 
-    this.emit("Log.entryAdded", {entry: data});
+    this.emit("Log.entryAdded", {entry});
+  }
+
+  get QueryInterface() {
+    return ChromeUtils.generateQI([Ci.nsIConsoleListener]);
   }
 
   static get schema() {
     return {
       methods: {
         enable: {},
         disable: {},
       },
@@ -102,8 +110,78 @@ Log.LogEntry = {
     url: t.Optional(t.String),
     lineNumber: t.Optional(t.Number),
     stackTrace: t.Optional(Runtime.StackTrace.schema),
     networkRequestId: t.Optional(Network.RequestId.schema),
     workerId: t.Optional(t.String),
     args: t.Optional(t.Array(Runtime.RemoteObject.schema)),
   },
 };
+
+function fromConsoleMessage(message) {
+  const levels = {
+    [Ci.nsIConsoleMessage.debug]: "verbose",
+    [Ci.nsIConsoleMessage.info]: "info",
+    [Ci.nsIConsoleMessage.warn]: "warning",
+    [Ci.nsIConsoleMessage.error]: "error",
+  };
+  const level = levels[message.logLevel];
+
+  return {
+    source: "javascript",
+    level,
+    text: message.message,
+    timestamp: Date.now(),
+  };
+}
+
+function fromConsoleAPI(message) {
+  // message is a ConsoleEvent instance:
+  // https://searchfox.org/mozilla-central/rev/00c0d068ece99717bea7475f7dc07e61f7f35984/dom/webidl/Console.webidl#83-107
+
+  // A couple of possible level are defined here:
+  // https://searchfox.org/mozilla-central/rev/00c0d068ece99717bea7475f7dc07e61f7f35984/dom/console/Console.cpp#1086-1100
+  const levels = {
+    "log": "verbose",
+    "info": "info",
+    "warn": "warning",
+    "error": "error",
+    "exception": "error",
+
+  };
+  const level = levels[message.level] || "info";
+
+  return {
+    source: "javascript",
+    level,
+    text: message.arguments,
+    url: message.filename,
+    lineNumber: message.lineNumber,
+    stackTrace: message.stacktrace,
+    timestamp: Date.now(),
+  };
+}
+
+function fromScriptError(error) {
+  const {flags, errorMessage, sourceName, lineNumber, stack} = error;
+
+  // lossy reduction from bitmask to CDP string level
+  let level = "verbose";
+  if ((flags & Ci.nsIScriptError.exceptionFlag) ||
+      (flags & Ci.nsIScriptError.errorFlag)) {
+    level = "error";
+  } else if ((flags & Ci.nsIScriptError.warningFlag) ||
+      (flags & Ci.nsIScriptError.strictFlag)) {
+    level = "warning";
+  } else if (flags & Ci.nsIScriptError.infoFlag) {
+    level = "info";
+  }
+
+  return {
+    source: "javascript",
+    level,
+    text: errorMessage,
+    timestamp: Date.now(),
+    url: sourceName,
+    lineNumber,
+    stackTrace: stack,
+  };
+}
--- a/remote/domain/Page.jsm
+++ b/remote/domain/Page.jsm
@@ -16,79 +16,81 @@ class Page extends Domain {
     super(session, target);
     this.enabled = false;
   }
 
   destructor() {
     this.disable();
   }
 
+  QueryInterface(iid) {
+    if (iid.equals(Ci.nsIWebProgressListener) ||
+      iid.equals(Ci.nsISupportsWeakReference) ||
+      iid.equals(Ci.nsIObserver)) {
+      return this;
+    }
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  }
+
   // commands
 
   async enable() {
     if (!this.enabled) {
       this.enabled = true;
-      Services.mm.addMessageListener("RemoteAgent:DOM:OnEvent", this);
+      this.chromeEventHandler.addEventListener("DOMContentLoaded", this,
+        {mozSystemGroup: true});
+      this.chromeEventHandler.addEventListener("pageshow", this,
+        {mozSystemGroup: true});
     }
   }
 
   disable() {
     if (this.enabled) {
-      Services.mm.removeMessageListener("RemoteAgent:DOM:OnEvent", this);
+      this.chromeEventHandler.removeEventListener("DOMContentLoaded", this,
+        {mozSystemGroup: true});
+      this.chromeEventHandler.removeEventListener("pageshow", this,
+        {mozSystemGroup: true});
       this.enabled = false;
     }
   }
 
   async navigate({url, referrer, transitionType, frameId} = {}) {
     if (frameId) {
       throw new UnsupportedError("frameId not supported");
     }
 
     const opts = {
       loadFlags: transitionToLoadFlag(transitionType),
       referrerURI: referrer,
-      triggeringPrincipal: this.browser.contentPrincipal,
+      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
     };
-    this.browser.webNavigation.loadURI(url, opts);
+    this.docShell.QueryInterface(Ci.nsIWebNavigation);
+    this.docShell.loadURI(url, opts);
 
     return {frameId: "42"};
   }
 
   url() {
-    return this.browsrer.currentURI.spec;
+    return this.content.location.href;
   }
 
-  onDOMEvent({type}) {
+  handleEvent({type}) {
     const timestamp = Date.now();
 
     switch (type) {
     case "DOMContentLoaded":
       this.emit("Page.domContentEventFired", {timestamp});
       break;
 
     case "pageshow":
       this.emit("Page.loadEventFired", {timestamp});
       break;
     }
   }
 
-  // nsIMessageListener
-
-  receiveMessage({target, name, data}) {
-    if (target !== this.target.browser) {
-      return;
-    }
-
-    switch (name) {
-    case "RemoteAgent:DOM:OnEvent":
-      this.onDOMEvent(data);
-      break;
-    }
-  }
-
   static get schema() {
     return {
       methods: {
         enable: {},
         disable: {},
         navigate: {
           params: {
             url: t.String,
new file mode 100644
--- /dev/null
+++ b/remote/frame-script.js
@@ -0,0 +1,11 @@
+/* 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";
+
+const {ContentProcessSession} =
+  ChromeUtils.import("chrome://remote/content/ContentProcessSession.jsm");
+
+/* global content, docShell */
+new ContentProcessSession(this, docShell.browsingContext, content, docShell);
--- a/remote/jar.mn
+++ b/remote/jar.mn
@@ -2,42 +2,42 @@
 # 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/Actor.jsm (Actor.jsm)
   content/Collections.jsm (Collections.jsm)
   content/Connection.jsm (Connection.jsm)
-  content/ConsoleServiceObserver.jsm (ConsoleServiceObserver.jsm)
+  content/ContentProcessSession.jsm (ContentProcessSession.jsm)
   content/Domain.jsm (Domain.jsm)
+  content/Domains.jsm (Domains.jsm)
   content/Error.jsm (Error.jsm)
   content/EventEmitter.jsm (EventEmitter.jsm)
   content/Handler.jsm (Handler.jsm)
   content/Log.jsm (Log.jsm)
   content/MessageChannel.jsm (MessageChannel.jsm)
   content/Observer.jsm (Observer.jsm)
   content/Prefs.jsm (Prefs.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/WindowManager.jsm (WindowManager.jsm)
 
+  # Frame scripts
+  content/frame-script.js (frame-script.js)
+
   # domains
   content/domain/Log.jsm (domain/Log.jsm)
   content/domain/Network.jsm (domain/Network.jsm)
   content/domain/Page.jsm (domain/Page.jsm)
   content/domain/Runtime.jsm (domain/Runtime.jsm)
 
-  # actors
-  content/actor/DOMChild.jsm (actor/DOMChild.jsm)
-  content/actor/LogChild.jsm (actor/LogChild.jsm)
-
   # transport layer
   content/server/HTTPD.jsm (../netwerk/test/httpserver/httpd.js)
   content/server/Packet.jsm (server/Packet.jsm)
   content/server/Socket.jsm (server/Socket.jsm)
   content/server/Stream.jsm (server/Stream.jsm)
   content/server/Transport.jsm (server/Transport.jsm)
   content/server/WebSocket.jsm (server/WebSocket.jsm)
   content/server/WebSocketTransport.jsm (server/WebSocketTransport.jsm)
--- a/remote/moz.build
+++ b/remote/moz.build
@@ -1,14 +1,13 @@
 # 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/.
 
 DIRS += [
-  "actor",
   "pref",
   "test",
 ]
 
 EXTRA_COMPONENTS += [
     "RemoteAgent.js",
     "RemoteAgent.manifest",
 ]