summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/watcher/target-helpers
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/server/actors/watcher/target-helpers
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/actors/watcher/target-helpers')
-rw-r--r--devtools/server/actors/watcher/target-helpers/frame-helper.js322
-rw-r--r--devtools/server/actors/watcher/target-helpers/moz.build11
-rw-r--r--devtools/server/actors/watcher/target-helpers/process-helper.js380
-rw-r--r--devtools/server/actors/watcher/target-helpers/worker-helper.js128
4 files changed, 841 insertions, 0 deletions
diff --git a/devtools/server/actors/watcher/target-helpers/frame-helper.js b/devtools/server/actors/watcher/target-helpers/frame-helper.js
new file mode 100644
index 0000000000..855a64ae5e
--- /dev/null
+++ b/devtools/server/actors/watcher/target-helpers/frame-helper.js
@@ -0,0 +1,322 @@
+/* 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,
+ }
+);
+const { WindowGlobalLogger } = ChromeUtils.importESModule(
+ "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs"
+);
+const Targets = require("resource://devtools/server/actors/targets/index.js");
+
+const browsingContextAttachedObserverByWatcher = new Map();
+
+/**
+ * Force creating targets for all existing BrowsingContext, that, for a given Watcher Actor.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to watch for new targets.
+ */
+async function createTargets(watcher) {
+ // Go over all existing BrowsingContext in order to:
+ // - Force the instantiation of a DevToolsFrameChild
+ // - Have the DevToolsFrameChild to spawn the WindowGlobalTargetActor
+
+ // If we have a browserElement, set the watchedByDevTools flag on its related browsing context
+ // TODO: We should also set the flag for the "parent process" browsing context when we're
+ // in the browser toolbox. This is blocked by Bug 1675763, and should be handled as part
+ // of Bug 1709529.
+ if (watcher.sessionContext.type == "browser-element") {
+ // The `watchedByDevTools` enables gecko behavior tied to this flag, such as:
+ // - reporting the contents of HTML loaded in the docshells
+ // - capturing stacks for the network monitor.
+ watcher.browserElement.browsingContext.watchedByDevTools = true;
+ }
+
+ if (!browsingContextAttachedObserverByWatcher.has(watcher)) {
+ // We store the browserId here as watcher.browserElement.browserId can momentary be
+ // set to 0 when there's a navigation to a new browsing context.
+ const browserId = watcher.sessionContext.browserId;
+ const onBrowsingContextAttached = browsingContext => {
+ // We want to set watchedByDevTools on new top-level browsing contexts:
+ // - in the case of the BrowserToolbox/BrowserConsole, that would be the browsing
+ // contexts of all the tabs we want to handle.
+ // - for the regular toolbox, browsing context that are being created when navigating
+ // to a page that forces a new browsing context.
+ // Then BrowsingContext will propagate to all the tree of children BrowsingContext's.
+ if (
+ !browsingContext.parent &&
+ (watcher.sessionContext.type != "browser-element" ||
+ browserId === browsingContext.browserId)
+ ) {
+ browsingContext.watchedByDevTools = true;
+ }
+ };
+ Services.obs.addObserver(
+ onBrowsingContextAttached,
+ "browsing-context-attached"
+ );
+ // We store the observer so we can retrieve it elsewhere (e.g. for removal in destroyTargets).
+ browsingContextAttachedObserverByWatcher.set(
+ watcher,
+ onBrowsingContextAttached
+ );
+ }
+
+ if (
+ watcher.sessionContext.isServerTargetSwitchingEnabled &&
+ watcher.sessionContext.type == "browser-element"
+ ) {
+ // If server side target switching is enabled, process the top level browsing context first,
+ // so that we guarantee it is notified to the client first.
+ // If it is disabled, the top level target will be created from the client instead.
+ await createTargetForBrowsingContext({
+ watcher,
+ browsingContext: watcher.browserElement.browsingContext,
+ retryOnAbortError: true,
+ });
+ }
+
+ const browsingContexts = watcher.getAllBrowsingContexts().filter(
+ // Filter out the top browsing context we just processed.
+ browsingContext =>
+ browsingContext != watcher.browserElement?.browsingContext
+ );
+ // Await for the all the queries in order to resolve only *after* we received all
+ // already available targets.
+ // i.e. each call to `createTargetForBrowsingContext` should end up emitting
+ // a target-available-form event via the WatcherActor.
+ await Promise.allSettled(
+ browsingContexts.map(browsingContext =>
+ createTargetForBrowsingContext({ watcher, browsingContext })
+ )
+ );
+}
+
+/**
+ * (internal helper method) Force creating the target actor for a given BrowsingContext.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to watch for new targets.
+ * @param BrowsingContext browsingContext
+ * The context for which a target should be created.
+ * @param Boolean retryOnAbortError
+ * Set to true to retry creating existing targets when receiving an AbortError.
+ * An AbortError is sent when the JSWindowActor pair was destroyed before the query
+ * was complete, which can happen if the document navigates while the query is pending.
+ */
+async function createTargetForBrowsingContext({
+ watcher,
+ browsingContext,
+ retryOnAbortError = false,
+}) {
+ logWindowGlobal(browsingContext.currentWindowGlobal, "Existing WindowGlobal");
+
+ // We need to set the watchedByDevTools flag on all top-level browsing context. In the
+ // case of a content toolbox, this is done in the tab descriptor, but when we're in the
+ // browser toolbox, such descriptor is not created.
+ // Then BrowsingContext will propagate to all the tree of children BbrowsingContext's.
+ if (!browsingContext.parent) {
+ browsingContext.watchedByDevTools = true;
+ }
+
+ try {
+ await browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .instantiateTarget({
+ watcherActorID: watcher.actorID,
+ connectionPrefix: watcher.conn.prefix,
+ sessionContext: watcher.sessionContext,
+ sessionData: watcher.sessionData,
+ });
+ } catch (e) {
+ console.warn(
+ "Failed to create DevTools Frame target for browsingContext",
+ browsingContext.id,
+ ": ",
+ e,
+ retryOnAbortError ? "retrying" : ""
+ );
+ if (retryOnAbortError && e.name === "AbortError") {
+ await createTargetForBrowsingContext({
+ watcher,
+ browsingContext,
+ retryOnAbortError,
+ });
+ } else {
+ throw e;
+ }
+ }
+}
+
+/**
+ * Force destroying all BrowsingContext targets which were related to a given watcher.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to stop watching for new targets.
+ * @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) {
+ // Go over all existing BrowsingContext in order to destroy all targets
+ const browsingContexts = watcher.getAllBrowsingContexts();
+
+ for (const browsingContext of browsingContexts) {
+ logWindowGlobal(
+ browsingContext.currentWindowGlobal,
+ "Existing WindowGlobal"
+ );
+
+ if (!browsingContext.parent) {
+ browsingContext.watchedByDevTools = false;
+ }
+
+ browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .destroyTarget({
+ watcherActorID: watcher.actorID,
+ sessionContext: watcher.sessionContext,
+ options,
+ });
+ }
+
+ if (watcher.sessionContext.type == "browser-element") {
+ watcher.browserElement.browsingContext.watchedByDevTools = false;
+ }
+
+ if (browsingContextAttachedObserverByWatcher.has(watcher)) {
+ Services.obs.removeObserver(
+ browsingContextAttachedObserverByWatcher.get(watcher),
+ "browsing-context-attached"
+ );
+ browsingContextAttachedObserverByWatcher.delete(watcher);
+ }
+}
+
+/**
+ * Go over all existing BrowsingContext in order to communicate about new data entries
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to stop watching for new targets.
+ * @param string type
+ * The type of data to be added
+ * @param Array<Object> entries
+ * The values to be added to this type of data
+ */
+async function addSessionDataEntry({ watcher, type, entries }) {
+ const browsingContexts = getWatchingBrowsingContexts(watcher);
+ const promises = [];
+ for (const browsingContext of browsingContexts) {
+ logWindowGlobal(
+ browsingContext.currentWindowGlobal,
+ "Existing WindowGlobal"
+ );
+
+ const promise = browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .addSessionDataEntry({
+ watcherActorID: watcher.actorID,
+ sessionContext: watcher.sessionContext,
+ type,
+ entries,
+ });
+ promises.push(promise);
+ }
+ // Await for the queries in order to try to resolve only *after* the remote code processed the new data
+ return Promise.all(promises);
+}
+
+/**
+ * Notify all existing frame targets that some data entries have been removed
+ *
+ * See addSessionDataEntry for argument documentation.
+ */
+function removeSessionDataEntry({ watcher, type, entries }) {
+ const browsingContexts = getWatchingBrowsingContexts(watcher);
+ for (const browsingContext of browsingContexts) {
+ logWindowGlobal(
+ browsingContext.currentWindowGlobal,
+ "Existing WindowGlobal"
+ );
+
+ browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .removeSessionDataEntry({
+ watcherActorID: watcher.actorID,
+ sessionContext: watcher.sessionContext,
+ type,
+ entries,
+ });
+ }
+}
+
+module.exports = {
+ createTargets,
+ destroyTargets,
+ addSessionDataEntry,
+ removeSessionDataEntry,
+};
+
+/**
+ * Return the list of BrowsingContexts which should be targeted in order to communicate
+ * updated session data.
+ *
+ * @param WatcherActor watcher
+ * The watcher actor will be used to know which target we debug
+ * and what BrowsingContext should be considered.
+ */
+function getWatchingBrowsingContexts(watcher) {
+ // If we are watching for additional frame targets, it means that the multiprocess or fission mode is enabled,
+ // either for a content toolbox or a BrowserToolbox via scope set to everything.
+ const watchingAdditionalTargets = WatcherRegistry.isWatchingTargets(
+ watcher,
+ Targets.TYPES.FRAME
+ );
+ if (watchingAdditionalTargets) {
+ return watcher.getAllBrowsingContexts();
+ }
+ // By default, when we are no longer watching for frame targets, we should no longer try to
+ // communicate with any browsing-context. But.
+ //
+ // For "browser-element" debugging, all targets are provided by watching by watching for frame targets.
+ // So, when we are no longer watching for frame, we don't expect to have any frame target to talk to.
+ // => we should no longer reach any browsing context.
+ //
+ // For "all" (=browser toolbox), there is only the special ParentProcessTargetActor we might want to return here.
+ // But this is actually handled by the WatcherActor which uses `WatcherActor.getTargetActorInParentProcess` to convey session data.
+ // => we should no longer reach any browsing context.
+ //
+ // For "webextension" debugging, there is the special WebExtensionTargetActor, which doesn't run in the parent process,
+ // so that we can't rely on the same code as the browser toolbox.
+ // => we should always reach out this particular browsing context.
+ if (watcher.sessionContext.type == "webextension") {
+ const browsingContext = BrowsingContext.get(
+ watcher.sessionContext.addonBrowsingContextID
+ );
+ // The add-on browsing context may be destroying, in which case we shouldn't try to communicate with it
+ if (browsingContext.currentWindowGlobal) {
+ return [browsingContext];
+ }
+ }
+ return [];
+}
+
+// Set to true to log info about about WindowGlobal's being watched.
+const DEBUG = false;
+
+function logWindowGlobal(windowGlobal, message) {
+ if (!DEBUG) {
+ return;
+ }
+
+ WindowGlobalLogger.logWindowGlobal(windowGlobal, message);
+}
diff --git a/devtools/server/actors/watcher/target-helpers/moz.build b/devtools/server/actors/watcher/target-helpers/moz.build
new file mode 100644
index 0000000000..d78a2a6dbe
--- /dev/null
+++ b/devtools/server/actors/watcher/target-helpers/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "frame-helper.js",
+ "process-helper.js",
+ "worker-helper.js",
+)
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..cef24c2f93
--- /dev/null
+++ b/devtools/server/actors/watcher/target-helpers/process-helper.js
@@ -0,0 +1,380 @@
+/* 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
+ */
+async function addSessionDataEntry({ watcher, type, entries }) {
+ 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-session-data-entry-done",
+ listener
+ );
+ const onContentProcessClosed = (messageManager, topic, data) => {
+ expectedCount--;
+ maybeResolve();
+ };
+ const maybeResolve = () => {
+ if (count == expectedCount) {
+ Services.ppmm.removeMessageListener(
+ "debug:add-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-session-data-entry", {
+ watcherActorID: watcher.actorID,
+ type,
+ entries,
+ });
+
+ await onAllReplied;
+}
+
+/**
+ * Notify all existing content processes that some data entries have been removed
+ *
+ * See addSessionDataEntry 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,
+ addSessionDataEntry,
+ removeSessionDataEntry,
+};
diff --git a/devtools/server/actors/watcher/target-helpers/worker-helper.js b/devtools/server/actors/watcher/target-helpers/worker-helper.js
new file mode 100644
index 0000000000..6fda83e6bb
--- /dev/null
+++ b/devtools/server/actors/watcher/target-helpers/worker-helper.js
@@ -0,0 +1,128 @@
+/* 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 DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME = "DevToolsWorker";
+
+/**
+ * Force creating targets for all existing workers for a given Watcher Actor.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to watch for new targets.
+ */
+async function createTargets(watcher) {
+ // Go over all existing BrowsingContext in order to:
+ // - Force the instantiation of a DevToolsWorkerChild
+ // - Have the DevToolsWorkerChild to spawn the WorkerTargetActors
+ const browsingContexts = watcher.getAllBrowsingContexts({
+ acceptSameProcessIframes: true,
+ forceAcceptTopLevelTarget: true,
+ });
+ const promises = [];
+ for (const browsingContext of browsingContexts) {
+ const promise = browsingContext.currentWindowGlobal
+ .getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME)
+ .instantiateWorkerTargets({
+ watcherActorID: watcher.actorID,
+ connectionPrefix: watcher.conn.prefix,
+ sessionContext: watcher.sessionContext,
+ sessionData: watcher.sessionData,
+ });
+ promises.push(promise);
+ }
+
+ // Await for the different queries in order to try to resolve only *after* we received
+ // the already available worker targets.
+ return Promise.all(promises);
+}
+
+/**
+ * Force destroying all worker targets which were related to a given watcher.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to stop watching for new targets.
+ */
+async function destroyTargets(watcher) {
+ // Go over all existing BrowsingContext in order to destroy all targets
+ const browsingContexts = watcher.getAllBrowsingContexts({
+ acceptSameProcessIframes: true,
+ forceAcceptTopLevelTarget: true,
+ });
+ for (const browsingContext of browsingContexts) {
+ let windowActor;
+ try {
+ windowActor = browsingContext.currentWindowGlobal.getActor(
+ DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME
+ );
+ } catch (e) {
+ continue;
+ }
+
+ windowActor.destroyWorkerTargets({
+ watcherActorID: watcher.actorID,
+ sessionContext: watcher.sessionContext,
+ });
+ }
+}
+
+/**
+ * Go over all existing BrowsingContext in order to communicate about new data entries
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to stop watching for new targets.
+ * @param string type
+ * The type of data to be added
+ * @param Array<Object> entries
+ * The values to be added to this type of data
+ */
+async function addSessionDataEntry({ watcher, type, entries }) {
+ const browsingContexts = watcher.getAllBrowsingContexts({
+ acceptSameProcessIframes: true,
+ forceAcceptTopLevelTarget: true,
+ });
+ const promises = [];
+ for (const browsingContext of browsingContexts) {
+ const promise = browsingContext.currentWindowGlobal
+ .getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME)
+ .addSessionDataEntry({
+ watcherActorID: watcher.actorID,
+ sessionContext: watcher.sessionContext,
+ type,
+ entries,
+ });
+ promises.push(promise);
+ }
+ // Await for the queries in order to try to resolve only *after* the remote code processed the new data
+ return Promise.all(promises);
+}
+
+/**
+ * Notify all existing frame targets that some data entries have been removed
+ *
+ * See addSessionDataEntry for argument documentation.
+ */
+function removeSessionDataEntry({ watcher, type, entries }) {
+ const browsingContexts = watcher.getAllBrowsingContexts({
+ acceptSameProcessIframes: true,
+ forceAcceptTopLevelTarget: true,
+ });
+ for (const browsingContext of browsingContexts) {
+ browsingContext.currentWindowGlobal
+ .getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME)
+ .removeSessionDataEntry({
+ watcherActorID: watcher.actorID,
+ sessionContext: watcher.sessionContext,
+ type,
+ entries,
+ });
+ }
+}
+
+module.exports = {
+ createTargets,
+ destroyTargets,
+ addSessionDataEntry,
+ removeSessionDataEntry,
+};