Bug 1477988 - Implements DebuggerServer.spawnActorInParentProcess. r=jryans draft
authorAlexandre Poirot <poirot.alex@gmail.com>
Mon, 16 Jul 2018 09:53:28 -0700
changeset 822879 836ddf2dc1af6554610250ae28fc80db4fd514ae
parent 822878 f6a2924ee429bde8789367df4fb1c3a5d2295d06
child 823131 c73a95da2ad6de8de6cfbccd84014f93a346206c
push id117505
push userbmo:poirot.alex@gmail.com
push dateThu, 26 Jul 2018 08:14:55 +0000
reviewersjryans
bugs1477988
milestone63.0a1
Bug 1477988 - Implements DebuggerServer.spawnActorInParentProcess. r=jryans This new API allows to instanciate an actor in the parent process from actors running in the parent process. The created actors are returned to the client from the actors running in content, but after that, the client communicates directly with the created actors running in the parent process. MozReview-Commit-ID: 5B3wRQ94UEx
devtools/server/main.js
devtools/server/tests/browser/browser.ini
devtools/server/tests/browser/browser_spawn_actor_in_parent.js
devtools/server/tests/browser/test-spawn-actor-in-parent.js
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -873,25 +873,27 @@ var DebuggerServer = {
       // Get messageManager from XUL browser (which might be a specialized tunnel for RDM)
       // or else fallback to asking the frameLoader itself.
       let mm = frame.messageManager || frame.frameLoader.messageManager;
       mm.loadFrameScript("resource://devtools/server/startup/frame.js", false);
 
       const trackMessageManager = () => {
         frame.addEventListener("DevTools:BrowserSwap", onBrowserSwap);
         mm.addMessageListener("debug:setup-in-parent", onSetupInParent);
+        mm.addMessageListener("debug:spawn-actor-in-parent", onSpawnActorInParent);
         if (!actor) {
           mm.addMessageListener("debug:actor", onActorCreated);
         }
         DebuggerServer._childMessageManagers.add(mm);
       };
 
       const untrackMessageManager = () => {
         frame.removeEventListener("DevTools:BrowserSwap", onBrowserSwap);
         mm.removeMessageListener("debug:setup-in-parent", onSetupInParent);
+        mm.removeMessageListener("debug:spawn-actor-in-parent", onSpawnActorInParent);
         if (!actor) {
           mm.removeMessageListener("debug:actor", onActorCreated);
         }
         DebuggerServer._childMessageManagers.delete(mm);
       };
 
       let actor, childTransport;
       const prefix = connection.allocID("child");
@@ -927,25 +929,74 @@ var DebuggerServer = {
             "Exception during actor module setup running in the parent process: ";
           DevToolsUtils.reportException(errorMessage + e);
           dumpn(`ERROR: ${errorMessage}\n\t module: '${module}'\n\t ` +
                 `setupParent: '${setupParent}'\n${DevToolsUtils.safeErrorString(e)}`);
           return false;
         }
       };
 
