From d8bbc7858622b6d9c278469aab701ca0b609cddf Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:35:49 +0200 Subject: Merging upstream version 126.0. Signed-off-by: Daniel Baumann --- .../ContentProcessWatcherRegistry.sys.mjs | 430 +++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs (limited to 'devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs') diff --git a/devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs b/devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs new file mode 100644 index 0000000000..41ce80c9fd --- /dev/null +++ b/devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs @@ -0,0 +1,430 @@ +/* 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/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters( + lazy, + { + releaseDistinctSystemPrincipalLoader: + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", + useDistinctSystemPrincipalLoader: + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", + loader: "resource://devtools/shared/loader/Loader.sys.mjs", + }, + { global: "contextual" } +); + +// Name of the attribute into which we save data in `sharedData` object. +const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; + +// Map(String => Object) +// Map storing the data objects for all currently active watcher actors. +// The data objects are defined by `createWatcherDataObject()`. +// The main attribute of interest is the `sessionData` one which is set alongside +// various other attributes necessary to maintain state per watcher in the content process. +// +// The Session Data object is maintained by ParentProcessWatcherRegistry, in the parent process +// and is fetched from the content process via `sharedData` API. +// It is then manually maintained via DevToolsProcess JS Actor queries. +let gAllWatcherData = null; + +export const ContentProcessWatcherRegistry = { + _getAllWatchersDataMap() { + if (gAllWatcherData) { + return gAllWatcherData; + } + const { sharedData } = Services.cpmm; + const sessionDataByWatcherActorID = sharedData.get(SHARED_DATA_KEY_NAME); + if (!sessionDataByWatcherActorID) { + throw new Error("Missing session data in `sharedData`"); + } + + // Initialize a distinct Map to replicate the one read from `sharedData`. + // This distinct Map will be updated via DevToolsProcess JS Actor queries. + // This helps better control the execution flow. + gAllWatcherData = new Map(); + + // The Browser Toolbox will load its server modules in a distinct global/compartment whose name is "DevTools global". + // (See https://searchfox.org/mozilla-central/rev/0e9ea50a999420d93df0e4e27094952af48dd3b8/js/xpconnect/loader/mozJSModuleLoader.cpp#699) + // It means that this class will be instantiated twice, one in each global (the shared one and the browser toolbox one). + // We then have to distinguish the two subset of watcher actors accordingly within `sharedMap`, + // as `sharedMap` will be shared between the two module instances. + // Session type "all" relates to the Browser Toolbox. + const isInBrowserToolboxLoader = + // eslint-disable-next-line mozilla/reject-globalThis-modification + Cu.getRealmLocation(globalThis) == "DevTools global"; + + for (const [watcherActorID, sessionData] of sessionDataByWatcherActorID) { + // Filter in/out the watchers based on the current module loader and the watcher session type. + const isBrowserToolboxWatcher = sessionData.sessionContext.type == "all"; + if ( + (isInBrowserToolboxLoader && !isBrowserToolboxWatcher) || + (!isInBrowserToolboxLoader && isBrowserToolboxWatcher) + ) { + continue; + } + + gAllWatcherData.set( + watcherActorID, + createWatcherDataObject(watcherActorID, sessionData) + ); + } + + return gAllWatcherData; + }, + + /** + * Get all data objects for all currently active watcher actors. + * If a specific target type is passed, this will only return objects of watcher actively watching for a given target type. + * + * @param {String} targetType + * Optional target type to filter only a subset of watchers. + * @return {Array|Iterator} + * List of data objects. (see createWatcherDataObject) + */ + getAllWatchersDataObjects(targetType) { + if (targetType) { + const list = []; + for (const watcherDataObject of this._getAllWatchersDataMap().values()) { + if (watcherDataObject.sessionData.targets?.includes(targetType)) { + list.push(watcherDataObject); + } + } + return list; + } + return this._getAllWatchersDataMap().values(); + }, + + /** + * Get the watcher data object for a given watcher actor. + * + * @param {String} watcherActorID + * @param {Boolean} onlyFromCache + * If set explicitly to true, will avoid falling back to shared data. + * This is typically useful on destructor/removing/cleanup to avoid creating unexpected data. + * It is also used to avoid the exception thrown when sharedData is cleared on toolbox destruction. + */ + getWatcherDataObject(watcherActorID, onlyFromCache = false) { + let data = + ContentProcessWatcherRegistry._getAllWatchersDataMap().get( + watcherActorID + ); + if (!data && !onlyFromCache) { + // When there is more than one DevTools opened, the DevToolsProcess JS Actor spawned by the first DevTools + // created a cached Map in `_getAllWatchersDataMap`. + // When opening a second DevTools, this cached Map may miss some new SessionData related to this new DevTools instance, + // and new Watcher Actor. + // When such scenario happens, fallback to `sharedData` which should hopefully be containing the latest DevTools instance SessionData. + // + // May be the watcher should trigger a very first JS Actor query before any others in order to transfer the base Session Data object? + const { sharedData } = Services.cpmm; + const sessionDataByWatcherActorID = sharedData.get(SHARED_DATA_KEY_NAME); + const sessionData = sessionDataByWatcherActorID.get(watcherActorID); + if (!sessionData) { + throw new Error("Unable to find data for watcher " + watcherActorID); + } + data = createWatcherDataObject(watcherActorID, sessionData); + gAllWatcherData.set(watcherActorID, data); + } + return data; + }, + + /** + * Instantiate a DevToolsServerConnection for a given Watcher. + * + * This function will be the one forcing to load the first DevTools CommonJS modules + * and spawning the DevTools Loader as well as the DevToolsServer. So better call it + * only once when it is strictly necessary. + * + * This connection will be the communication channel for RDP between this content process + * and the parent process, which will route RDP packets from/to the client by using + * a unique "forwarding prefix". + * + * @param {String} watcherActorID + * @param {Boolean} useDistinctLoader + * To be set to true when debugging a privileged context running the shared system principal global. + * This is a requirement for spidermonkey Debugger API used by the thread actor. + * @return {Object} + * Object with connection (DevToolsServerConnection) and loader (DevToolsLoader) attributes. + */ + getOrCreateConnectionForWatcher(watcherActorID, useDistinctLoader) { + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); + let { connection, loader } = watcherDataObject; + + if (connection) { + return { connection, loader }; + } + + const { sessionContext, forwardingPrefix } = watcherDataObject; + // For the browser toolbox, we need to use a distinct loader in order to debug privileged JS. + // The thread actor ultimately need to be in a distinct compartments from its debuggees. + loader = + useDistinctLoader || sessionContext.type == "all" + ? lazy.useDistinctSystemPrincipalLoader(watcherDataObject) + : lazy.loader; + watcherDataObject.loader = loader; + + // Note that this a key step in loading DevTools backend / modules. + const { DevToolsServer } = loader.require( + "resource://devtools/server/devtools-server.js" + ); + + DevToolsServer.init(); + + // Within the content process, we only need the target scoped actors. + // (inspector, console, storage,...) + DevToolsServer.registerActors({ target: true }); + + // Instantiate a DevToolsServerConnection which will pipe all its outgoing RDP packets + // up to the parent process manager via DevToolsProcess JS Actor messages. + connection = DevToolsServer.connectToParentWindowActor( + watcherDataObject.jsProcessActor, + forwardingPrefix, + "DevToolsProcessChild:packet" + ); + watcherDataObject.connection = connection; + + return { connection, loader }; + }, + + /** + * Method to be called each time a new target actor is instantiated. + * + * @param {Object} watcherDataObject + * @param {Actor} targetActor + * @param {Boolean} isDocumentCreation + */ + onNewTargetActor(watcherDataObject, targetActor, isDocumentCreation = false) { + // There is no root actor in content processes and so + // the target actor can't be managed by it, but we do have to manage + // the actor to have it working and be registered in the DevToolsServerConnection. + // We make it manage itself and become a top level actor. + targetActor.manage(targetActor); + + const { watcherActorID } = watcherDataObject; + targetActor.once("destroyed", options => { + // Maintain the registry and notify the parent process + ContentProcessWatcherRegistry.destroyTargetActor( + watcherDataObject, + targetActor, + options + ); + }); + + watcherDataObject.actors.push(targetActor); + + // Immediately queue a message for the parent process, + // in order to ensure that the JSWindowActorTransport is instantiated + // before any packet is sent from the content process. + // As messages are guaranteed to be delivered in the order they + // were queued, we don't have to wait for anything around this sendAsyncMessage call. + // In theory, the Target Actor may emit events in its constructor. + // If it does, such RDP packets may be lost. But in practice, no events + // are emitted during its construction. Instead the frontend will start + // the communication first. + const { forwardingPrefix } = watcherDataObject; + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:targetAvailable", + { + watcherActorID, + forwardingPrefix, + targetActorForm: targetActor.form(), + } + ); + + // Pass initialization data to the target actor + const { sessionData } = watcherDataObject; + for (const type in sessionData) { + // `sessionData` will also contain `browserId` as well as entries with empty arrays, + // which shouldn't be processed. + const entries = sessionData[type]; + if (!Array.isArray(entries) || !entries.length) { + continue; + } + targetActor.addOrSetSessionDataEntry( + type, + sessionData[type], + isDocumentCreation, + "set" + ); + } + }, + + /** + * Method to be called each time a target actor is meant to be destroyed. + * + * @param {Object} watcherDataObject + * @param {Actor} targetActor + * @param {object} options + * @param {boolean} options.isModeSwitching + * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref + */ + destroyTargetActor(watcherDataObject, targetActor, options) { + const idx = watcherDataObject.actors.indexOf(targetActor); + if (idx != -1) { + watcherDataObject.actors.splice(idx, 1); + } + const form = targetActor.form(); + targetActor.destroy(options); + + // And this will destroy the parent process one + try { + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:targetDestroyed", + { + actors: [ + { + watcherActorID: watcherDataObject.watcherActorID, + targetActorForm: form, + }, + ], + options, + } + ); + } catch (e) { + // Ignore exception when the JSProcessActorChild has already been destroyed. + // We often try to emit this message while the process is being destroyed, + // but sendAsyncMessage doesn't have time to complete and throws. + if ( + !e.message.includes("JSProcessActorChild cannot send at the moment") + ) { + throw e; + } + } + }, + + /** + * Method to know if a given Watcher Actor is still registered. + * + * @param {String} watcherActorID + * @return {Boolean} + */ + has(watcherActorID) { + return gAllWatcherData.has(watcherActorID); + }, + + /** + * Method to unregister a given Watcher Actor. + * + * @param {Object} watcherDataObject + */ + remove(watcherDataObject) { + // We do not need to destroy each actor individually as they + // are all registered in this DevToolsServerConnection, which will + // destroy all the registered actors. + if (watcherDataObject.connection) { + watcherDataObject.connection.close(); + } + // If we were using a distinct and dedicated loader, + // we have to manually release it. + if (watcherDataObject.loader && watcherDataObject.loader !== lazy.loader) { + lazy.releaseDistinctSystemPrincipalLoader(watcherDataObject); + } + + gAllWatcherData.delete(watcherDataObject.watcherActorID); + if (gAllWatcherData.size == 0) { + gAllWatcherData = null; + } + }, + + /** + * Method to know if there is no more Watcher registered. + * + * @return {Boolean} + */ + isEmpty() { + return !gAllWatcherData || gAllWatcherData.size == 0; + }, + + /** + * Method to unregister all the Watcher Actors + */ + clear() { + if (!gAllWatcherData) { + return; + } + // Query gAllWatcherData internal map directly as we don't want to re-create the map from sharedData + for (const watcherDataObject of gAllWatcherData.values()) { + ContentProcessWatcherRegistry.remove(watcherDataObject); + } + gAllWatcherData = null; + }, +}; + +function createWatcherDataObject(watcherActorID, sessionData) { + // The prefix of the DevToolsServerConnection of the Watcher Actor in the parent process. + // This is used to compute a unique ID for this process. + const parentConnectionPrefix = sessionData.connectionPrefix; + + // Compute a unique prefix, just for this DOM Process. + // (nsIDOMProcessChild's childID should be unique across processes) + // + // This prefix will be used to create a JSWindowActorTransport pair between content and parent processes. + // This is slightly hacky as we typically compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, + // but here, we can't have access to any DevTools connection as we could run really early in the content process startup. + // + // Ensure appending a final slash, otherwise the prefix may be the same between childID 1 and 10... + const forwardingPrefix = + parentConnectionPrefix + + "process" + + ChromeUtils.domProcessChild.childID + + "/"; + + // The browser toolbox uses a distinct JS Actor, loaded in the "devtools" ESM loader. + const jsActorName = + sessionData.sessionContext.type == "all" + ? "BrowserToolboxDevToolsProcess" + : "DevToolsProcess"; + const jsProcessActor = ChromeUtils.domProcessChild.getActor(jsActorName); + + return { + // {String} + // Actor ID for this watcher + watcherActorID, + + // {Array} + // List of currently watched target types for this watcher + watchingTargetTypes: [], + + // {DevtoolsServerConnection} + // Connection bridge made from this content process to the parent process. + connection: null, + + // {JSActor} + // Reference to the related DevToolsProcessChild instance. + jsProcessActor, + + // {Object} + // Watcher's sessionContext object, which help identify the browser toolbox usecase. + sessionContext: sessionData.sessionContext, + + // {Object} + // Watcher's sessionData object, which is initiated with `sharedData` version, + // but is later updated on each Session Data update (addOrSetSessionDataEntry/removeSessionDataEntry). + // `sharedData` isn't timely updated and can be out of date. + sessionData, + + // {String} + // Prefix used against all RDP packets to route them correctly from/to this content process + forwardingPrefix, + + // {Array} + // List of active WindowGlobal and ContentProcess target actor instances. + actors: [], + + // {Array} + // We store workers independently as we don't have access to the TargetActor instance (it is in the worker thread) + // and we need to keep reference to some other specifics + // - {WorkerDebugger} dbg + workers: [], + + // {Set>} + // A Set of arrays which will be populated with concurrent Session Data updates + // being done while a worker target is being instantiated. + // Each pending worker being initialized register a new dedicated array which will be removed + // from the Set once its initialization is over. + pendingWorkers: new Set(), + }; +} -- cgit v1.2.3