diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/server/actors/watcher/target-helpers | |
parent | Initial commit. (diff) | |
download | firefox-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')
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, +}; |