+      const onSpawnActorInParent = function(msg) {
+        // We may have multiple connectToFrame instance running for the same tab
+        // and need to filter the messages.
+        if (msg.json.prefix != connPrefix) {
+          return;
+        }
+
+        const { module, constructor, args, spawnedByActorID } = msg.json;
+        let m;
+
+        try {
+          m = require(module);
+
+          if (!(constructor in m)) {
+            dump(`ERROR: module '${module}' does not export '${constructor}'`);
+            return;
+          }
+
+          const Constructor = m[constructor];
+          // Bind the actor to parent process connection so that these actors
+          // directly communicates with the client as regular actors instanciated from
+          // parent process
+          const instance = new Constructor(connection, ...args, mm);
+          instance.conn = connection;
+          instance.parentID = spawnedByActorID;
+
+          // Manually set the actor ID in order to insert parent actorID as prefix
+          // in order to help identifying actor hiearchy via actor IDs.
+          // Remove `/` as it may confuse message forwarding between processes.
+          const contentPrefix = spawnedByActorID.replace(connection.prefix, "")
+                                                .replace("/", "-");
+          instance.actorID = connection.allocID(contentPrefix + "/" + instance.typeName);
+          connection.addActor(instance);
+
+          mm.sendAsyncMessage("debug:spawn-actor-in-parent:actor", {
+            prefix: connPrefix,
+            actorID: instance.actorID
+          });
+        } catch (e) {
+          const errorMessage =
+            "Exception during actor module setup running in the parent process: ";
+          DevToolsUtils.reportException(errorMessage + e + "\n" + e.stack);
+          dumpn(`ERROR: ${errorMessage}\n\t module: '${module}'\n\t ` +
+                `constructor: '${constructor}'\n${DevToolsUtils.safeErrorString(e)}`);
+        }
+      };
+
       const onActorCreated = DevToolsUtils.makeInfallible(function(msg) {
         if (msg.json.prefix != prefix) {
           return;
         }
         mm.removeMessageListener("debug:actor", onActorCreated);
 
         // Pipe Debugger message from/to parent/child via the message manager
         childTransport = new ChildDebuggerTransport(mm, prefix);
         childTransport.hooks = {
+          // Pipe all the messages from content process actors back to the client
+          // through the parent process connection.
           onPacket: connection.send.bind(connection),
           onClosed() {}
         };
         childTransport.ready();
 
         connection.setForwarding(prefix, childTransport);
 
         dumpn(`Start forwarding for frame with prefix ${prefix}`);
@@ -1335,16 +1386,17 @@ function DebuggerServerConnection(prefix
   /*
    * We can forward packets to other servers, if the actors on that server
    * all use a distinct prefix on their names. This is a map from prefixes
    * to transports: it maps a prefix P to a transport T if T conveys
    * packets to the server whose actors' names all begin with P + "/".
    */
   this._forwardingPrefixes = new Map();
 }
+exports.DebuggerServerConnection = DebuggerServerConnection;
 
 DebuggerServerConnection.prototype = {
   _prefix: null,
   get prefix() {
     return this._prefix;
   },
 
   _transport: null,
@@ -1804,9 +1856,54 @@ DebuggerServerConnection.prototype = {
     const { sendSyncMessage } = this.parentMessageManager;
 
     return sendSyncMessage("debug:setup-in-parent", {
       prefix: this.prefix,
       module: module,
       setupParent: setupParent
     });
   },
+
+  /**
+   * Instanciates a protocol.js actor in the parent process, from the content process
+   * module is the absolute path to protocol.js actor module
+   *
+   * @param spawnByActorID string
+   *        The actor ID of the actor that is requesting an actor to be created.
+   *        This is used as a prefix to compute the actor id of the actor created
+   *        in the parent process.
+   * @param module string
+   *        Absolute path for the actor module to load.
+   * @param constructor string
+   *        The symbol exported by this module that implements Actor.
+   * @param args array
+   *        Arguments to pass to its constructor
+   */
+  spawnActorInParentProcess(spawnedByActorID, { module, constructor, args }) {
+    if (!this.parentMessageManager) {
+      return null;
+    }
+
+    const { addMessageListener, removeMessageListener, sendAsyncMessage } =
+      this.parentMessageManager;
+
+    const onResponse = new Promise(done => {
+      const listener = msg => {
+        if (msg.json.prefix != this.prefix) {
+          return;
+        }
+        removeMessageListener("debug:spawn-actor-in-parent:actor", listener);
+        done(msg.json.actorID);
+      };
+      addMessageListener("debug:spawn-actor-in-parent:actor", listener);
+    });
+
+    sendAsyncMessage("debug:spawn-actor-in-parent", {
+      prefix: this.prefix,
+      module,
+      constructor,
+      args,
+      spawnedByActorID,
+    });
+
+    return onResponse;
+  }
 };
--- a/devtools/server/tests/browser/browser.ini
+++ b/devtools/server/tests/browser/browser.ini
@@ -17,16 +17,17 @@ support-files =
   navigate-second.html
   storage-cookies-same-name.html
   storage-dynamic-windows.html
   storage-listings.html
   storage-unsecured-iframe.html
   storage-updates.html
   storage-secured-iframe.html
   stylesheets-nested-iframes.html
+  test-spawn-actor-in-parent.js
   timeline-iframe-child.html
   timeline-iframe-parent.html
   storage-helpers.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
   !/devtools/server/tests/mochitest/hello-actor.js
 
 [browser_accessibility_node.js]
@@ -89,16 +90,17 @@ skip-if = true # Needs to be updated for
 skip-if = true # Needs to be updated for async actor destruction
 [browser_perf-realtime-markers.js]
 [browser_perf-recording-actor-01.js]
 skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
 [browser_perf-recording-actor-02.js]
 skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
 [browser_perf-samples-01.js]
 [browser_perf-samples-02.js]
+[browser_spawn_actor_in_parent.js]
 [browser_storage_browser_toolbox_indexeddb.js]
 [browser_storage_cookies-duplicate-names.js]
 [browser_storage_dynamic_windows.js]
 [browser_storage_listings.js]
 [browser_storage_updates.js]
 skip-if = (verify && debug && (os == 'mac' || os == 'linux'))
 [browser_stylesheets_getTextEmpty.js]
 [browser_stylesheets_nested-iframes.js]
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_spawn_actor_in_parent.js
@@ -0,0 +1,48 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test DebuggerServerConnection.spawnActorInParentProcess.
+// This test instanciates a first test actor "InContentActor" that uses
+// spawnActorInParentProcess to instanciate the second test actor "InParentActor"
+
+const ACTOR_URL = "chrome://mochitests/content/browser/devtools/server/tests/browser/test-spawn-actor-in-parent.js";
+
+const { InContentFront, InParentFront } = require(ACTOR_URL);
+
+add_task(async function() {
+  await addTab("data:text/html;charset=utf-8,foo");
+
+  info("Register target-scoped actor in the content process");
+  await registerActorInContentProcess(ACTOR_URL, {
+    prefix: "inContent",
+    constructor: "InContentActor",
+    type: { target: true },
+  });
+
+  initDebuggerServer();
+  const client = new DebuggerClient(DebuggerServer.connectPipe());
+  const form = await connectDebuggerClient(client);
+  const inContentFront = InContentFront(client, form);
+  const isInContent = await inContentFront.isInContent();
+  ok(isInContent, "ContentActor really runs in the content process");
+  const formSpawn = await inContentFront.spawnInParent(ACTOR_URL);
+  const inParentFront = InParentFront(client, formSpawn);
+  const {
+    args,
+    isInParent,
+    conn,
+    mm
+  } = await inParentFront.test();
+  is(args[0], 1, "first actor constructor arg is correct");
+  is(args[1], 2, "first actor constructor arg is correct");
+  is(args[2], 3, "first actor constructor arg is correct");
+  ok(isInParent, "The ParentActor really runs in the parent process");
+  ok(conn, "`conn`, first contructor argument is a DebuggerServerConnection instance");
+  is(mm, "ChromeMessageSender", "`mm`, last constructor argument is a message manager");
+
+  await client.close();
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/test-spawn-actor-in-parent.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const protocol = require("devtools/shared/protocol");
+const { DebuggerServerConnection } = require("devtools/server/main");
+const Services = require("Services");
+
+const inContentSpec = protocol.generateActorSpec({
+  typeName: "inContent",
+
+  methods: {
+    isInContent: {
+      request: {},
+      response: {
+        isInContent: protocol.RetVal("boolean")
+      }
+    },
+    spawnInParent: {
+      request: {
+        url: protocol.Arg(0)
+      },
+      response: protocol.RetVal("json")
+    }
+  }
+});
+
+exports.InContentActor = protocol.ActorClassWithSpec(inContentSpec, {
+  initialize: function(conn) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+  },
+
+  isInContent: function() {
+    return Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+  },
+
+  spawnInParent: async function(url) {
+    const actorID = await this.conn.spawnActorInParentProcess(this.actorID, {
+      module: url,
+      constructor: "InParentActor",
+      // In the browser mochitest script, we are asserting these arguments passed to
+      // InParentActor constructor
+      args: [1, 2, 3]
+    });
+    return {
+      inParentActor: actorID
+    };
+  }
+});
+
+exports.InContentFront = protocol.FrontClassWithSpec(inContentSpec, {
+  initialize: function(client, tabForm) {
+    protocol.Front.prototype.initialize.call(this, client);
+    this.actorID = tabForm.inContentActor;
+    this.manage(this);
+  }
+});
+
+const inParentSpec = protocol.generateActorSpec({
+  typeName: "inParent",
+
+  methods: {
+    test: {
+      request: {},
+      response: protocol.RetVal("json")
+    }
+  }
+});
+
+exports.InParentActor = protocol.ActorClassWithSpec(inParentSpec, {
+  initialize: function(conn, a1, a2, a3, mm) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    // We save all arguments to later assert them in `test` request
+    this.conn = conn;
+    this.args = [a1, a2, a3];
+    this.mm = mm;
+  },
+
+  test: function() {
+    return {
+      args: this.args,
+      isInParent: Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT,
+      conn: this.conn instanceof DebuggerServerConnection,
+      // We don't have access to MessageListenerManager in Sandboxes,
+      // so fallback to constructor name checks...
+      mm: Object.getPrototypeOf(this.mm).constructor.name
+    };
+  }
+});
+
+exports.InParentFront = protocol.FrontClassWithSpec(inParentSpec, {
+  initialize: function(client, tabForm) {
+    protocol.Front.prototype.initialize.call(this, client);
+    this.actorID = tabForm.inParentActor;
+    this.manage(this);
+  }
+});