diff options
Diffstat (limited to 'devtools/server/connectors/frame-connector.js')
-rw-r--r-- | devtools/server/connectors/frame-connector.js | 276 |
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; |