/* 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"; const { WatcherRegistry } = ChromeUtils.importESModule( "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs", { // WatcherRegistry needs to be a true singleton and loads ActorManagerParent // which also has to be a true singleton. loadInDevToolsLoader: false, } ); loader.lazyRequireGetter( this, "ChildDebuggerTransport", "resource://devtools/shared/transport/child-transport.js", true ); const CONTENT_PROCESS_SCRIPT = "resource://devtools/server/startup/content-process-script.js"; /** * Map a MessageManager key to an Array of ContentProcessTargetActor "description" objects. * A single MessageManager might be linked to several ContentProcessTargetActors if there are several * Watcher actors instantiated on the DevToolsServer, via a single connection (in theory), but rather * via distinct connections (ex: a content toolbox and the browser toolbox). * Note that if we spawn two DevToolsServer, this module will be instantiated twice. * * Each ContentProcessTargetActor "description" object is structured as follows * - {Object} actor: form of the content process target actor * - {String} prefix: forwarding prefix used to redirect all packet to the right content process's transport * - {ChildDebuggerTransport} childTransport: Transport forwarding all packets to the target's content process * - {WatcherActor} watcher: The Watcher actor for which we instantiated this content process target actor */ const actors = new WeakMap(); // Save the list of all watcher actors that are watching for processes const watchers = new Set(); function onContentProcessActorCreated(msg) { const { watcherActorID, prefix, actor } = msg.data; const watcher = WatcherRegistry.getWatcher(watcherActorID); if (!watcher) { throw new Error( `Receiving a content process actor without a watcher actor ${watcherActorID}` ); } // Ignore watchers of other connections. // We may have two browser toolbox connected to the same process. // This will spawn two distinct Watcher actor and two distinct process target helper module. // Avoid processing the event many times, otherwise we will notify about the same target // multiple times. if (!watchers.has(watcher)) { return; } const messageManager = msg.target; const connection = watcher.conn; // Pipe Debugger message from/to parent/child via the message manager const childTransport = new ChildDebuggerTransport(messageManager, prefix); childTransport.hooks = { onPacket: connection.send.bind(connection), }; childTransport.ready(); connection.setForwarding(prefix, childTransport); const list = actors.get(messageManager) || []; list.push({ prefix, childTransport, actor, watcher, }); actors.set(messageManager, list); watcher.notifyTargetAvailable(actor); } function onContentProcessActorDestroyed(msg) { const { watcherActorID } = msg.data; const watcher = WatcherRegistry.getWatcher(watcherActorID); if (!watcher) { throw new Error( `Receiving a content process actor destruction without a watcher actor ${watcherActorID}` ); } // Ignore watchers of other connections. // We may have two browser toolbox connected to the same process. // This will spawn two distinct Watcher actor and two distinct process target helper module. // Avoid processing the event many times, otherwise we will notify about the same target // multiple times. if (!watchers.has(watcher)) { return; } const messageManager = msg.target; unregisterWatcherForMessageManager(watcher, messageManager); } function onMessageManagerClose(messageManager, topic, data) { const list = actors.get(messageManager); if (!list || !list.length) { return; } for (const { prefix, childTransport, actor, watcher } of list) { watcher.notifyTargetDestroyed(actor); // If we have a child transport, the actor has already // been created. We need to stop using this message manager. childTransport.close(); watcher.conn.cancelForwarding(prefix); } actors.delete(messageManager); } /** * Unregister everything created for a given watcher against a precise message manager: * - clear up things from `actors` WeakMap, * - notify all related target actors as being destroyed, * - close all DevTools Transports being created for each Message Manager. * * @param {WatcherActor} watcher * @param {MessageManager} * @param {object} options * @param {boolean} options.isModeSwitching * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref */ function unregisterWatcherForMessageManager(watcher, messageManager, options) { const targetActorDescriptions = actors.get(messageManager); if (!targetActorDescriptions || !targetActorDescriptions.length) { return; } // Destroy all transports related to this watcher and tells the client to purge all related actors const matchingTargetActorDescriptions = targetActorDescriptions.filter( item => item.watcher === watcher ); for (const { prefix, childTransport, actor, } of matchingTargetActorDescriptions) { watcher.notifyTargetDestroyed(actor, options); childTransport.close(); watcher.conn.cancelForwarding(prefix); } // Then update global `actors` WeakMap by stripping all data about this watcher const remainingTargetActorDescriptions = targetActorDescriptions.filter( item => item.watcher !== watcher ); if (!remainingTargetActorDescriptions.length) { actors.delete(messageManager); } else { actors.set(messageManager, remainingTargetActorDescriptions); } } /** * Destroy everything related to a given watcher that has been created in this module: * (See unregisterWatcherForMessageManager) * * @param {WatcherActor} watcher * @param {object} options * @param {boolean} options.isModeSwitching * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref */ function closeWatcherTransports(watcher, options) { for (let i = 0; i < Services.ppmm.childCount; i++) { const messageManager = Services.ppmm.getChildAt(i); unregisterWatcherForMessageManager(watcher, messageManager, options); } } function maybeRegisterMessageListeners(watcher) { const sizeBefore = watchers.size; watchers.add(watcher); if (sizeBefore == 0 && watchers.size == 1) { Services.ppmm.addMessageListener( "debug:content-process-actor", onContentProcessActorCreated ); Services.ppmm.addMessageListener( "debug:content-process-actor-destroyed", onContentProcessActorDestroyed ); Services.obs.addObserver(onMessageManagerClose, "message-manager-close"); // Load the content process server startup script only once, // otherwise it will be evaluated twice, listen to events twice and create // target actors twice. // We may try to load it twice when opening one Browser Toolbox via about:debugging // and another regular Browser Toolbox. Both will spawn a WatcherActor and watch for processes. const isContentProcessScripLoaded = Services.ppmm .getDelayedProcessScripts() .some(([uri]) => uri === CONTENT_PROCESS_SCRIPT); if (!isContentProcessScripLoaded) { Services.ppmm.loadProcessScript(CONTENT_PROCESS_SCRIPT, true); } } } /** * @param {WatcherActor} watcher * @param {object} options * @param {boolean} options.isModeSwitching * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref */ function maybeUnregisterMessageListeners(watcher, options = {}) { const sizeBefore = watchers.size; watchers.delete(watcher); closeWatcherTransports(watcher, options); if (sizeBefore == 1 && watchers.size == 0) { Services.ppmm.removeMessageListener( "debug:content-process-actor", onContentProcessActorCreated ); Services.ppmm.removeMessageListener( "debug:content-process-actor-destroyed", onContentProcessActorDestroyed ); Services.obs.removeObserver(onMessageManagerClose, "message-manager-close"); // We inconditionally remove the process script, while we should only remove it // once the last DevToolsServer stop watching for processes. // We might have many server, using distinct loaders, so that this module // will be spawn many times and we should remove the script only once the last // module unregister the last watcher of all. Services.ppmm.removeDelayedProcessScript(CONTENT_PROCESS_SCRIPT); Services.ppmm.broadcastAsyncMessage("debug:destroy-process-script", { options, }); } } async function createTargets(watcher) { // XXX: Should this move to WatcherRegistry?? maybeRegisterMessageListeners(watcher); // Bug 1648499: This could be simplified when migrating to JSProcessActor by using sendQuery. // For now, hack into WatcherActor in order to know when we created one target // actor for each existing content process. // Also, we substract one as the parent process has a message manager and is counted // in `childCount`, but we ignore it from the process script and it won't reply. let contentProcessCount = Services.ppmm.childCount - 1; if (contentProcessCount == 0) { return; } const onTargetsCreated = new Promise(resolve => { let receivedTargetCount = 0; const listener = () => { receivedTargetCount++; mayBeResolve(); }; watcher.on("target-available-form", listener); const onContentProcessClosed = () => { // Update the content process count as one has been just destroyed contentProcessCount--; mayBeResolve(); }; Services.obs.addObserver(onContentProcessClosed, "message-manager-close"); function mayBeResolve() { if (receivedTargetCount >= contentProcessCount) { watcher.off("target-available-form", listener); Services.obs.removeObserver( onContentProcessClosed, "message-manager-close" ); resolve(); } } }); Services.ppmm.broadcastAsyncMessage("debug:instantiate-already-available", { watcherActorID: watcher.actorID, connectionPrefix: watcher.conn.prefix, sessionData: watcher.sessionData, }); await onTargetsCreated; } /** * @param {WatcherActor} watcher * @param {object} options * @param {boolean} options.isModeSwitching * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref */ function destroyTargets(watcher, options) { maybeUnregisterMessageListeners(watcher, options); Services.ppmm.broadcastAsyncMessage("debug:destroy-target", { watcherActorID: watcher.actorID, }); } /** * Go over all existing content processes in order to communicate about new data entries * * @param {Object} options * @param {WatcherActor} options.watcher * The Watcher Actor providing new data entries * @param {string} options.type * The type of data to be added * @param {Array} options.entries * The values to be added to this type of data * @param String updateType * "add" will only add the new entries in the existing data set. * "set" will update the data set with the new entries. */ async function addOrSetSessionDataEntry({ watcher, type, entries, updateType, }) { let expectedCount = Services.ppmm.childCount - 1; if (expectedCount == 0) { return; } const onAllReplied = new Promise(resolve => { let count = 0; const listener = msg => { if (msg.data.watcherActorID != watcher.actorID) { return; } count++; maybeResolve(); }; Services.ppmm.addMessageListener( "debug:add-or-set-session-data-entry-done", listener ); const onContentProcessClosed = (messageManager, topic, data) => { expectedCount--; maybeResolve(); }; const maybeResolve = () => { if (count == expectedCount) { Services.ppmm.removeMessageListener( "debug:add-or-set-session-data-entry-done", listener ); Services.obs.removeObserver( onContentProcessClosed, "message-manager-close" ); resolve(); } }; Services.obs.addObserver(onContentProcessClosed, "message-manager-close"); }); Services.ppmm.broadcastAsyncMessage("debug:add-or-set-session-data-entry", { watcherActorID: watcher.actorID, type, entries, updateType, }); await onAllReplied; } /** * Notify all existing content processes that some data entries have been removed * * See addOrSetSessionDataEntry for argument documentation. */ function removeSessionDataEntry({ watcher, type, entries }) { Services.ppmm.broadcastAsyncMessage("debug:remove-session-data-entry", { watcherActorID: watcher.actorID, type, entries, }); } module.exports = { createTargets, destroyTargets, addOrSetSessionDataEntry, removeSessionDataEntry, };