summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/watcher/target-helpers/process-helper.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/watcher/target-helpers/process-helper.js')
-rw-r--r--devtools/server/actors/watcher/target-helpers/process-helper.js389
1 files changed, 389 insertions, 0 deletions
diff --git a/devtools/server/actors/watcher/target-helpers/process-helper.js b/devtools/server/actors/watcher/target-helpers/process-helper.js
new file mode 100644
index 0000000000..8895d7ed66
--- /dev/null
+++ b/devtools/server/actors/watcher/target-helpers/process-helper.js
@@ -0,0 +1,389 @@
+/* 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<Object>} 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,
+};