diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs')
-rw-r--r-- | devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs b/devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs new file mode 100644 index 0000000000..e9b3a9d50d --- /dev/null +++ b/devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs @@ -0,0 +1,437 @@ +/* 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/. */ + +/** + * Helper module around `sharedData` object that helps storing the state + * of all observed Targets and Resources, that, for all DevTools connections. + * Here is a few words about the C++ implementation of sharedData: + * https://searchfox.org/mozilla-central/rev/bc3600def806859c31b2c7ac06e3d69271052a89/dom/ipc/SharedMap.h#30-55 + * + * We may have more than one DevToolsServer and one server may have more than one + * client. This module will be the single source of truth in the parent process, + * in order to know which targets/resources are currently observed. It will also + * be used to declare when something starts/stops being observed. + * + * `sharedData` is a platform API that helps sharing JS Objects across processes. + * We use it in order to communicate to the content process which targets and resources + * should be observed. Content processes read this data only once, as soon as they are created. + * It isn't used beyond this point. Content processes are not going to update it. + * We will notify about changes in observed targets and resources for already running + * processes by some other means. (Via JS Window Actor queries "DevTools:(un)watch(Resources|Target)") + * This means that only this module will update the "DevTools:watchedPerWatcher" value. + * From the parent process, we should be going through this module to fetch the data, + * while from the content process, we will read `sharedData` directly. + */ + +const { SessionDataHelpers } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", + { global: "contextual" } +); + +const { SUPPORTED_DATA } = SessionDataHelpers; +const SUPPORTED_DATA_TYPES = Object.values(SUPPORTED_DATA); + +// Define the Map that will be saved in `sharedData`. +// It is keyed by WatcherActor ID and values contains following attributes: +// - targets: Set of strings, refering to target types to be listened to +// - resources: Set of strings, refering to resource types to be observed +// - sessionContext Object, The Session Context to help know what is debugged. +// See devtools/server/actors/watcher/session-context.js +// - connectionPrefix: The DevToolsConnection prefix of the watcher actor. Used to compute new actor ID in the content processes. +// +// Unfortunately, `sharedData` is subject to race condition and may have side effect +// when read/written from multiple places in the same process, +// which is why this map should be considered as the single source of truth. +const sessionDataByWatcherActor = new Map(); + +// In parallel to the previous map, keep all the WatcherActor keyed by the same WatcherActor ID, +// the WatcherActor ID. We don't (and can't) propagate the WatcherActor instances to the content +// processes, but still would like to match them by their ID. +const watcherActors = new Map(); + +// Name of the attribute into which we save this Map in `sharedData` object. +const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; + +/** + * Use `sharedData` to allow processes, early during their creation, + * to know which resources should be listened to. This will be read + * from the Target actor, when it gets created early during process start, + * in order to start listening to the expected resource types. + */ +function persistMapToSharedData() { + Services.ppmm.sharedData.set(SHARED_DATA_KEY_NAME, sessionDataByWatcherActor); + // Request to immediately flush the data to the content processes in order to prevent + // races (bug 1644649). Otherwise content process may have outdated sharedData + // and try to create targets for Watcher actor that already stopped watching for targets. + Services.ppmm.sharedData.flush(); +} + +export const ParentProcessWatcherRegistry = { + /** + * Tells if a given watcher currently watches for a given target type. + * + * @param WatcherActor watcher + * The WatcherActor which should be listening. + * @param string targetType + * The new target type to query. + * @return boolean + * Returns true if already watching. + */ + isWatchingTargets(watcher, targetType) { + const sessionData = this.getSessionData(watcher); + return !!sessionData?.targets?.includes(targetType); + }, + + /** + * Retrieve the data saved into `sharedData` that is used to know + * about which type of targets and resources we care listening about. + * `sessionDataByWatcherActor` is saved into `sharedData` after each mutation, + * but `sessionDataByWatcherActor` is the source of truth. + * + * @param WatcherActor watcher + * The related WatcherActor which starts/stops observing. + * @param object options (optional) + * A dictionary object with `createData` boolean attribute. + * If this attribute is set to true, we create the data structure in the Map + * if none exists for this prefix. + */ + getSessionData(watcher, { createData = false } = {}) { + // Use WatcherActor ID as a key as we may have multiple clients willing to watch for targets. + // For example, a Browser Toolbox debugging everything and a Content Toolbox debugging + // just one tab. We might also have multiple watchers, on the same connection when using about:debugging. + const watcherActorID = watcher.actorID; + let sessionData = sessionDataByWatcherActor.get(watcherActorID); + if (!sessionData && createData) { + sessionData = { + // The "session context" object help understand what should be debugged and which target should be created. + // See WatcherActor constructor for more info. + sessionContext: watcher.sessionContext, + // The DevToolsServerConnection prefix will be used to compute actor IDs created in the content process + connectionPrefix: watcher.conn.prefix, + }; + sessionDataByWatcherActor.set(watcherActorID, sessionData); + watcherActors.set(watcherActorID, watcher); + } + return sessionData; + }, + + /** + * Given a Watcher Actor ID, return the related Watcher Actor instance. + * + * @param String actorID + * The Watcher Actor ID to search for. + * @return WatcherActor + * The Watcher Actor instance. + */ + getWatcher(actorID) { + return watcherActors.get(actorID); + }, + + /** + * Return an array of the watcher actors that match the passed browserId + * + * @param {Number} browserId + * @returns {Array<WatcherActor>} An array of the matching watcher actors + */ + getWatchersForBrowserId(browserId) { + const watchers = []; + for (const watcherActor of watcherActors.values()) { + if ( + watcherActor.sessionContext.type == "browser-element" && + watcherActor.sessionContext.browserId === browserId + ) { + watchers.push(watcherActor); + } + } + + return watchers; + }, + + /** + * Notify that a given watcher added or set some entries for given data type. + * + * @param WatcherActor watcher + * The WatcherActor which starts observing. + * @param string type + * The type of data to be added + * @param Array<Object> 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. + */ + addOrSetSessionDataEntry(watcher, type, entries, updateType) { + const sessionData = this.getSessionData(watcher, { + createData: true, + }); + + if (!SUPPORTED_DATA_TYPES.includes(type)) { + throw new Error(`Unsupported session data type: ${type}`); + } + + SessionDataHelpers.addOrSetSessionDataEntry( + sessionData, + type, + entries, + updateType + ); + + // Flush sharedData before registering the JS Actors as it is used + // during their instantiation. + persistMapToSharedData(); + + // Register the JS Window Actor the first time we start watching for something (e.g. resource, target, …). + if (watcher.sessionContext.type == "all") { + registerBrowserToolboxJSProcessActor(); + } else { + registerJSProcessActor(); + } + }, + + /** + * Notify that a given watcher removed an entry in a given data type. + * + * @param WatcherActor watcher + * The WatcherActor which stops observing. + * @param string type + * The type of data to be removed + * @param Array<Object> entries + * The values to be removed to this type of data + * @params {Object} options + * @params {Boolean} options.isModeSwitching: Set to true true when this is called as the + * result of a change to the devtools.browsertoolbox.scope pref. + * + * @return boolean + * True if we such entry was already registered, for this watcher actor. + */ + removeSessionDataEntry(watcher, type, entries, options) { + const sessionData = this.getSessionData(watcher); + if (!sessionData) { + return false; + } + + if (!SUPPORTED_DATA_TYPES.includes(type)) { + throw new Error(`Unsupported session data type: ${type}`); + } + + if ( + !SessionDataHelpers.removeSessionDataEntry(sessionData, type, entries) + ) { + return false; + } + + const isWatchingSomething = SUPPORTED_DATA_TYPES.some( + dataType => sessionData[dataType] && !!sessionData[dataType].length + ); + + // Remove the watcher reference if it's not watching for anything anymore, unless we're + // doing a mode switch; in such case we don't mean to end the DevTools session, so we + // still want to have access to the underlying data (furthermore, such case should only + // happen in tests, in a regular workflow we'd still be watching for resources). + if (!isWatchingSomething && !options?.isModeSwitching) { + sessionDataByWatcherActor.delete(watcher.actorID); + watcherActors.delete(watcher.actorID); + } + + persistMapToSharedData(); + + return true; + }, + + /** + * Cleanup everything about a given watcher actor. + * Remove it from any registry so that we stop interacting with it. + * + * The watcher would be automatically unregistered from removeWatcherEntry, + * if we remove all entries. But we aren't removing all breakpoints. + * So here, we force clearing any reference to the watcher actor when it destroys. + */ + unregisterWatcher(watcherActorID) { + sessionDataByWatcherActor.delete(watcherActorID); + watcherActors.delete(watcherActorID); + this.maybeUnregisterJSActors(); + }, + + /** + * Unregister the JS Actors if there is no more DevTools code observing any target/resource. + */ + maybeUnregisterJSActors() { + if (sessionDataByWatcherActor.size == 0) { + unregisterBrowserToolboxJSProcessActor(); + unregisterJSProcessActor(); + } + }, + + /** + * Notify that a given watcher starts observing a new target type. + * + * @param WatcherActor watcher + * The WatcherActor which starts observing. + * @param string targetType + * The new target type to start listening to. + */ + watchTargets(watcher, targetType) { + this.addOrSetSessionDataEntry( + watcher, + SUPPORTED_DATA.TARGETS, + [targetType], + "add" + ); + }, + + /** + * Notify that a given watcher stops observing a given target type. + * + * @param WatcherActor watcher + * The WatcherActor which stops observing. + * @param string targetType + * The new target type to stop listening to. + * @params {Object} options + * @params {Boolean} options.isModeSwitching: Set to true true when this is called as the + * result of a change to the devtools.browsertoolbox.scope pref. + * @return boolean + * True if we were watching for this target type, for this watcher actor. + */ + unwatchTargets(watcher, targetType, options) { + return this.removeSessionDataEntry( + watcher, + SUPPORTED_DATA.TARGETS, + [targetType], + options + ); + }, + + /** + * Notify that a given watcher starts observing new resource types. + * + * @param WatcherActor watcher + * The WatcherActor which starts observing. + * @param Array<string> resourceTypes + * The new resource types to start listening to. + */ + watchResources(watcher, resourceTypes) { + this.addOrSetSessionDataEntry( + watcher, + SUPPORTED_DATA.RESOURCES, + resourceTypes, + "add" + ); + }, + + /** + * Notify that a given watcher stops observing given resource types. + * + * See `watchResources` for argument definition. + * + * @return boolean + * True if we were watching for this resource type, for this watcher actor. + */ + unwatchResources(watcher, resourceTypes) { + return this.removeSessionDataEntry( + watcher, + SUPPORTED_DATA.RESOURCES, + resourceTypes + ); + }, +}; + +// Boolean flag to know if the DevToolsProcess JS Process Actor is currently registered +let isJSProcessActorRegistered = false; +let isBrowserToolboxJSProcessActorRegistered = false; + +const JSProcessActorConfig = { + parent: { + esModuleURI: + "resource://devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs", + }, + child: { + esModuleURI: + "resource://devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs", + // There is no good observer service notification we can listen to to instantiate the JSProcess Actor + // reliably as soon as the process start. + // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification... + observers: ["init-devtools-content-process-actor"], + }, + // The parent process is handled very differently from content processes + // This uses the ParentProcessTarget which inherits from BrowsingContextTarget + // and is, for now, manually created by the descriptor as the top level target. + includeParent: true, +}; + +const BrowserToolboxJSProcessActorConfig = { + ...JSProcessActorConfig, + + // This JS Process Actor is used to bootstrap DevTools code debugging the privileged code + // in content processes. The privileged code runs in the "shared JSM global" (See mozJSModuleLoader). + // DevTools modules should be loaded in a distinct global in order to be able to debug this privileged code. + // There is a strong requirement in spidermonkey for the debuggee and debugger to be using distinct compartments. + // This flag will force both parent and child modules to be loaded via a dedicated loader (See mozJSModuleLoader::GetOrCreateDevToolsLoader) + // + // Note that as a side effect, it makes these modules and all their dependencies to be invisible to the debugger. + loadInDevToolsLoader: true, +}; + +const PROCESS_SCRIPT_URL = + "resource://devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js"; + +function registerJSProcessActor() { + if (isJSProcessActorRegistered) { + return; + } + isJSProcessActorRegistered = true; + ChromeUtils.registerProcessActor("DevToolsProcess", JSProcessActorConfig); + + // There is no good observer service notification we can listen to to instantiate the JSProcess Actor + // as soon as the process start. + // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification... + Services.ppmm.loadProcessScript(PROCESS_SCRIPT_URL, true); +} + +function registerBrowserToolboxJSProcessActor() { + if (isBrowserToolboxJSProcessActorRegistered) { + return; + } + isBrowserToolboxJSProcessActorRegistered = true; + ChromeUtils.registerProcessActor( + "BrowserToolboxDevToolsProcess", + BrowserToolboxJSProcessActorConfig + ); + + // There is no good observer service notification we can listen to to instantiate the JSProcess Actor + // as soon as the process start. + // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification... + Services.ppmm.loadProcessScript(PROCESS_SCRIPT_URL, true); +} + +function unregisterJSProcessActor() { + if (!isJSProcessActorRegistered) { + return; + } + isJSProcessActorRegistered = false; + try { + ChromeUtils.unregisterProcessActor("DevToolsProcess"); + } catch (e) { + // If any pending query was still ongoing, this would throw + } + if (isBrowserToolboxJSProcessActorRegistered) { + return; + } + Services.ppmm.removeDelayedProcessScript(PROCESS_SCRIPT_URL); +} + +function unregisterBrowserToolboxJSProcessActor() { + if (!isBrowserToolboxJSProcessActorRegistered) { + return; + } + isBrowserToolboxJSProcessActorRegistered = false; + try { + ChromeUtils.unregisterProcessActor("BrowserToolboxDevToolsProcess"); + } catch (e) { + // If any pending query was still ongoing, this would throw + } + if (isJSProcessActorRegistered) { + return; + } + Services.ppmm.removeDelayedProcessScript(PROCESS_SCRIPT_URL); +} |