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/connectors/js-process-actor/DevToolsProcessChild.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/connectors/js-process-actor/DevToolsProcessChild.sys.mjs')
-rw-r--r-- | devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs | 614 |
1 files changed, 373 insertions, 241 deletions
diff --git a/devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs b/devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs index 9e8ad64eea..d98c416e34 100644 --- a/devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs +++ b/devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs @@ -3,260 +3,260 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; +import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters( lazy, { - releaseDistinctSystemPrincipalLoader: - "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", - useDistinctSystemPrincipalLoader: - "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", + ProcessTargetWatcher: + "resource://devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs", + SessionDataHelpers: + "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", + ServiceWorkerTargetWatcher: + "resource://devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs", + WorkerTargetWatcher: + "resource://devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs", + WindowGlobalTargetWatcher: + "resource://devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs", }, { global: "contextual" } ); -// Name of the attribute into which we save data in `sharedData` object. -const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; - -// If true, log info about DOMProcess's being created. -const DEBUG = false; - -/** - * Print information about operation being done against each content process. - * - * @param {nsIDOMProcessChild} domProcessChild - * The process for which we should log a message. - * @param {String} message - * Message to log. - */ -function logDOMProcess(domProcessChild, message) { - if (!DEBUG) { - return; - } - dump(" [pid:" + domProcessChild + "] " + message + "\n"); -} +// TargetActorRegistery has to be shared between all devtools instances +// and so is loaded into the shared global. +ChromeUtils.defineESModuleGetters( + lazy, + { + TargetActorRegistry: + "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs", + }, + { global: "shared" } +); export class DevToolsProcessChild extends JSProcessActorChild { constructor() { super(); - // The map is indexed by the Watcher Actor ID. - // The values are objects containing the following properties: - // - connection: the DevToolsServerConnection itself - // - actor: the ContentProcessTargetActor instance - this._connections = new Map(); - - this._onConnectionChange = this._onConnectionChange.bind(this); + // The EventEmitter interface is used for DevToolsTransport's packet-received event. EventEmitter.decorate(this); } + #watchers = { + // Keys are target types, which are defined in this CommonJS Module: + // https://searchfox.org/mozilla-central/rev/0e9ea50a999420d93df0e4e27094952af48dd3b8/devtools/server/actors/targets/index.js#7-14 + // We avoid loading it as this ESM should be lightweight and avoid spawning DevTools CommonJS Loader until + // whe know we have to instantiate a Target Actor. + frame: { + // Number of active watcher actors currently watching for the given target type + activeListener: 0, + + // Instance of a target watcher class whose task is to observe new target instances + get watcher() { + return lazy.WindowGlobalTargetWatcher; + }, + }, + + process: { + activeListener: 0, + get watcher() { + return lazy.ProcessTargetWatcher; + }, + }, + + worker: { + activeListener: 0, + get watcher() { + return lazy.WorkerTargetWatcher; + }, + }, + + service_worker: { + activeListener: 0, + get watcher() { + return lazy.ServiceWorkerTargetWatcher; + }, + }, + }; + + #initialized = false; + + /** + * Called when this JSProcess Actor instantiate either when we start observing for first target types, + * or when the process just started. + */ instantiate() { - const { sharedData } = Services.cpmm; - const watchedDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME); - if (!watchedDataByWatcherActor) { - throw new Error( - "Request to instantiate the target(s) for the process, but `sharedData` is empty about watched targets" - ); + if (this.#initialized) { + return; } - - // Create one Target actor for each prefix/client which listen to processes - for (const [watcherActorID, sessionData] of watchedDataByWatcherActor) { - const { connectionPrefix } = sessionData; - - if (sessionData.targets?.includes("process")) { - this._createTargetActor(watcherActorID, connectionPrefix, sessionData); - } + this.#initialized = true; + // Create and watch for future target actors for each watcher currently watching some target types + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) { + this.#watchInitialTargetsForWatcher(watcherDataObject); } } /** - * Instantiate a new ProcessTarget for the given connection. + * Instantiate and watch future target actors based on the already watched targets. * - * @param String watcherActorID - * The ID of the WatcherActor who requested to observe and create these target actors. - * @param String parentConnectionPrefix - * The prefix of the DevToolsServerConnection of the Watcher Actor. - * This is used to compute a unique ID for the target actor. - * @param Object sessionData - * All data managed by the Watcher Actor and WatcherRegistry.sys.mjs, containing - * target types, resources types to be listened as well as breakpoints and any - * other data meant to be shared across processes and threads. + * @param Object watcherDataObject + * See ContentProcessWatcherRegistry. */ - _createTargetActor(watcherActorID, parentConnectionPrefix, sessionData) { - // This method will be concurrently called from `observe()` and `DevToolsProcessParent:instantiate-already-available` - // When the JSprocessActor initializes itself and when the watcher want to force instantiating existing targets. - // Simply ignore the second call as there is nothing to return, neither to wait for as this method is synchronous. - if (this._connections.has(watcherActorID)) { - return; - } - - // Compute a unique prefix, just for this DOM Process, - // which will be used to create a JSWindowActorTransport pair between content and parent processes. - // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, - // but here, we can't have access to any DevTools connection as we are really early in the content process startup - // XXX: nsIDOMProcessChild's childID should be unique across processes, I think. So that should be safe? - // (this.manager == nsIDOMProcessChild interface) - // Ensure appending a final slash, otherwise the prefix may be the same between childID 1 and 10... - const forwardingPrefix = - parentConnectionPrefix + "contentProcess" + this.manager.childID + "/"; - - logDOMProcess( - this.manager, - "Instantiate ContentProcessTarget with prefix: " + forwardingPrefix - ); - - const { connection, targetActor } = this._createConnectionAndActor( - watcherActorID, - forwardingPrefix, - sessionData - ); - this._connections.set(watcherActorID, { - connection, - actor: 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 the order of messages is 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 ContentProcessTargetActor 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. - this.sendAsyncMessage("DevToolsProcessChild:connectFromContent", { - watcherActorID, - forwardingPrefix, - actor: targetActor.form(), - }); - - // Pass initialization data to the target actor - 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], - false, - "set" + #watchInitialTargetsForWatcher(watcherDataObject) { + const { sessionData, sessionContext } = watcherDataObject; + + // About WebExtension, see note in addOrSetSessionDataEntry. + // Their target actor aren't created by this class, but session data is still managed by it + // and we need to pass the initial session data coming to already instantiated target actor. + if (sessionContext.type == "webextension") { + const { watcherActorID } = watcherDataObject; + const connectionPrefix = watcherActorID.replace(/watcher\d+$/, ""); + const targetActors = lazy.TargetActorRegistry.getTargetActors( + sessionContext, + connectionPrefix ); + if (targetActors.length) { + // Pass initialization data to the target actor + 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; + } + targetActors[0].addOrSetSessionDataEntry(type, entries, false, "set"); + } + } } - } - _destroyTargetActor(watcherActorID, isModeSwitching) { - const connectionInfo = this._connections.get(watcherActorID); - // This connection has already been cleaned? - if (!connectionInfo) { - throw new Error( - `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}` - ); + // Ignore the call if the watched targets property isn't populated yet. + // This typically happens when instantiating the JS Process Actor on toolbox opening, + // where the actor is spawn early and a watchTarget message comes later with the `targets` array set. + if (!sessionData.targets) { + return; } - connectionInfo.connection.close({ isModeSwitching }); - this._connections.delete(watcherActorID); - if (this._connections.size == 0) { - this.didDestroy({ isModeSwitching }); + + for (const targetType of sessionData.targets) { + this.#watchNewTargetTypeForWatcher(watcherDataObject, targetType, true); } } - _createConnectionAndActor(watcherActorID, forwardingPrefix, sessionData) { - if (!this.loader) { - this.loader = lazy.useDistinctSystemPrincipalLoader(this); + /** + * Instantiate and watch future target actors based on the already watched targets. + * + * @param Object watcherDataObject + * See ContentProcessWatcherRegistry. + * @param String targetType + * New typeof target to start watching. + * @param Boolean isProcessActorStartup + * True when we are watching for targets during this JS Process actor instantiation. + * It shouldn't be the case on toolbox opening, but only when a new process starts. + * On toolbox opening, the Actor will receive an explicit watchTargets query. + */ + #watchNewTargetTypeForWatcher( + watcherDataObject, + targetType, + isProcessActorStartup + ) { + const { watchingTargetTypes } = watcherDataObject; + // Ensure creating and watching only once per target type and watcher actor. + if (watchingTargetTypes.includes(targetType)) { + return; } - const { DevToolsServer } = this.loader.require( - "devtools/server/devtools-server" + watchingTargetTypes.push(targetType); + + // Update sessionData as watched target types are a Session Data + // used later for example by worker target watcher + lazy.SessionDataHelpers.addOrSetSessionDataEntry( + watcherDataObject.sessionData, + "targets", + [targetType], + "add" ); - const { ContentProcessTargetActor } = this.loader.require( - "devtools/server/actors/targets/content-process" - ); - - DevToolsServer.init(); + this.#watchers[targetType].activeListener++; - // For browser content toolbox, we do need a regular root actor and all tab - // actors, but don't need all the "browser actors" that are only useful when - // debugging the parent process via the browser toolbox. - DevToolsServer.registerActors({ target: true }); - DevToolsServer.on("connectionchange", this._onConnectionChange); + // Start listening for platform events when we are observing this type for the first time + if (this.#watchers[targetType].activeListener === 1) { + this.#watchers[targetType].watcher.watch(); + } - const connection = DevToolsServer.connectToParentWindowActor( - this, - forwardingPrefix, - "DevToolsProcessChild:packet" + // And instantiate targets for the already existing instances + this.#watchers[targetType].watcher.createTargetsForWatcher( + watcherDataObject, + isProcessActorStartup ); - - // Create the actual target actor. - const targetActor = new ContentProcessTargetActor(connection, { - sessionContext: sessionData.sessionContext, - }); - // 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 form = targetActor.form(); - targetActor.once("destroyed", options => { - // This will destroy the content process one - this._destroyTargetActor(watcherActorID, options.isModeSwitching); - // And this will destroy the parent process one - try { - this.sendAsyncMessage("DevToolsProcessChild:destroy", { - actors: [ - { - watcherActorID, - 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; - } - } - }); - - return { connection, targetActor }; } /** - * Destroy the server once its last connection closes. Note that multiple - * frame scripts may be running in parallel and reuse the same server. + * Stop watching for all target types and destroy all existing targets actor + * related to a given watcher actor. + * + * @param {Object} watcherDataObject + * @param {String} targetType + * @param {Object} options */ - _onConnectionChange() { - if (this._destroyed) { + #unwatchTargetsForWatcher(watcherDataObject, targetType, options) { + const { watchingTargetTypes } = watcherDataObject; + const targetTypeIndex = watchingTargetTypes.indexOf(targetType); + // Ignore targetTypes which were not observed + if (targetTypeIndex === -1) { return; } - this._destroyed = true; + // Update to the new list of currently watched target types + watchingTargetTypes.splice(targetTypeIndex, 1); + + // Update sessionData as watched target types are a Session Data + // used later for example by worker target watcher + lazy.SessionDataHelpers.removeSessionDataEntry( + watcherDataObject.sessionData, + "targets", + [targetType] + ); - const { DevToolsServer } = this.loader.require( - "devtools/server/devtools-server" + this.#watchers[targetType].activeListener--; + + // Stop observing for platform events + if (this.#watchers[targetType].activeListener === 0) { + this.#watchers[targetType].watcher.unwatch(); + } + + // Destroy all targets which are still instantiated for this type + this.#watchers[targetType].watcher.destroyTargetsForWatcher( + watcherDataObject, + options ); - // Only destroy the server if there is no more connections to it. It may be - // used to debug another tab running in the same process. - if (DevToolsServer.hasConnection() || DevToolsServer.keepAlive) { - return; + // Unregister the watcher if we stopped watching for all target types + if (!watchingTargetTypes.length) { + ContentProcessWatcherRegistry.remove(watcherDataObject); } - DevToolsServer.off("connectionchange", this._onConnectionChange); - DevToolsServer.destroy(); + // If we removed the last watcher, clean the internal state of this class. + if (ContentProcessWatcherRegistry.isEmpty()) { + this.didDestroy(options); + } } /** - * Supported Queries + * Cleanup everything around a given watcher actor + * + * @param {Object} watcherDataObject */ + #destroyWatcher(watcherDataObject) { + const { watchingTargetTypes } = watcherDataObject; + // Clone the array as it will be modified during the loop execution + for (const targetType of [...watchingTargetTypes]) { + this.#unwatchTargetsForWatcher(watcherDataObject, targetType); + } + } + /** + * Used by DevTools Transport to send packets to the content process. + * + * @param {JSON} packet + * @param {String} prefix + */ sendPacket(packet, prefix) { this.sendAsyncMessage("DevToolsProcessChild:packet", { packet, prefix }); } @@ -276,23 +276,33 @@ export class DevToolsProcessChild extends JSProcessActorChild { } } + /** + * Called by the JSProcessActor API when the process process sent us a message. + */ receiveMessage(message) { switch (message.name) { - case "DevToolsProcessParent:instantiate-already-available": { - const { watcherActorID, connectionPrefix, sessionData } = message.data; - return this._createTargetActor( - watcherActorID, - connectionPrefix, - sessionData + case "DevToolsProcessParent:watchTargets": { + const { watcherActorID, targetType } = message.data; + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); + return this.#watchNewTargetTypeForWatcher( + watcherDataObject, + targetType ); } - case "DevToolsProcessParent:destroy": { - const { watcherActorID, isModeSwitching } = message.data; - return this._destroyTargetActor(watcherActorID, isModeSwitching); + case "DevToolsProcessParent:unwatchTargets": { + const { watcherActorID, targetType, options } = message.data; + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); + return this.#unwatchTargetsForWatcher( + watcherDataObject, + targetType, + options + ); } case "DevToolsProcessParent:addOrSetSessionDataEntry": { const { watcherActorID, type, entries, updateType } = message.data; - return this._addOrSetSessionDataEntry( + return this.#addOrSetSessionDataEntry( watcherActorID, type, entries, @@ -301,7 +311,20 @@ export class DevToolsProcessChild extends JSProcessActorChild { } case "DevToolsProcessParent:removeSessionDataEntry": { const { watcherActorID, type, entries } = message.data; - return this._removeSessionDataEntry(watcherActorID, type, entries); + return this.#removeSessionDataEntry(watcherActorID, type, entries); + } + case "DevToolsProcessParent:destroyWatcher": { + const { watcherActorID } = message.data; + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject( + watcherActorID, + true + ); + // The watcher may already be destroyed if the client unwatched for all target types. + if (watcherDataObject) { + return this.#destroyWatcher(watcherDataObject); + } + return null; } case "DevToolsProcessParent:packet": return this.emit("packet-received", message); @@ -312,51 +335,160 @@ export class DevToolsProcessChild extends JSProcessActorChild { } } - _getTargetActorForWatcherActorID(watcherActorID) { - const connectionInfo = this._connections.get(watcherActorID); - return connectionInfo?.actor; - } - - _addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) { - const targetActor = this._getTargetActorForWatcherActorID(watcherActorID); - if (!targetActor) { - throw new Error( - `No target actor for this Watcher Actor ID:"${watcherActorID}"` - ); - } - return targetActor.addOrSetSessionDataEntry( + /** + * The parent process requested that some session data have been added or set. + * + * @param {String} watcherActorID + * The Watcher Actor ID requesting to add new session data + * @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. + */ + async #addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) { + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); + + // Maintain the copy of `sessionData` so that it is up-to-date when + // a new worker target needs to be instantiated + const { sessionData } = watcherDataObject; + lazy.SessionDataHelpers.addOrSetSessionDataEntry( + sessionData, type, entries, - false, updateType ); + + // This type is really specific to Service Workers and doesn't need to be transferred to any target. + // We only need to instantiate and destroy the target actors based on this new host. + const { watchingTargetTypes } = watcherDataObject; + if (type == "browser-element-host") { + if (watchingTargetTypes.includes("service_worker")) { + this.#watchers.service_worker.watcher.updateBrowserElementHost( + watcherDataObject + ); + } + return; + } + + const promises = []; + for (const targetActor of watcherDataObject.actors) { + promises.push( + targetActor.addOrSetSessionDataEntry(type, entries, false, updateType) + ); + } + + // Very special codepath for Web Extensions. + // Their WebExtension Target Actor is still created manually by WebExtensionDescritpor.getTarget, + // via a message manager. That, instead of being instantiated via the WatcherActor.watchTargets and this JSProcess actor. + // The Watcher Actor will still instantiate a JS Actor for the WebExt DOM Content Process + // and send the addOrSetSessionDataEntry query. But as the target actor isn't managed by the JS Actor, + // we have to manually retrieve it via the TargetActorRegistry. + if (sessionData.sessionContext.type == "webextension") { + const connectionPrefix = watcherActorID.replace(/watcher\d+$/, ""); + const targetActors = lazy.TargetActorRegistry.getTargetActors( + sessionData.sessionContext, + connectionPrefix + ); + // We will have a single match only in the DOM Process where the add-on runs + if (targetActors.length) { + promises.push( + targetActors[0].addOrSetSessionDataEntry( + type, + entries, + false, + updateType + ) + ); + } + } + await Promise.all(promises); + + if (watchingTargetTypes.includes("worker")) { + await this.#watchers.worker.watcher.addOrSetSessionDataEntry( + watcherDataObject, + type, + entries, + updateType + ); + } + if (watchingTargetTypes.includes("service_worker")) { + await this.#watchers.service_worker.watcher.addOrSetSessionDataEntry( + watcherDataObject, + type, + entries, + updateType + ); + } } - _removeSessionDataEntry(watcherActorID, type, entries) { - const targetActor = this._getTargetActorForWatcherActorID(watcherActorID); - // By the time we are calling this, the target may already have been destroyed. - if (!targetActor) { - return null; + /** + * The parent process requested that some session data have been removed. + * + * @param {String} watcherActorID + * The Watcher Actor ID requesting to remove session data + * @param {String}} type + * The type of data to be removed + * @param {Array<Object>} entries + * The values to be removed to this type of data + */ + #removeSessionDataEntry(watcherActorID, type, entries) { + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID, true); + + // When we unwatch resources after targets during the devtools shutdown, + // the watcher will be removed on last target type unwatch. + if (!watcherDataObject) { + return; + } + + // Maintain the copy of `sessionData` so that it is up-to-date when + // a new worker target needs to be instantiated + lazy.SessionDataHelpers.removeSessionDataEntry( + watcherDataObject.sessionData, + type, + entries + ); + + for (const targetActor of watcherDataObject.actors) { + targetActor.removeSessionDataEntry(type, entries); } - return targetActor.removeSessionDataEntry(type, entries); } - observe(subject, topic) { + /** + * Observer service notification handler. + * + * @param {DOMWindow|Document} subject + * A window for *-document-global-created + * A document for *-page-{shown|hide} + * @param {String} topic + */ + observe = (subject, topic) => { if (topic === "init-devtools-content-process-actor") { // This is triggered by the process actor registration and some code in process-helper.js // which defines a unique topic to be observed this.instantiate(); } - } + }; - didDestroy(options) { - for (const { connection } of this._connections.values()) { - connection.close(options); - } - this._connections.clear(); - if (this.loader) { - lazy.releaseDistinctSystemPrincipalLoader(this); - this.loader = null; + /** + * Called by JS Process Actor API when the current process is destroyed, + * but also within this class when the last watcher stopped watching for targets. + */ + didDestroy() { + // Stop watching for all target types + for (const entry of Object.values(this.#watchers)) { + if (entry.activeListener > 0) { + entry.watcher.unwatch(); + entry.activeListener = 0; + } } + + ContentProcessWatcherRegistry.clear(); } } + +export class BrowserToolboxDevToolsProcessChild extends DevToolsProcessChild {} |