/* 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} 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 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 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 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); }