diff options
Diffstat (limited to 'devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs')
-rw-r--r-- | devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs | 362 |
1 files changed, 362 insertions, 0 deletions
diff --git a/devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs b/devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs new file mode 100644 index 0000000000..9e8ad64eea --- /dev/null +++ b/devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs @@ -0,0 +1,362 @@ +/* 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/. */ + +import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters( + lazy, + { + releaseDistinctSystemPrincipalLoader: + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", + useDistinctSystemPrincipalLoader: + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", + }, + { global: "contextual" } +); + +// Name of the attribute into which we save data in `sharedData` object. +const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; + +// If true, log info about DOMProcess's being created. +const DEBUG = false; + +/** + * Print information about operation being done against each content process. + * + * @param {nsIDOMProcessChild} domProcessChild + * The process for which we should log a message. + * @param {String} message + * Message to log. + */ +function logDOMProcess(domProcessChild, message) { + if (!DEBUG) { + return; + } + dump(" [pid:" + domProcessChild + "] " + message + "\n"); +} + +export class DevToolsProcessChild extends JSProcessActorChild { + constructor() { + super(); + + // The map is indexed by the Watcher Actor ID. + // The values are objects containing the following properties: + // - connection: the DevToolsServerConnection itself + // - actor: the ContentProcessTargetActor instance + this._connections = new Map(); + + this._onConnectionChange = this._onConnectionChange.bind(this); + EventEmitter.decorate(this); + } + + instantiate() { + const { sharedData } = Services.cpmm; + const watchedDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME); + if (!watchedDataByWatcherActor) { + throw new Error( + "Request to instantiate the target(s) for the process, but `sharedData` is empty about watched targets" + ); + } + + // Create one Target actor for each prefix/client which listen to processes + for (const [watcherActorID, sessionData] of watchedDataByWatcherActor) { + const { connectionPrefix } = sessionData; + + if (sessionData.targets?.includes("process")) { + this._createTargetActor(watcherActorID, connectionPrefix, sessionData); + } + } + } + + /** + * Instantiate a new ProcessTarget for the given connection. + * + * @param String watcherActorID + * The ID of the WatcherActor who requested to observe and create these target actors. + * @param String parentConnectionPrefix + * The prefix of the DevToolsServerConnection of the Watcher Actor. + * This is used to compute a unique ID for the target actor. + * @param Object sessionData + * All data managed by the Watcher Actor and WatcherRegistry.sys.mjs, containing + * target types, resources types to be listened as well as breakpoints and any + * other data meant to be shared across processes and threads. + */ + _createTargetActor(watcherActorID, parentConnectionPrefix, sessionData) { + // This method will be concurrently called from `observe()` and `DevToolsProcessParent:instantiate-already-available` + // When the JSprocessActor initializes itself and when the watcher want to force instantiating existing targets. + // Simply ignore the second call as there is nothing to return, neither to wait for as this method is synchronous. + if (this._connections.has(watcherActorID)) { + return; + } + + // Compute a unique prefix, just for this DOM Process, + // which will be used to create a JSWindowActorTransport pair between content and parent processes. + // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, + // but here, we can't have access to any DevTools connection as we are really early in the content process startup + // XXX: nsIDOMProcessChild's childID should be unique across processes, I think. So that should be safe? + // (this.manager == nsIDOMProcessChild interface) + // Ensure appending a final slash, otherwise the prefix may be the same between childID 1 and 10... + const forwardingPrefix = + parentConnectionPrefix + "contentProcess" + this.manager.childID + "/"; + + logDOMProcess( + this.manager, + "Instantiate ContentProcessTarget with prefix: " + forwardingPrefix + ); + + const { connection, targetActor } = this._createConnectionAndActor( + watcherActorID, + forwardingPrefix, + sessionData + ); + this._connections.set(watcherActorID, { + connection, + actor: targetActor, + }); + + // Immediately queue a message for the parent process, + // in order to ensure that the JSWindowActorTransport is instantiated + // before any packet is sent from the content process. + // As the order of messages is guaranteed to be delivered in the order they + // were queued, we don't have to wait for anything around this sendAsyncMessage call. + // In theory, the ContentProcessTargetActor may emit events in its constructor. + // If it does, such RDP packets may be lost. But in practice, no events + // are emitted during its construction. Instead the frontend will start + // the communication first. + this.sendAsyncMessage("DevToolsProcessChild:connectFromContent", { + watcherActorID, + forwardingPrefix, + actor: targetActor.form(), + }); + + // Pass initialization data to the target actor + for (const type in sessionData) { + // `sessionData` will also contain `browserId` as well as entries with empty arrays, + // which shouldn't be processed. + const entries = sessionData[type]; + if (!Array.isArray(entries) || !entries.length) { + continue; + } + targetActor.addOrSetSessionDataEntry( + type, + sessionData[type], + false, + "set" + ); + } + } + + _destroyTargetActor(watcherActorID, isModeSwitching) { + const connectionInfo = this._connections.get(watcherActorID); + // This connection has already been cleaned? + if (!connectionInfo) { + throw new Error( + `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}` + ); + } + connectionInfo.connection.close({ isModeSwitching }); + this._connections.delete(watcherActorID); + if (this._connections.size == 0) { + this.didDestroy({ isModeSwitching }); + } + } + + _createConnectionAndActor(watcherActorID, forwardingPrefix, sessionData) { + if (!this.loader) { + this.loader = lazy.useDistinctSystemPrincipalLoader(this); + } + const { DevToolsServer } = this.loader.require( + "devtools/server/devtools-server" + ); + + const { ContentProcessTargetActor } = this.loader.require( + "devtools/server/actors/targets/content-process" + ); + + DevToolsServer.init(); + + // For browser content toolbox, we do need a regular root actor and all tab + // actors, but don't need all the "browser actors" that are only useful when + // debugging the parent process via the browser toolbox. + DevToolsServer.registerActors({ target: true }); + DevToolsServer.on("connectionchange", this._onConnectionChange); + + const connection = DevToolsServer.connectToParentWindowActor( + this, + forwardingPrefix, + "DevToolsProcessChild:packet" + ); + + // Create the actual target actor. + const targetActor = new ContentProcessTargetActor(connection, { + sessionContext: sessionData.sessionContext, + }); + // There is no root actor in content processes and so + // the target actor can't be managed by it, but we do have to manage + // the actor to have it working and be registered in the DevToolsServerConnection. + // We make it manage itself and become a top level actor. + targetActor.manage(targetActor); + + const form = targetActor.form(); + targetActor.once("destroyed", options => { + // This will destroy the content process one + this._destroyTargetActor(watcherActorID, options.isModeSwitching); + // And this will destroy the parent process one + try { + this.sendAsyncMessage("DevToolsProcessChild:destroy", { + actors: [ + { + watcherActorID, + form, + }, + ], + options, + }); + } catch (e) { + // Ignore exception when the JSProcessActorChild has already been destroyed. + // We often try to emit this message while the process is being destroyed, + // but sendAsyncMessage doesn't have time to complete and throws. + if ( + !e.message.includes("JSProcessActorChild cannot send at the moment") + ) { + throw e; + } + } + }); + + return { connection, targetActor }; + } + + /** + * Destroy the server once its last connection closes. Note that multiple + * frame scripts may be running in parallel and reuse the same server. + */ + _onConnectionChange() { + if (this._destroyed) { + return; + } + this._destroyed = true; + + const { DevToolsServer } = this.loader.require( + "devtools/server/devtools-server" + ); + + // Only destroy the server if there is no more connections to it. It may be + // used to debug another tab running in the same process. + if (DevToolsServer.hasConnection() || DevToolsServer.keepAlive) { + return; + } + + DevToolsServer.off("connectionchange", this._onConnectionChange); + DevToolsServer.destroy(); + } + + /** + * Supported Queries + */ + + sendPacket(packet, prefix) { + this.sendAsyncMessage("DevToolsProcessChild:packet", { packet, prefix }); + } + + /** + * JsWindowActor API + */ + + async sendQuery(msg, args) { + try { + const res = await super.sendQuery(msg, args); + return res; + } catch (e) { + console.error("Failed to sendQuery in DevToolsProcessChild", msg); + console.error(e.toString()); + throw e; + } + } + + receiveMessage(message) { + switch (message.name) { + case "DevToolsProcessParent:instantiate-already-available": { + const { watcherActorID, connectionPrefix, sessionData } = message.data; + return this._createTargetActor( + watcherActorID, + connectionPrefix, + sessionData + ); + } + case "DevToolsProcessParent:destroy": { + const { watcherActorID, isModeSwitching } = message.data; + return this._destroyTargetActor(watcherActorID, isModeSwitching); + } + case "DevToolsProcessParent:addOrSetSessionDataEntry": { + const { watcherActorID, type, entries, updateType } = message.data; + return this._addOrSetSessionDataEntry( + watcherActorID, + type, + entries, + updateType + ); + } + case "DevToolsProcessParent:removeSessionDataEntry": { + const { watcherActorID, type, entries } = message.data; + return this._removeSessionDataEntry(watcherActorID, type, entries); + } + case "DevToolsProcessParent:packet": + return this.emit("packet-received", message); + default: + throw new Error( + "Unsupported message in DevToolsProcessParent: " + message.name + ); + } + } + + _getTargetActorForWatcherActorID(watcherActorID) { + const connectionInfo = this._connections.get(watcherActorID); + return connectionInfo?.actor; + } + + _addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) { + const targetActor = this._getTargetActorForWatcherActorID(watcherActorID); + if (!targetActor) { + throw new Error( + `No target actor for this Watcher Actor ID:"${watcherActorID}"` + ); + } + return targetActor.addOrSetSessionDataEntry( + type, + entries, + false, + updateType + ); + } + + _removeSessionDataEntry(watcherActorID, type, entries) { + const targetActor = this._getTargetActorForWatcherActorID(watcherActorID); + // By the time we are calling this, the target may already have been destroyed. + if (!targetActor) { + return null; + } + return targetActor.removeSessionDataEntry(type, entries); + } + + observe(subject, topic) { + if (topic === "init-devtools-content-process-actor") { + // This is triggered by the process actor registration and some code in process-helper.js + // which defines a unique topic to be observed + this.instantiate(); + } + } + + didDestroy(options) { + for (const { connection } of this._connections.values()) { + connection.close(options); + } + this._connections.clear(); + if (this.loader) { + lazy.releaseDistinctSystemPrincipalLoader(this); + this.loader = null; + } + } +} |