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
--- 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);
+ }
+});