summaryrefslogtreecommitdiffstats
path: root/devtools/server/connectors/frame-connector.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/connectors/frame-connector.js')
-rw-r--r--devtools/server/connectors/frame-connector.js276
1 files changed, 276 insertions, 0 deletions
diff --git a/devtools/server/connectors/frame-connector.js b/devtools/server/connectors/frame-connector.js
new file mode 100644
index 0000000000..6b88845fe0
--- /dev/null
+++ b/devtools/server/connectors/frame-connector.js
@@ -0,0 +1,276 @@
+/* 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 Services = require("Services");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var { dumpn } = DevToolsUtils;
+const { Pool } = require("devtools/shared/protocol/Pool");
+
+loader.lazyRequireGetter(
+ this,
+ "DevToolsServer",
+ "devtools/server/devtools-server",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "ChildDebuggerTransport",
+ "devtools/shared/transport/child-transport",
+ true
+);
+
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+
+/**
+ * Start a DevTools server in a remote frame's process and add it as a child server for
+ * an active connection.
+ *
+ * @param object connection
+ * The devtools server connection to use.
+ * @param Element frame
+ * The frame element with remote content to connect to.
+ * @param function [onDestroy]
+ * Optional function to invoke when the child process closes or the connection
+ * shuts down. (Need to forget about the related target actor.)
+ * @return object
+ * A promise object that is resolved once the connection is established.
+ */
+function connectToFrame(connection, frame, onDestroy, { addonId } = {}) {
+ return new Promise(resolve => {
+ // Get messageManager from XUL browser (which might be a specialized tunnel for RDM)
+ // or else fallback to asking the frameLoader itself.
+ const mm = frame.messageManager || frame.frameLoader.messageManager;
+ mm.loadFrameScript("resource://devtools/server/startup/frame.js", false);
+
+ const spawnInParentActorPool = new Pool(
+ connection,
+ "connectToFrame-spawnInParent"
+ );
+ connection.addActor(spawnInParentActorPool);
+
+ const trackMessageManager = () => {
+ mm.addMessageListener("debug:setup-in-parent", onSetupInParent);
+ mm.addMessageListener(
+ "debug:spawn-actor-in-parent",
+ onSpawnActorInParent
+ );
+ if (!actor) {
+ mm.addMessageListener("debug:actor", onActorCreated);
+ }
+ };
+
+ const untrackMessageManager = () => {
+ mm.removeMessageListener("debug:setup-in-parent", onSetupInParent);
+ mm.removeMessageListener(
+ "debug:spawn-actor-in-parent",
+ onSpawnActorInParent
+ );
+ if (!actor) {
+ mm.removeMessageListener("debug:actor", onActorCreated);
+ }
+ };
+
+ let actor, childTransport;
+ const prefix = connection.allocID("child");
+ // Compute the same prefix that's used by DevToolsServerConnection
+ const connPrefix = prefix + "/";
+
+ // provides hook to actor modules that need to exchange messages
+ // between e10s parent and child processes
+ const parentModules = [];
+ const onSetupInParent = function(msg) {
+ // We may have multiple connectToFrame instance running for the same frame and
+ // need to filter the messages.
+ if (msg.json.prefix != connPrefix) {
+ return false;
+ }
+
+ const { module, setupParent } = msg.json;
+ let m;
+
+ try {
+ m = require(module);
+
+ if (!(setupParent in m)) {
+ dumpn(`ERROR: module '${module}' does not export '${setupParent}'`);
+ return false;
+ }
+
+ parentModules.push(m[setupParent]({ mm, prefix: connPrefix }));
+
+ return true;
+ } catch (e) {
+ const errorMessage =
+ "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 parentActors = [];
+ 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 hierarchy 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
+ );
+ spawnInParentActorPool.manage(instance);
+
+ mm.sendAsyncMessage("debug:spawn-actor-in-parent:actor", {
+ prefix: connPrefix,
+ actorID: instance.actorID,
+ });
+
+ parentActors.push(instance);
+ } 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),
+ };
+ childTransport.ready();
+
+ connection.setForwarding(prefix, childTransport);
+
+ dumpn(`Start forwarding for frame with prefix ${prefix}`);
+
+ actor = msg.json.actor;
+ resolve(actor);
+ });
+
+ const destroy = DevToolsUtils.makeInfallible(function() {
+ EventEmitter.off(connection, "closed", destroy);
+ Services.obs.removeObserver(
+ onMessageManagerClose,
+ "message-manager-close"
+ );
+
+ // provides hook to actor modules that need to exchange messages
+ // between e10s parent and child processes
+ parentModules.forEach(mod => {
+ if (mod.onDisconnected) {
+ mod.onDisconnected();
+ }
+ });
+ // TODO: Remove this deprecated path once it's no longer needed by add-ons.
+ DevToolsServer.emit("disconnected-from-child:" + connPrefix, {
+ mm,
+ prefix: connPrefix,
+ });
+
+ // Destroy all actors created via spawnActorInParentProcess
+ // in case content wasn't able to destroy them via a message
+ spawnInParentActorPool.destroy();
+
+ if (actor) {
+ // The FrameTargetActor within the child process doesn't necessary
+ // have time to uninitialize itself when the frame is closed/killed.
+ // So ensure telling the client that the related actor is detached.
+ connection.send({ from: actor.actor, type: "tabDetached" });
+ actor = null;
+ }
+
+ if (childTransport) {
+ // If we have a child transport, the actor has already
+ // been created. We need to stop using this message manager.
+ childTransport.close();
+ childTransport = null;
+ connection.cancelForwarding(prefix);
+
+ // ... and notify the child process to clean the target-scoped actors.
+ try {
+ // Bug 1169643: Ignore any exception as the child process
+ // may already be destroyed by now.
+ mm.sendAsyncMessage("debug:disconnect", { prefix });
+ } catch (e) {
+ // Nothing to do
+ }
+ } else {
+ // Otherwise, the frame has been closed before the actor
+ // had a chance to be created, so we are not able to create
+ // the actor.
+ resolve(null);
+ }
+
+ if (onDestroy) {
+ onDestroy(mm);
+ }
+
+ // Cleanup all listeners
+ untrackMessageManager();
+ });
+
+ // Listen for various messages and frame events
+ trackMessageManager();
+
+ // Listen for app process exit
+ const onMessageManagerClose = function(subject, topic, data) {
+ if (subject == mm) {
+ destroy();
+ }
+ };
+ Services.obs.addObserver(onMessageManagerClose, "message-manager-close");
+
+ // Listen for connection close to cleanup things
+ // when user unplug the device or we lose the connection somehow.
+ EventEmitter.on(connection, "closed", destroy);
+
+ mm.sendAsyncMessage("debug:connect", { prefix, addonId });
+ });
+}
+
+exports.connectToFrame = connectToFrame;