summaryrefslogtreecommitdiffstats
path: root/devtools/server/connectors
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
commitd8bbc7858622b6d9c278469aab701ca0b609cddf (patch)
treeeff41dc61d9f714852212739e6b3738b82a2af87 /devtools/server/connectors
parentReleasing progress-linux version 125.0.3-1~progress7.99u1. (diff)
downloadfirefox-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')
-rw-r--r--devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs430
-rw-r--r--devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs614
-rw-r--r--devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs272
-rw-r--r--devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js33
-rw-r--r--devtools/server/connectors/js-process-actor/moz.build6
-rw-r--r--devtools/server/connectors/js-process-actor/target-watchers/moz.build (renamed from devtools/server/connectors/process-actor/moz.build)6
-rw-r--r--devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs95
-rw-r--r--devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs51
-rw-r--r--devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs574
-rw-r--r--devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs457
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs710
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs277
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs571
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs294
-rw-r--r--devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs76
-rw-r--r--devtools/server/connectors/js-window-actor/moz.build13
-rw-r--r--devtools/server/connectors/moz.build2
-rw-r--r--devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs741
-rw-r--r--devtools/server/connectors/process-actor/DevToolsServiceWorkerParent.sys.mjs308
-rw-r--r--devtools/server/connectors/worker-connector.js6
20 files changed, 2198 insertions, 3338 deletions
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<String>}
+ // 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<Object>}
+ // List of active WindowGlobal and ContentProcess target actor instances.
+ actors: [],
+
+ // {Array<Object>}
+ // 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<Array<Object>>}
+ // 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(),
+ };
+}
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 {}
diff --git a/devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs b/devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs
index 28e11def68..303c85e68f 100644
--- a/devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs
+++ b/devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs
@@ -5,9 +5,9 @@
import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs";
import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
-const { WatcherRegistry } = ChromeUtils.importESModule(
- "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs",
- // WatcherRegistry needs to be a true singleton and loads ActorManagerParent
+const { ParentProcessWatcherRegistry } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs",
+ // ParentProcessWatcherRegistry needs to be a true singleton and loads ActorManagerParent
// which also has to be a true singleton.
{ global: "shared" }
);
@@ -24,8 +24,6 @@ export class DevToolsProcessParent extends JSProcessActorParent {
constructor() {
super();
- this._destroyed = false;
-
// Map of DevToolsServerConnection's used to forward the messages from/to
// the client. The connections run in the parent process, as this code. We
// may have more than one when there is more than one client debugging the
@@ -44,41 +42,38 @@ export class DevToolsProcessParent extends JSProcessActorParent {
// which can be considered as a kind of id. On top of this, parent process
// DevToolsServerConnections also have forwarding prefixes because they are
// responsible for forwarding messages to content process connections.
- this._connections = new Map();
- this._onConnectionClosed = this._onConnectionClosed.bind(this);
EventEmitter.decorate(this);
}
+ #destroyed = false;
+ #connections = new Map();
+
/**
- * Request the content process to create the ContentProcessTarget
+ * Request the content process to create all the targets currently watched
+ * and start observing for new ones to be created later.
*/
- instantiateTarget({
- watcherActorID,
- connectionPrefix,
- sessionContext,
- sessionData,
- }) {
- return this.sendQuery(
- "DevToolsProcessParent:instantiate-already-available",
- {
- watcherActorID,
- connectionPrefix,
- sessionContext,
- sessionData,
- }
- );
+ watchTargets({ watcherActorID, targetType }) {
+ return this.sendQuery("DevToolsProcessParent:watchTargets", {
+ watcherActorID,
+ targetType,
+ });
}
- destroyTarget({ watcherActorID, isModeSwitching }) {
- this.sendAsyncMessage("DevToolsProcessParent:destroy", {
+ /**
+ * Request the content process to stop observing for currently watched targets
+ * and destroy all the currently active ones.
+ */
+ unwatchTargets({ watcherActorID, targetType, options }) {
+ this.sendAsyncMessage("DevToolsProcessParent:unwatchTargets", {
watcherActorID,
- isModeSwitching,
+ targetType,
+ options,
});
}
/**
- * Communicate to the content process that some data have been added.
+ * Communicate to the content process that some data have been added or set.
*/
addOrSetSessionDataEntry({ watcherActorID, type, entries, updateType }) {
return this.sendQuery("DevToolsProcessParent:addOrSetSessionDataEntry", {
@@ -100,8 +95,17 @@ export class DevToolsProcessParent extends JSProcessActorParent {
});
}
- connectFromContent({ watcherActorID, forwardingPrefix, actor }) {
- const watcher = WatcherRegistry.getWatcher(watcherActorID);
+ destroyWatcher({ watcherActorID }) {
+ return this.sendAsyncMessage("DevToolsProcessParent:destroyWatcher", {
+ watcherActorID,
+ });
+ }
+
+ /**
+ * Called when the content process notified us about a new target actor
+ */
+ #onTargetAvailable({ watcherActorID, forwardingPrefix, targetActorForm }) {
+ const watcher = ParentProcessWatcherRegistry.getWatcher(watcherActorID);
if (!watcher) {
throw new Error(
@@ -110,45 +114,86 @@ export class DevToolsProcessParent extends JSProcessActorParent {
}
const connection = watcher.conn;
- connection.on("closed", this._onConnectionClosed);
+ // If this is the first target actor for this watcher,
+ // hook up the DevToolsServerConnection which will bridge
+ // communication between the parent process DevToolsServer
+ // and the content process.
+ if (!this.#connections.get(watcher.conn.prefix)) {
+ connection.on("closed", this.#onConnectionClosed);
- // Create a js-window-actor based transport.
- const transport = new lazy.JsWindowActorTransport(
- this,
- forwardingPrefix,
- "DevToolsProcessParent:packet"
- );
- transport.hooks = {
- onPacket: connection.send.bind(connection),
- onClosed() {},
- };
- transport.ready();
+ // Create a js-window-actor based transport.
+ const transport = new lazy.JsWindowActorTransport(
+ this,
+ forwardingPrefix,
+ "DevToolsProcessParent:packet"
+ );
+ transport.hooks = {
+ onPacket: connection.send.bind(connection),
+ onClosed() {},
+ };
+ transport.ready();
- connection.setForwarding(forwardingPrefix, transport);
+ connection.setForwarding(forwardingPrefix, transport);
- this._connections.set(watcher.conn.prefix, {
- watcher,
- connection,
- // This prefix is the prefix of the DevToolsServerConnection, running
- // in the content process, for which we should forward packets to, based on its prefix.
- // While `watcher.connection` is also a DevToolsServerConnection, but from this process,
- // the parent process. It is the one receiving Client packets and the one, from which
- // we should forward packets from.
- forwardingPrefix,
- transport,
- actor,
- });
+ this.#connections.set(watcher.conn.prefix, {
+ watcher,
+ connection,
+ // This prefix is the prefix of the DevToolsServerConnection, running
+ // in the content process, for which we should forward packets to, based on its prefix.
+ // While `watcher.connection` is also a DevToolsServerConnection, but from this process,
+ // the parent process. It is the one receiving Client packets and the one, from which
+ // we should forward packets from.
+ forwardingPrefix,
+ transport,
+ targetActorForms: [],
+ });
+ }
- watcher.notifyTargetAvailable(actor);
+ this.#connections
+ .get(watcher.conn.prefix)
+ .targetActorForms.push(targetActorForm);
+
+ watcher.notifyTargetAvailable(targetActorForm);
}
- _onConnectionClosed(status, prefix) {
- if (this._connections.has(prefix)) {
- const { connection } = this._connections.get(prefix);
- this._cleanupConnection(connection);
+ /**
+ * Called when the content process notified us about a target actor that has been destroyed.
+ */
+ #onTargetDestroyed({ actors, options }) {
+ for (const { watcherActorID, targetActorForm } of actors) {
+ const watcher = ParentProcessWatcherRegistry.getWatcher(watcherActorID);
+ // As we instruct to destroy all targets when the watcher is destroyed,
+ // we may easily receive the target destruction notification *after*
+ // the watcher has been removed from the registry.
+ if (!watcher || watcher.isDestroyed()) {
+ continue;
+ }
+ watcher.notifyTargetDestroyed(targetActorForm, options);
+ const connectionInfo = this.#connections.get(watcher.conn.prefix);
+ if (connectionInfo) {
+ const idx = connectionInfo.targetActorForms.findIndex(
+ form => form.actor == targetActorForm.actor
+ );
+ if (idx != -1) {
+ connectionInfo.targetActorForms.splice(idx, 1);
+ }
+ // Once the last active target is removed, disconnect the DevTools transport
+ // and cleanup everything bound to this DOM Process. We will re-instantiate
+ // a new connection/transport on the next reported target actor.
+ if (!connectionInfo.targetActorForms.length) {
+ this.#cleanupConnection(connectionInfo.connection);
+ }
+ }
}
}
+ #onConnectionClosed = (status, prefix) => {
+ if (this.#connections.has(prefix)) {
+ const { connection } = this.#connections.get(prefix);
+ this.#cleanupConnection(connection);
+ }
+ };
+
/**
* Close and unregister a given DevToolsServerConnection.
*
@@ -157,30 +202,27 @@ export class DevToolsProcessParent extends JSProcessActorParent {
* @param {boolean} options.isModeSwitching
* true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
*/
- async _cleanupConnection(connection, options = {}) {
- const connectionInfo = this._connections.get(connection.prefix);
- if (!connectionInfo) {
- return;
+ async #cleanupConnection(connection, options = {}) {
+ const watcherConnectionInfo = this.#connections.get(connection.prefix);
+ if (watcherConnectionInfo) {
+ const { forwardingPrefix, transport } = watcherConnectionInfo;
+ if (transport) {
+ // If we have a child transport, the actor has already
+ // been created. We need to stop using this transport.
+ transport.close(options);
+ }
+ // When cancelling the forwarding, one RDP event is sent to the client to purge all requests
+ // and actors related to a given prefix.
+ // Be careful that any late RDP event would be ignored by the client passed this call.
+ connection.cancelForwarding(forwardingPrefix);
}
- const { forwardingPrefix, transport } = connectionInfo;
- connection.off("closed", this._onConnectionClosed);
- if (transport) {
- // If we have a child transport, the actor has already
- // been created. We need to stop using this transport.
- transport.close(options);
- }
+ connection.off("closed", this.#onConnectionClosed);
- this._connections.delete(connection.prefix);
- if (!this._connections.size) {
- this._destroy(options);
+ this.#connections.delete(connection.prefix);
+ if (!this.#connections.size) {
+ this.#destroy(options);
}
-
- // When cancelling the forwarding, one RDP event is sent to the client to purge all requests
- // and actors related to a given prefix. Do this *after* calling _destroy which will emit
- // the target-destroyed RDP event. This helps the Watcher Front retrieve the related target front,
- // otherwise it would be too eagerly destroyed by the purge event.
- connection.cancelForwarding(forwardingPrefix);
}
/**
@@ -190,20 +232,26 @@ export class DevToolsProcessParent extends JSProcessActorParent {
* @param {boolean} options.isModeSwitching
* true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
*/
- _destroy(options) {
- if (this._destroyed) {
+ #destroy(options) {
+ if (this.#destroyed) {
return;
}
- this._destroyed = true;
+ this.#destroyed = true;
- for (const { actor, connection, watcher } of this._connections.values()) {
- watcher.notifyTargetDestroyed(actor, options);
- this._cleanupConnection(connection, options);
+ for (const {
+ targetActorForms,
+ connection,
+ watcher,
+ } of this.#connections.values()) {
+ for (const actor of targetActorForms) {
+ watcher.notifyTargetDestroyed(actor, options);
+ }
+ this.#cleanupConnection(connection, options);
}
}
/**
- * Supported Queries
+ * Used by DevTools Transport to send packets to the content process.
*/
sendPacket(packet, prefix) {
@@ -211,7 +259,7 @@ export class DevToolsProcessParent extends JSProcessActorParent {
}
/**
- * JsWindowActor API
+ * JsProcessActor API
*/
async sendQuery(msg, args) {
@@ -225,24 +273,43 @@ export class DevToolsProcessParent extends JSProcessActorParent {
}
}
+ /**
+ * Called by the JSProcessActor API when the content process sent us a message
+ */
receiveMessage(message) {
switch (message.name) {
- case "DevToolsProcessChild:connectFromContent":
- return this.connectFromContent(message.data);
+ case "DevToolsProcessChild:targetAvailable":
+ return this.#onTargetAvailable(message.data);
case "DevToolsProcessChild:packet":
return this.emit("packet-received", message);
- case "DevToolsProcessChild:destroy":
- for (const { form, watcherActorID } of message.data.actors) {
- const watcher = WatcherRegistry.getWatcher(watcherActorID);
- // As we instruct to destroy all targets when the watcher is destroyed,
- // we may easily receive the target destruction notification *after*
- // the watcher has been removed from the registry.
- if (watcher) {
- watcher.notifyTargetDestroyed(form, message.data.options);
- this._cleanupConnection(watcher.conn, message.data.options);
- }
+ case "DevToolsProcessChild:targetDestroyed":
+ return this.#onTargetDestroyed(message.data);
+ case "DevToolsProcessChild:bf-cache-navigation-pageshow": {
+ const browsingContext = BrowsingContext.get(
+ message.data.browsingContextId
+ );
+ for (const watcherActor of ParentProcessWatcherRegistry.getWatchersForBrowserId(
+ browsingContext.browserId
+ )) {
+ watcherActor.emit("bf-cache-navigation-pageshow", {
+ windowGlobal: browsingContext.currentWindowGlobal,
+ });
}
return null;
+ }
+ case "DevToolsProcessChild:bf-cache-navigation-pagehide": {
+ const browsingContext = BrowsingContext.get(
+ message.data.browsingContextId
+ );
+ for (const watcherActor of ParentProcessWatcherRegistry.getWatchersForBrowserId(
+ browsingContext.browserId
+ )) {
+ watcherActor.emit("bf-cache-navigation-pagehide", {
+ windowGlobal: browsingContext.currentWindowGlobal,
+ });
+ }
+ return null;
+ }
default:
throw new Error(
"Unsupported message in DevToolsProcessParent: " + message.name
@@ -250,7 +317,12 @@ export class DevToolsProcessParent extends JSProcessActorParent {
}
}
+ /**
+ * Called by the JSProcessActor API when this content process is destroyed.
+ */
didDestroy() {
- this._destroy();
+ this.#destroy();
}
}
+
+export class BrowserToolboxDevToolsProcessParent extends DevToolsProcessParent {}
diff --git a/devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js b/devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js
new file mode 100644
index 0000000000..fbc71e2d90
--- /dev/null
+++ b/devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js
@@ -0,0 +1,33 @@
+/* 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";
+
+/*
+ We want this to only startup the DevToolsProcess JS Actor on process start
+ and not when we only register the JS Process Actor when watching the first target type.
+ The Watcher Actor will query each individual JS Process Actor and fine control
+ the ordering of requests. It is especially important to spawn the top level target first.
+*/
+const isContentProcessStartup = !Services.ww
+ .getWindowEnumerator()
+ .hasMoreElements();
+if (isContentProcessStartup) {
+ /*
+ We can't spawn the JSProcessActor right away and have to spin the event loop.
+ Otherwise it isn't registered yet and isn't listening to observer service.
+ Could it be the reason why JSProcessActor aren't spawn via process actor option's child.observers notifications ??
+ */
+ Services.tm.dispatchToMainThread(() => {
+ /*
+ This notification is registered in DevToolsServiceWorker JS process actor's options's `observers` attribute
+ and will force the JS Process actor to be instantiated in all processes.
+ */
+ Services.obs.notifyObservers(null, "init-devtools-content-process-actor");
+ /*
+ Instead of using observer service, we could also manually call some method of the actor:
+ ChromeUtils.domProcessChild.getActor("DevToolsProcess").observe(null, "init-devtools-content-process-actor");
+ */
+ });
+}
diff --git a/devtools/server/connectors/js-process-actor/moz.build b/devtools/server/connectors/js-process-actor/moz.build
index e1a1f5dc9d..c1843b4e16 100644
--- a/devtools/server/connectors/js-process-actor/moz.build
+++ b/devtools/server/connectors/js-process-actor/moz.build
@@ -4,7 +4,13 @@
# 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/.
+DIRS += [
+ "target-watchers",
+]
+
DevToolsModules(
+ "content-process-jsprocessactor-startup.js",
+ "ContentProcessWatcherRegistry.sys.mjs",
"DevToolsProcessChild.sys.mjs",
"DevToolsProcessParent.sys.mjs",
)
diff --git a/devtools/server/connectors/process-actor/moz.build b/devtools/server/connectors/js-process-actor/target-watchers/moz.build
index 63f768bd3c..0574b0399e 100644
--- a/devtools/server/connectors/process-actor/moz.build
+++ b/devtools/server/connectors/js-process-actor/target-watchers/moz.build
@@ -5,6 +5,8 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
- "DevToolsServiceWorkerChild.sys.mjs",
- "DevToolsServiceWorkerParent.sys.mjs",
+ "process.sys.mjs",
+ "service_worker.sys.mjs",
+ "window-global.sys.mjs",
+ "worker.sys.mjs",
)
diff --git a/devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs b/devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs
new file mode 100644
index 0000000000..c2b6dd807c
--- /dev/null
+++ b/devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs
@@ -0,0 +1,95 @@
+/* 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/. */
+
+import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs";
+
+function watch() {
+ // There is nothing to watch. This JS Process Actor will automatically be spawned
+ // for each new DOM Process.
+}
+function unwatch() {}
+
+function createTargetsForWatcher(watcherDataObject) {
+ // Always ignore the parent process. A special WindowGlobal target actor will be spawned.
+ if (ChromeUtils.domProcessChild.childID == 0) {
+ return;
+ }
+
+ createContentProcessTargetActor(watcherDataObject);
+}
+
+/**
+ * Instantiate a content process target actor for the current process
+ * and for a given watcher actor.
+ *
+ * @param {Object} watcherDataObject
+ */
+function createContentProcessTargetActor(watcherDataObject) {
+ logDOMProcess(
+ ChromeUtils.domProcessChild,
+ "Instantiate ContentProcessTarget"
+ );
+
+ const { connection, loader } =
+ ContentProcessWatcherRegistry.getOrCreateConnectionForWatcher(
+ watcherDataObject.watcherActorID
+ );
+
+ const { ContentProcessTargetActor } = loader.require(
+ "devtools/server/actors/targets/content-process"
+ );
+
+ // Create the actual target actor.
+ const targetActor = new ContentProcessTargetActor(connection, {
+ sessionContext: watcherDataObject.sessionContext,
+ });
+
+ ContentProcessWatcherRegistry.onNewTargetActor(
+ watcherDataObject,
+ targetActor
+ );
+}
+
+function destroyTargetsForWatcher(watcherDataObject, options) {
+ // Unregister and destroy the existing target actors for this target type
+ const actorsToDestroy = watcherDataObject.actors.filter(
+ actor => actor.targetType == "process"
+ );
+ watcherDataObject.actors = watcherDataObject.actors.filter(
+ actor => actor.targetType != "process"
+ );
+
+ for (const actor of actorsToDestroy) {
+ ContentProcessWatcherRegistry.destroyTargetActor(
+ watcherDataObject,
+ actor,
+ options
+ );
+ }
+}
+
+// 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");
+}
+
+export const ProcessTargetWatcher = {
+ watch,
+ unwatch,
+ createTargetsForWatcher,
+ destroyTargetsForWatcher,
+};
diff --git a/devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs b/devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs
new file mode 100644
index 0000000000..f2f307f297
--- /dev/null
+++ b/devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs
@@ -0,0 +1,51 @@
+/* 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/. */
+
+import { WorkerTargetWatcherClass } from "resource://devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "wdm",
+ "@mozilla.org/dom/workers/workerdebuggermanager;1",
+ "nsIWorkerDebuggerManager"
+);
+
+class ServiceWorkerTargetWatcherClass extends WorkerTargetWatcherClass {
+ constructor() {
+ super("service_worker");
+ }
+
+ /**
+ * Called whenever the debugged browser element navigates to a new page
+ * and the URL's host changes.
+ * This is used to maintain the list of active Service Worker targets
+ * based on that host name.
+ *
+ * @param {Object} watcherDataObject
+ * See ContentProcessWatcherRegistry
+ */
+ async updateBrowserElementHost(watcherDataObject) {
+ const { sessionData } = watcherDataObject;
+
+ // Create target actor matching this new host.
+ // Note that we may be navigating to the same host name and the target will already exist.
+ const promises = [];
+ for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) {
+ const alreadyCreated = watcherDataObject.workers.some(
+ info => info.dbg === dbg
+ );
+ if (
+ this.shouldHandleWorker(sessionData, dbg, "service_worker") &&
+ !alreadyCreated
+ ) {
+ promises.push(this.createWorkerTargetActor(watcherDataObject, dbg));
+ }
+ }
+ await Promise.all(promises);
+ }
+}
+
+export const ServiceWorkerTargetWatcher = new ServiceWorkerTargetWatcherClass();
diff --git a/devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs b/devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs
new file mode 100644
index 0000000000..66c71cbc1e
--- /dev/null
+++ b/devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs
@@ -0,0 +1,574 @@
+/* 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/. */
+
+import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(
+ lazy,
+ {
+ isWindowGlobalPartOfContext:
+ "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
+ WindowGlobalLogger:
+ "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs",
+ },
+ { global: "contextual" }
+);
+
+// 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" }
+);
+
+const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
+ "devtools.every-frame-target.enabled",
+ false
+);
+
+// If true, log info about DOMProcess's being created.
+const DEBUG = false;
+
+/**
+ * Print information about operation being done against each Window Global.
+ *
+ * @param {WindowGlobalChild} windowGlobal
+ * The window global for which we should log a message.
+ * @param {String} message
+ * Message to log.
+ */
+function logWindowGlobal(windowGlobal, message) {
+ if (!DEBUG) {
+ return;
+ }
+ lazy.WindowGlobalLogger.logWindowGlobal(windowGlobal, message);
+}
+
+function watch() {
+ // Set the following preference in this function, so that we can easily
+ // toggle these preferences on and off from tests and have the new value being picked up.
+
+ // bfcache-in-parent changes significantly how navigation behaves.
+ // We may start reusing previously existing WindowGlobal and so reuse
+ // previous set of JSWindowActor pairs (i.e. DevToolsProcessParent/DevToolsProcessChild).
+ // When enabled, regular navigations may also change and spawn new BrowsingContexts.
+ // If the page we navigate from supports being stored in bfcache,
+ // the navigation will use a new BrowsingContext. And so force spawning
+ // a new top-level target.
+ ChromeUtils.defineLazyGetter(
+ lazy,
+ "isBfcacheInParentEnabled",
+ () =>
+ Services.appinfo.sessionHistoryInParent &&
+ Services.prefs.getBoolPref("fission.bfcacheInParent", false)
+ );
+
+ // Observe for all necessary event to track new and destroyed WindowGlobals.
+ Services.obs.addObserver(observe, "content-document-global-created");
+ Services.obs.addObserver(observe, "chrome-document-global-created");
+ Services.obs.addObserver(observe, "content-page-shown");
+ Services.obs.addObserver(observe, "chrome-page-shown");
+ Services.obs.addObserver(observe, "content-page-hidden");
+ Services.obs.addObserver(observe, "chrome-page-hidden");
+ Services.obs.addObserver(observe, "inner-window-destroyed");
+ Services.obs.addObserver(observe, "initial-document-element-inserted");
+}
+
+function unwatch() {
+ // Observe for all necessary event to track new and destroyed WindowGlobals.
+ Services.obs.removeObserver(observe, "content-document-global-created");
+ Services.obs.removeObserver(observe, "chrome-document-global-created");
+ Services.obs.removeObserver(observe, "content-page-shown");
+ Services.obs.removeObserver(observe, "chrome-page-shown");
+ Services.obs.removeObserver(observe, "content-page-hidden");
+ Services.obs.removeObserver(observe, "chrome-page-hidden");
+ Services.obs.removeObserver(observe, "inner-window-destroyed");
+ Services.obs.removeObserver(observe, "initial-document-element-inserted");
+}
+
+function createTargetsForWatcher(watcherDataObject, isProcessActorStartup) {
+ const { sessionContext } = watcherDataObject;
+ // Bug 1785266 - For now, in browser, when debugging the parent process (childID == 0),
+ // we spawn only the ParentProcessTargetActor, which will debug all the BrowsingContext running in the process.
+ // So that we have to avoid instantiating any here.
+ if (
+ sessionContext.type == "all" &&
+ ChromeUtils.domProcessChild.childID === 0
+ ) {
+ return;
+ }
+
+ function lookupForTargets(window) {
+ // Do not only track top level BrowsingContext in this content process,
+ // but also any nested iframe which may be running in the same process.
+ for (const browsingContext of window.docShell.browsingContext.getAllBrowsingContextsInSubtree()) {
+ const { currentWindowContext } = browsingContext;
+ // Only consider Window Global which are running in this process
+ if (!currentWindowContext || !currentWindowContext.isInProcess) {
+ continue;
+ }
+
+ // WindowContext's windowGlobalChild should be defined for WindowGlobal running in this process
+ const { windowGlobalChild } = currentWindowContext;
+
+ // getWindowEnumerator will expose somewhat unexpected WindowGlobal when a tab navigated.
+ // This will expose WindowGlobals of past navigations. Document which are in the bfcache
+ // and aren't the current WindowGlobal of their BrowsingContext.
+ if (!windowGlobalChild.isCurrentGlobal) {
+ continue;
+ }
+
+ // Accept the initial about:blank document:
+ // - only from createTargetsForWatcher, when instantiating the target for the already existing WindowGlobals,
+ // - when we do that on toolbox opening, to prevent creating one when the process is starting.
+ //
+ // This is to allow debugging blank tabs, which are on an initial about:blank document.
+ //
+ // We want to avoid creating transient targets for initial about blank when a new WindowGlobal
+ // just get created as it will most likely navigate away just after and confuse the frontend with short lived target.
+ const acceptInitialDocument = !isProcessActorStartup;
+
+ if (
+ lazy.isWindowGlobalPartOfContext(windowGlobalChild, sessionContext, {
+ acceptInitialDocument,
+ })
+ ) {
+ createWindowGlobalTargetActor(watcherDataObject, windowGlobalChild);
+ } else if (
+ !browsingContext.parent &&
+ sessionContext.browserId &&
+ browsingContext.browserId == sessionContext.browserId &&
+ browsingContext.window.document.isInitialDocument
+ ) {
+ // In order to succesfully get the devtools-html-content event in SourcesManager,
+ // we have to ensure flagging the initial about:blank document...
+ // While we don't create a target for it, we need to set this flag for this event to be emitted.
+ browsingContext.watchedByDevTools = true;
+ }
+ }
+ }
+ for (const window of Services.ww.getWindowEnumerator()) {
+ lookupForTargets(window);
+
+ // `lookupForTargets` uses `getAllBrowsingContextsInSubTree`, but this will ignore browser elements
+ // using type="content". So manually retrieve the windows for these browser elements,
+ // in case we have tabs opened on document loaded in the same process.
+ // This codepath is meant when we are in the parent process, with browser.xhtml having these <browser type="content">
+ // elements for tabs.
+ for (const browser of window.document.querySelectorAll(
+ `browser[type="content"]`
+ )) {
+ const childWindow = browser.browsingContext.window;
+ // If the tab isn't on a document loaded in the parent process,
+ // the window will be null.
+ if (childWindow) {
+ lookupForTargets(childWindow);
+ }
+ }
+ }
+}
+
+function destroyTargetsForWatcher(watcherDataObject, options) {
+ // Unregister and destroy the existing target actors for this target type
+ const actorsToDestroy = watcherDataObject.actors.filter(
+ actor => actor.targetType == "frame"
+ );
+ watcherDataObject.actors = watcherDataObject.actors.filter(
+ actor => actor.targetType != "frame"
+ );
+
+ for (const actor of actorsToDestroy) {
+ ContentProcessWatcherRegistry.destroyTargetActor(
+ watcherDataObject,
+ actor,
+ options
+ );
+ }
+}
+
+/**
+ * Called whenever a new WindowGlobal is instantiated either:
+ * - when navigating to a new page (DOMWindowCreated)
+ * - by a bfcache navigation (pageshow)
+ *
+ * @param {Window} window
+ * @param {Object} options
+ * @param {Boolean} options.isBFCache
+ * True, if the request to instantiate a new target comes from a bfcache navigation.
+ * i.e. when we receive a pageshow event with persisted=true.
+ * This will be true regardless of bfcacheInParent being enabled or disabled.
+ * @param {Boolean} options.ignoreIfExisting
+ * By default to false. If true is passed, we avoid instantiating a target actor
+ * if one already exists for this windowGlobal.
+ */
+function onWindowGlobalCreated(
+ window,
+ { isBFCache = false, ignoreIfExisting = false } = {}
+) {
+ try {
+ const windowGlobal = window.windowGlobalChild;
+
+ // For bfcache navigations, we only create new targets when bfcacheInParent is enabled,
+ // as this would be the only case where new DocShells will be created. This requires us to spawn a
+ // new WindowGlobalTargetActor as such actor is bound to a unique DocShell.
+ const forceAcceptTopLevelTarget =
+ isBFCache && lazy.isBfcacheInParentEnabled;
+
+ for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects(
+ "frame"
+ )) {
+ const { sessionContext } = watcherDataObject;
+ if (
+ lazy.isWindowGlobalPartOfContext(windowGlobal, sessionContext, {
+ forceAcceptTopLevelTarget,
+ })
+ ) {
+ // If this was triggered because of a navigation, we want to retrieve the existing
+ // target we were debugging so we can destroy it before creating the new target.
+ // This is important because we had cases where the destruction of an old target
+ // was unsetting a flag on the **new** target document, breaking the toolbox (See Bug 1721398).
+
+ // We're checking for an existing target given a watcherActorID + browserId + browsingContext.
+ // Note that a target switch might create a new browsing context, so we wouldn't
+ // retrieve the existing target here. We are okay with this as:
+ // - this shouldn't happen much
+ // - in such case we weren't seeing the issue of Bug 1721398 (the old target can't access the new document)
+ const existingTarget = findTargetActor({
+ watcherDataObject,
+ innerWindowId: windowGlobal.innerWindowId,
+ });
+
+ // See comment in `observe()` method and `DOMDocElementInserted` condition to know why we sometime
+ // ignore this method call if a target actor already exists.
+ // It means that we got a previous DOMWindowCreated event, related to a non-about:blank document,
+ // and we should ignore the DOMDocElementInserted.
+ // In any other scenario, destroy the already existing target and re-create a new one.
+ if (existingTarget && ignoreIfExisting) {
+ continue;
+ }
+
+ // Bail if there is already an existing WindowGlobalTargetActor which wasn't
+ // created from a JSWIndowActor.
+ // This means we are reloading or navigating (same-process) a Target
+ // which has not been created using the Watcher, but from the client (most likely
+ // the initial target of a local-tab toolbox).
+ // However, we force overriding the first message manager based target in case of
+ // BFCache navigations.
+ if (
+ existingTarget &&
+ !existingTarget.createdFromJsWindowActor &&
+ !isBFCache
+ ) {
+ continue;
+ }
+
+ // If we decide to instantiate a new target and there was one before,
+ // first destroy the previous one.
+ // Otherwise its destroy sequence will be executed *after* the new one
+ // is being initialized and may easily revert changes made against platform API.
+ // (typically toggle platform boolean attributes back to default…)
+ if (existingTarget) {
+ existingTarget.destroy({ isTargetSwitching: true });
+ }
+
+ // When navigating to another process, the Watcher Actor won't have sent any query
+ // to the new process JS Actor as the debugged tab was on another process before navigation.
+ // But `sharedData` will have data about all the current watchers.
+ // Here we have to ensure calling watchTargetsForWatcher in order to populate #connections
+ // for the currently processed watcher actor and start listening for future targets.
+ if (
+ !ContentProcessWatcherRegistry.has(watcherDataObject.watcherActorID)
+ ) {
+ throw new Error("Watcher data seems out of sync");
+ }
+
+ createWindowGlobalTargetActor(watcherDataObject, windowGlobal, true);
+ }
+ }
+ } catch (e) {
+ // Ensure logging exception as they are silently ignore otherwise
+ dump(
+ " Exception while observing a new window: " + e + "\n" + e.stack + "\n"
+ );
+ }
+}
+
+/**
+ * Called whenever a WindowGlobal just got destroyed, when closing the tab, or navigating to another one.
+ *
+ * @param {innerWindowId} innerWindowId
+ * The WindowGlobal's unique identifier.
+ */
+function onWindowGlobalDestroyed(innerWindowId) {
+ for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects(
+ "frame"
+ )) {
+ const existingTarget = findTargetActor({
+ watcherDataObject,
+ innerWindowId,
+ });
+
+ if (!existingTarget) {
+ continue;
+ }
+
+ // Do not do anything if both bfcache in parent and server targets are disabled
+ // As history navigations will be handled within the same DocShell and by the
+ // same WindowGlobalTargetActor. The actor will listen to pageshow/pagehide by itself.
+ // We should not destroy any target.
+ if (
+ !lazy.isBfcacheInParentEnabled &&
+ !watcherDataObject.sessionContext.isServerTargetSwitchingEnabled
+ ) {
+ continue;
+ }
+ // If the target actor isn't in watcher data object, it is a top level actor
+ // instantiated via a Descriptor's getTarget method. It isn't registered into Watcher objects.
+ // But we still want to destroy such target actor, and need to manually emit the targetDestroyed to the parent process.
+ // Hopefully bug 1754452 should allow us to get rid of this workaround by making the top level actor
+ // be created and managed by the watcher universe, like all the others.
+ const isTopLevelActorRegisteredOutsideOfWatcherActor =
+ !watcherDataObject.actors.find(
+ actor => actor.innerWindowId == innerWindowId
+ );
+ const targetActorForm = isTopLevelActorRegisteredOutsideOfWatcherActor
+ ? existingTarget.form()
+ : null;
+
+ existingTarget.destroy();
+
+ if (isTopLevelActorRegisteredOutsideOfWatcherActor) {
+ watcherDataObject.jsProcessActor.sendAsyncMessage(
+ "DevToolsProcessChild:targetDestroyed",
+ {
+ actors: [
+ {
+ watcherActorID: watcherDataObject.watcherActorID,
+ targetActorForm,
+ },
+ ],
+ options: {},
+ }
+ );
+ }
+ }
+}
+
+/**
+ * Instantiate a WindowGlobal target actor for a given browsing context
+ * and for a given watcher actor.
+ *
+ * @param {Object} watcherDataObject
+ * @param {BrowsingContext} windowGlobalChild
+ * @param {Boolean} isDocumentCreation
+ */
+function createWindowGlobalTargetActor(
+ watcherDataObject,
+ windowGlobalChild,
+ isDocumentCreation = false
+) {
+ logWindowGlobal(windowGlobalChild, "Instantiate WindowGlobalTarget");
+
+ // When debugging privileged pages running a the shared system compartment, and we aren't in the browser toolbox (which already uses a distinct loader),
+ // we have to use the distinct loader in order to ensure running DevTools in a distinct compartment than the page we are about to debug
+ // Such page could be about:addons, chrome://browser/content/browser.xhtml,...
+ const { browsingContext } = windowGlobalChild;
+ const useDistinctLoader =
+ browsingContext.associatedWindow.document.nodePrincipal.isSystemPrincipal;
+ const { connection, loader } =
+ ContentProcessWatcherRegistry.getOrCreateConnectionForWatcher(
+ watcherDataObject.watcherActorID,
+ useDistinctLoader
+ );
+
+ const { WindowGlobalTargetActor } = loader.require(
+ "devtools/server/actors/targets/window-global"
+ );
+
+ // In the case of the browser toolbox, tab's BrowsingContext don't have
+ // any parent BC and shouldn't be considered as top-level.
+ // This is why we check for browserId's.
+ const { sessionContext } = watcherDataObject;
+ const isTopLevelTarget =
+ !browsingContext.parent &&
+ browsingContext.browserId == sessionContext.browserId;
+
+ // Create the actual target actor.
+ const targetActor = new WindowGlobalTargetActor(connection, {
+ docShell: browsingContext.docShell,
+ // Targets created from the server side, via Watcher actor and DevToolsProcess JSWindow
+ // actor pairs are following WindowGlobal lifecycle. i.e. will be destroyed on any
+ // type of navigation/reload.
+ followWindowGlobalLifeCycle: true,
+ isTopLevelTarget,
+ ignoreSubFrames: isEveryFrameTargetEnabled,
+ sessionContext,
+ });
+ targetActor.createdFromJsWindowActor = true;
+
+ ContentProcessWatcherRegistry.onNewTargetActor(
+ watcherDataObject,
+ targetActor,
+ isDocumentCreation
+ );
+}
+
+/**
+ * Observer service notification handler.
+ *
+ * @param {DOMWindow|Document} subject
+ * A window for *-document-global-created
+ * A document for *-page-{shown|hide}
+ * @param {String} topic
+ */
+function observe(subject, topic) {
+ if (
+ topic == "content-document-global-created" ||
+ topic == "chrome-document-global-created"
+ ) {
+ onWindowGlobalCreated(subject);
+ } else if (topic == "inner-window-destroyed") {
+ const innerWindowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ onWindowGlobalDestroyed(innerWindowId);
+ } else if (topic == "content-page-shown" || topic == "chrome-page-shown") {
+ // The observer service notification doesn't receive the "persisted" DOM Event attribute,
+ // but thanksfully is fired just before the dispatching of that DOM event.
+ subject.defaultView.addEventListener("pageshow", handleEvent, {
+ capture: true,
+ once: true,
+ });
+ } else if (topic == "content-page-hidden" || topic == "chrome-page-hidden") {
+ // Same as previous elseif branch
+ subject.defaultView.addEventListener("pagehide", handleEvent, {
+ capture: true,
+ once: true,
+ });
+ } else if (topic == "initial-document-element-inserted") {
+ // We may be notified about SVG documents which we don't care about here.
+ if (!subject.location || !subject.defaultView) {
+ return;
+ }
+ // We might have ignored the DOMWindowCreated event because it was the initial about:blank document.
+ // But when loading same-process iframes, we reuse the WindowGlobal of the previously ignored about:bank document
+ // to load the actual URL loaded in the iframe. This means we won't have a new DOMWindowCreated
+ // for the actual document. But there is a DOMDocElementInserted fired just after, that we are processing here
+ // to create a target for same-process iframes. We only have to tell onWindowGlobalCreated to ignore
+ // the call if a target was created on the DOMWindowCreated event (if that was a non-about:blank document).
+ //
+ // All this means that we still do not create any target for the initial documents.
+ // It is complex to instantiate targets for initial documents because:
+ // - it would mean spawning two targets for the same WindowGlobal and sharing the same innerWindowId
+ // - or have WindowGlobalTargets to handle more than one document (it would mean reusing will-navigate/window-ready events
+ // both on client and server)
+ onWindowGlobalCreated(subject.defaultView, { ignoreIfExisting: true });
+ }
+}
+
+/**
+ * DOM Event handler.
+ *
+ * @param {String} type
+ * DOM event name
+ * @param {Boolean} persisted
+ * A flag set to true in cache of BFCache navigation
+ * @param {Document} target
+ * The navigating document
+ */
+function handleEvent({ type, persisted, target }) {
+ // If persisted=true, this is a BFCache navigation.
+ //
+ // With Fission enabled and bfcacheInParent, BFCache navigations will spawn a new DocShell
+ // in the same process:
+ // * the previous page won't be destroyed, its JSWindowActor will stay alive (`didDestroy` won't be called)
+ // and a 'pagehide' with persisted=true will be emitted on it.
+ // * the new page page won't emit any DOMWindowCreated, but instead a pageshow with persisted=true
+ // will be emitted.
+ if (type === "pageshow" && persisted) {
+ // Notify all bfcache navigations, even the one for which we don't create a new target
+ // as that's being useful for parent process storage resource watchers.
+ for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) {
+ watcherDataObject.jsProcessActor.sendAsyncMessage(
+ "DevToolsProcessChild:bf-cache-navigation-pageshow",
+ {
+ browsingContextId: target.defaultView.browsingContext.id,
+ }
+ );
+ }
+
+ // Here we are going to re-instantiate a target that got destroyed before while processing a pagehide event.
+ // We force instantiating a new top level target, within `instantiate()` even if server targets are disabled.
+ // But we only do that if bfcacheInParent is enabled. Otherwise for same-process, same-docshell bfcache navigation,
+ // we don't want to spawn new targets.
+ onWindowGlobalCreated(target.defaultView, {
+ isBFCache: true,
+ });
+ }
+
+ if (type === "pagehide" && persisted) {
+ // Notify all bfcache navigations, even the one for which we don't create a new target
+ // as that's being useful for parent process storage resource watchers.
+ for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) {
+ watcherDataObject.jsProcessActor.sendAsyncMessage(
+ "DevToolsProcessChild:bf-cache-navigation-pagehide",
+ {
+ browsingContextId: target.defaultView.browsingContext.id,
+ }
+ );
+ }
+
+ // We might navigate away for the first top level target,
+ // which isn't using JSWindowActor (it still uses messages manager and is created by the client, via TabDescriptor.getTarget).
+ // We have to unregister it from the TargetActorRegistry, otherwise,
+ // if we navigate back to it, the next DOMWindowCreated won't create a new target for it.
+ onWindowGlobalDestroyed(target.defaultView.windowGlobalChild.innerWindowId);
+ }
+}
+
+/**
+ * Return an existing Window Global target for given a WatcherActor
+ * and against a given WindowGlobal.
+ *
+ * @param {Object} options
+ * @param {String} options.watcherDataObject
+ * @param {Number} options.innerWindowId
+ * The WindowGlobal inner window ID.
+ *
+ * @returns {WindowGlobalTargetActor|null}
+ */
+function findTargetActor({ watcherDataObject, innerWindowId }) {
+ // First let's check if a target was created for this watcher actor in this specific
+ // DevToolsProcessChild instance.
+ const targetActor = watcherDataObject.actors.find(
+ actor => actor.innerWindowId == innerWindowId
+ );
+ if (targetActor) {
+ return targetActor;
+ }
+
+ // Ensure retrieving the one target actor related to this connection.
+ // This allows to distinguish actors created for various toolboxes.
+ // For ex, regular toolbox versus browser console versus browser toolbox
+ const connectionPrefix = watcherDataObject.watcherActorID.replace(
+ /watcher\d+$/,
+ ""
+ );
+ const targetActors = lazy.TargetActorRegistry.getTargetActors(
+ watcherDataObject.sessionContext,
+ connectionPrefix
+ );
+
+ return targetActors.find(actor => actor.innerWindowId == innerWindowId);
+}
+
+export const WindowGlobalTargetWatcher = {
+ watch,
+ unwatch,
+ createTargetsForWatcher,
+ destroyTargetsForWatcher,
+};
diff --git a/devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs b/devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs
new file mode 100644
index 0000000000..0b67e8b038
--- /dev/null
+++ b/devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs
@@ -0,0 +1,457 @@
+/* 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/. */
+
+import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "wdm",
+ "@mozilla.org/dom/workers/workerdebuggermanager;1",
+ "nsIWorkerDebuggerManager"
+);
+
+const { TYPE_DEDICATED, TYPE_SERVICE, TYPE_SHARED } = Ci.nsIWorkerDebugger;
+
+export class WorkerTargetWatcherClass {
+ constructor(workerTargetType = "worker") {
+ this.#workerTargetType = workerTargetType;
+ this.#workerDebuggerListener = {
+ onRegister: this.#onWorkerRegister.bind(this),
+ onUnregister: this.#onWorkerUnregister.bind(this),
+ };
+ }
+
+ // {String}
+ #workerTargetType;
+ // {nsIWorkerDebuggerListener}
+ #workerDebuggerListener;
+
+ watch() {
+ lazy.wdm.addListener(this.#workerDebuggerListener);
+ }
+
+ unwatch() {
+ lazy.wdm.removeListener(this.#workerDebuggerListener);
+ }
+
+ createTargetsForWatcher(watcherDataObject) {
+ const { sessionData } = watcherDataObject;
+ for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) {
+ if (!this.shouldHandleWorker(sessionData, dbg, this.#workerTargetType)) {
+ continue;
+ }
+ this.createWorkerTargetActor(watcherDataObject, dbg);
+ }
+ }
+
+ async addOrSetSessionDataEntry(watcherDataObject, type, entries, updateType) {
+ // Collect the SessionData update into `pendingWorkers` in order to notify
+ // about the updates to workers which are still in process of being hooked by devtools.
+ for (const concurrentSessionUpdates of watcherDataObject.pendingWorkers) {
+ concurrentSessionUpdates.push({
+ type,
+ entries,
+ updateType,
+ });
+ }
+
+ const promises = [];
+ for (const {
+ dbg,
+ workerThreadServerForwardingPrefix,
+ } of watcherDataObject.workers) {
+ promises.push(
+ addOrSetSessionDataEntryInWorkerTarget({
+ dbg,
+ workerThreadServerForwardingPrefix,
+ type,
+ entries,
+ updateType,
+ })
+ );
+ }
+ await Promise.all(promises);
+ }
+
+ /**
+ * Called whenever a new Worker is instantiated in the current process
+ *
+ * @param {WorkerDebugger} dbg
+ */
+ #onWorkerRegister(dbg) {
+ // Create a Target Actor for each watcher currently watching for Workers
+ for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects(
+ this.#workerTargetType
+ )) {
+ const { sessionData } = watcherDataObject;
+ if (this.shouldHandleWorker(sessionData, dbg, this.#workerTargetType)) {
+ this.createWorkerTargetActor(watcherDataObject, dbg);
+ }
+ }
+ }
+
+ /**
+ * Called whenever a Worker is destroyed in the current process
+ *
+ * @param {WorkerDebugger} dbg
+ */
+ #onWorkerUnregister(dbg) {
+ for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects(
+ this.#workerTargetType
+ )) {
+ const { watcherActorID, workers } = watcherDataObject;
+ // Check if the worker registration was handled for this watcherActorID.
+ const unregisteredActorIndex = workers.findIndex(worker => {
+ try {
+ // Accessing the WorkerDebugger id might throw (NS_ERROR_UNEXPECTED).
+ return worker.dbg.id === dbg.id;
+ } catch (e) {
+ return false;
+ }
+ });
+ if (unregisteredActorIndex === -1) {
+ continue;
+ }
+
+ const { workerTargetForm, transport } = workers[unregisteredActorIndex];
+ // Close the transport made to the worker thread
+ transport.close();
+
+ try {
+ watcherDataObject.jsProcessActor.sendAsyncMessage(
+ "DevToolsProcessChild:targetDestroyed",
+ {
+ actors: [
+ {
+ watcherActorID,
+ targetActorForm: workerTargetForm,
+ },
+ ],
+ options: {},
+ }
+ );
+ } catch (e) {
+ // This often throws as the JSActor is being destroyed when DevTools closes
+ // and we are trying to notify about the destroyed targets.
+ }
+
+ workers.splice(unregisteredActorIndex, 1);
+ }
+ }
+
+ /**
+ * Instantiate a worker target actor related to a given WorkerDebugger object
+ * and for a given watcher actor.
+ *
+ * @param {Object} watcherDataObject
+ * @param {WorkerDebugger} dbg
+ */
+ async createWorkerTargetActor(watcherDataObject, dbg) {
+ // Prevent the debuggee from executing in this worker until the client has
+ // finished attaching to it. This call will throw if the debugger is already "registered"
+ // (i.e. if this is called outside of the register listener)
+ // See https://searchfox.org/mozilla-central/rev/84922363f4014eae684aabc4f1d06380066494c5/dom/workers/nsIWorkerDebugger.idl#55-66
+ try {
+ dbg.setDebuggerReady(false);
+ } catch (e) {
+ if (!e.message.startsWith("Component returned failure code")) {
+ throw e;
+ }
+ }
+
+ const { watcherActorID } = watcherDataObject;
+ const { connection, loader } =
+ ContentProcessWatcherRegistry.getOrCreateConnectionForWatcher(
+ watcherActorID
+ );
+
+ // Compute a unique prefix for the bridge made between this content process main thread
+ // and the worker thread.
+ const workerThreadServerForwardingPrefix =
+ connection.allocID("workerTarget");
+
+ const { connectToWorker } = loader.require(
+ "resource://devtools/server/connectors/worker-connector.js"
+ );
+
+ // Create the actual worker target actor, in the worker thread.
+ const { sessionData, sessionContext } = watcherDataObject;
+ const onConnectToWorker = connectToWorker(
+ connection,
+ dbg,
+ workerThreadServerForwardingPrefix,
+ {
+ sessionData,
+ sessionContext,
+ }
+ );
+
+ // Only add data to the connection if we successfully send the
+ // workerTargetAvailable message.
+ const workerInfo = {
+ dbg,
+ workerThreadServerForwardingPrefix,
+ };
+ watcherDataObject.workers.push(workerInfo);
+
+ // The onConnectToWorker is async and we may receive new Session Data (e.g breakpoints)
+ // while we are instantiating the worker targets.
+ // Let cache the pending session data and flush it after the targets are being instantiated.
+ const concurrentSessionUpdates = [];
+ watcherDataObject.pendingWorkers.add(concurrentSessionUpdates);
+
+ try {
+ await onConnectToWorker;
+ } catch (e) {
+ // connectToWorker is supposed to call setDebuggerReady(true) to release the worker execution.
+ // But if anything goes wrong and an exception is thrown, ensure releasing its execution,
+ // otherwise if devtools is broken, it will freeze the worker indefinitely.
+ //
+ // onConnectToWorker can reject if the Worker Debugger is closed; so we only want to
+ // resume the debugger if it is not closed (otherwise it can cause crashes).
+ if (!dbg.isClosed) {
+ dbg.setDebuggerReady(true);
+ }
+ // Also unregister the worker
+ watcherDataObject.workers.splice(
+ watcherDataObject.workers.indexOf(workerInfo),
+ 1
+ );
+ watcherDataObject.pendingWorkers.delete(concurrentSessionUpdates);
+ return;
+ }
+ watcherDataObject.pendingWorkers.delete(concurrentSessionUpdates);
+
+ const { workerTargetForm, transport } = await onConnectToWorker;
+ workerInfo.workerTargetForm = workerTargetForm;
+ workerInfo.transport = transport;
+
+ const { forwardingPrefix } = watcherDataObject;
+ // Immediately queue a message for the parent process, before applying any SessionData
+ // as it may start emitting RDP events on the target actor and be lost if the client
+ // didn't get notified about the target actor first
+ try {
+ watcherDataObject.jsProcessActor.sendAsyncMessage(
+ "DevToolsProcessChild:targetAvailable",
+ {
+ watcherActorID,
+ forwardingPrefix,
+ targetActorForm: workerTargetForm,
+ }
+ );
+ } catch (e) {
+ // If there was an error while sending the message, we are not going to use this
+ // connection to communicate with the worker.
+ transport.close();
+ // Also unregister the worker
+ watcherDataObject.workers.splice(
+ watcherDataObject.workers.indexOf(workerInfo),
+ 1
+ );
+ return;
+ }
+
+ // Dispatch to the worker thread any SessionData updates which may have been notified
+ // while we were waiting for onConnectToWorker to resolve.
+ const promises = [];
+ for (const { type, entries, updateType } of concurrentSessionUpdates) {
+ promises.push(
+ addOrSetSessionDataEntryInWorkerTarget({
+ dbg,
+ workerThreadServerForwardingPrefix,
+ type,
+ entries,
+ updateType,
+ })
+ );
+ }
+ await Promise.all(promises);
+ }
+
+ destroyTargetsForWatcher(watcherDataObject) {
+ // Notify to all worker threads to destroy their target actor running in them
+ for (const {
+ dbg,
+ workerThreadServerForwardingPrefix,
+ transport,
+ } of watcherDataObject.workers) {
+ if (isWorkerDebuggerAlive(dbg)) {
+ try {
+ dbg.postMessage(
+ JSON.stringify({
+ type: "disconnect",
+ forwardingPrefix: workerThreadServerForwardingPrefix,
+ })
+ );
+ } catch (e) {}
+ }
+ // Also cleanup the DevToolsTransport created in the main thread to bridge RDP to the worker thread
+ if (transport) {
+ transport.close();
+ }
+ }
+ // Wipe all workers info
+ watcherDataObject.workers = [];
+ }
+
+ /**
+ * Indicates whether or not we should handle the worker debugger
+ *
+ * @param {Object} sessionData
+ * The session data for a given watcher, which includes metadata
+ * about the debugged context.
+ * @param {WorkerDebugger} dbg
+ * The worker debugger we want to check.
+ * @param {String} targetType
+ * The expected worker target type.
+ * @returns {Boolean}
+ */
+ shouldHandleWorker(sessionData, dbg, targetType) {
+ if (!isWorkerDebuggerAlive(dbg)) {
+ return false;
+ }
+
+ if (
+ (dbg.type === TYPE_DEDICATED && targetType != "worker") ||
+ (dbg.type === TYPE_SERVICE && targetType != "service_worker") ||
+ (dbg.type === TYPE_SHARED && targetType != "shared_worker")
+ ) {
+ return false;
+ }
+
+ const { type: sessionContextType } = sessionData.sessionContext;
+ if (sessionContextType == "all") {
+ return true;
+ }
+ if (sessionContextType == "content-process") {
+ throw new Error(
+ "Content process session type shouldn't try to spawn workers"
+ );
+ }
+ if (sessionContextType == "worker") {
+ throw new Error(
+ "worker session type should spawn only one target via the WorkerDescriptor"
+ );
+ }
+
+ if (dbg.type === TYPE_DEDICATED) {
+ // Assume that all dedicated workers executes in the same process as the debugged document.
+ const browsingContext = BrowsingContext.getCurrentTopByBrowserId(
+ sessionData.sessionContext.browserId
+ );
+ // If we aren't executing in the same process as the worker and its BrowsingContext,
+ // it will be undefined.
+ if (!browsingContext) {
+ return false;
+ }
+ for (const subBrowsingContext of browsingContext.getAllBrowsingContextsInSubtree()) {
+ if (
+ subBrowsingContext.currentWindowContext &&
+ dbg.windowIDs.includes(
+ subBrowsingContext.currentWindowContext.innerWindowId
+ )
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ if (dbg.type === TYPE_SERVICE) {
+ // Accessing `nsIPrincipal.host` may easily throw on non-http URLs.
+ // Ignore all non-HTTP as they most likely don't have any valid host name.
+ if (!dbg.principal.scheme.startsWith("http")) {
+ return false;
+ }
+
+ const workerHost = dbg.principal.hostPort;
+ return workerHost == sessionData["browser-element-host"][0];
+ }
+
+ if (dbg.type === TYPE_SHARED) {
+ // We still don't fully support instantiating targets for shared workers from the server side
+ throw new Error(
+ "Server side listening for shared workers isn't supported"
+ );
+ }
+
+ return false;
+ }
+}
+
+/**
+ * Communicate the type and entries to the Worker Target actor, via the WorkerDebugger.
+ *
+ * @param {WorkerDebugger} dbg
+ * @param {String} workerThreadServerForwardingPrefix
+ * @param {String} type
+ * Session data type name
+ * @param {Array} entries
+ * Session data entries to add or set.
+ * @param {String} updateType
+ * Either "add" or "set", to control if we should only add some items,
+ * or replace the whole data set with the new entries.
+ * @returns {Promise} Returns a Promise that resolves once the data entry were handled
+ * by the worker target.
+ */
+function addOrSetSessionDataEntryInWorkerTarget({
+ dbg,
+ workerThreadServerForwardingPrefix,
+ type,
+ entries,
+ updateType,
+}) {
+ if (!isWorkerDebuggerAlive(dbg)) {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ // Wait until we're notified by the worker that the resources are watched.
+ // This is important so we know existing resources were handled.
+ const listener = {
+ onMessage: message => {
+ message = JSON.parse(message);
+ if (message.type === "session-data-entry-added-or-set") {
+ dbg.removeListener(listener);
+ resolve();
+ }
+ },
+ // Resolve if the worker is being destroyed so we don't have a dangling promise.
+ onClose: () => {
+ dbg.removeListener(listener);
+ resolve();
+ },
+ };
+
+ dbg.addListener(listener);
+
+ dbg.postMessage(
+ JSON.stringify({
+ type: "add-or-set-session-data-entry",
+ forwardingPrefix: workerThreadServerForwardingPrefix,
+ dataEntryType: type,
+ entries,
+ updateType,
+ })
+ );
+ });
+}
+
+function isWorkerDebuggerAlive(dbg) {
+ if (dbg.isClosed) {
+ return false;
+ }
+ // Some workers are zombies. `isClosed` is false, but nothing works.
+ // `postMessage` is a noop, `addListener`'s `onClosed` doesn't work.
+ return (
+ dbg.window?.docShell ||
+ // consider dbg without `window` as being alive, as they aren't related
+ // to any docShell and probably do not suffer from this issue
+ !dbg.window
+ );
+}
+
+export const WorkerTargetWatcher = new WorkerTargetWatcherClass();
diff --git a/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs
deleted file mode 100644
index acb5e97110..0000000000
--- a/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs
+++ /dev/null
@@ -1,710 +0,0 @@
-/* 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/. */
-
-import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
-import * as Loader from "resource://devtools/shared/loader/Loader.sys.mjs";
-
-const lazy = {};
-ChromeUtils.defineESModuleGetters(lazy, {
- isWindowGlobalPartOfContext:
- "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
- releaseDistinctSystemPrincipalLoader:
- "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
- TargetActorRegistry:
- "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
- useDistinctSystemPrincipalLoader:
- "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
- WindowGlobalLogger:
- "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs",
-});
-
-const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
- "devtools.every-frame-target.enabled",
- false
-);
-
-// Name of the attribute into which we save data in `sharedData` object.
-const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
-
-// If true, log info about WindowGlobal's being created.
-const DEBUG = false;
-function logWindowGlobal(windowGlobal, message) {
- if (!DEBUG) {
- return;
- }
- lazy.WindowGlobalLogger.logWindowGlobal(windowGlobal, message);
-}
-
-export class DevToolsFrameChild extends JSWindowActorChild {
- 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 WindowGlobalTargetActor instance
- this._connections = new Map();
-
- EventEmitter.decorate(this);
-
- // Set the following preference on the constructor, so that we can easily
- // toggle these preferences on and off from tests and have the new value being picked up.
-
- // bfcache-in-parent changes significantly how navigation behaves.
- // We may start reusing previously existing WindowGlobal and so reuse
- // previous set of JSWindowActor pairs (i.e. DevToolsFrameParent/DevToolsFrameChild).
- // When enabled, regular navigations may also change and spawn new BrowsingContexts.
- // If the page we navigate from supports being stored in bfcache,
- // the navigation will use a new BrowsingContext. And so force spawning
- // a new top-level target.
- ChromeUtils.defineLazyGetter(
- this,
- "isBfcacheInParentEnabled",
- () =>
- Services.appinfo.sessionHistoryInParent &&
- Services.prefs.getBoolPref("fission.bfcacheInParent", false)
- );
- }
-
- /**
- * Try to instantiate new target actors for the current WindowGlobal, and that,
- * for all the currently registered Watcher actors.
- *
- * Read the `sharedData` to get metadata about all registered watcher actors.
- * If these watcher actors are interested in the current WindowGlobal,
- * instantiate a new dedicated target actor for each of the watchers.
- *
- * @param Object options
- * @param Boolean options.isBFCache
- * True, if the request to instantiate a new target comes from a bfcache navigation.
- * i.e. when we receive a pageshow event with persisted=true.
- * This will be true regardless of bfcacheInParent being enabled or disabled.
- * @param Boolean options.ignoreIfExisting
- * By default to false. If true is passed, we avoid instantiating a target actor
- * if one already exists for this windowGlobal.
- */
- instantiate({ isBFCache = false, ignoreIfExisting = false } = {}) {
- const { sharedData } = Services.cpmm;
- const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
- if (!sessionDataByWatcherActor) {
- throw new Error(
- "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets"
- );
- }
-
- // Create one Target actor for each prefix/client which listen to frames
- for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
- const { connectionPrefix, sessionContext } = sessionData;
- // For bfcache navigations, we only create new targets when bfcacheInParent is enabled,
- // as this would be the only case where new DocShells will be created. This requires us to spawn a
- // new WindowGlobalTargetActor as one such actor is bound to a unique DocShell.
- const forceAcceptTopLevelTarget =
- isBFCache && this.isBfcacheInParentEnabled;
- if (
- sessionData.targets?.includes("frame") &&
- lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, {
- forceAcceptTopLevelTarget,
- })
- ) {
- // If this was triggered because of a navigation, we want to retrieve the existing
- // target we were debugging so we can destroy it before creating the new target.
- // This is important because we had cases where the destruction of an old target
- // was unsetting a flag on the **new** target document, breaking the toolbox (See Bug 1721398).
-
- // We're checking for an existing target given a watcherActorID + browserId + browsingContext
- // Note that a target switch might create a new browsing context, so we wouldn't
- // retrieve the existing target here. We are okay with this as:
- // - this shouldn't happen much
- // - in such case we weren't seeing the issue of Bug 1721398 (the old target can't access the new document)
- const existingTarget = this._findTargetActor({
- watcherActorID,
- sessionContext,
- browsingContextId: this.manager.browsingContext.id,
- });
-
- // See comment in handleEvent(DOMDocElementInserted) to know why we try to
- // create targets if none already exists
- if (existingTarget && ignoreIfExisting) {
- continue;
- }
-
- // Bail if there is already an existing WindowGlobalTargetActor which wasn't
- // created from a JSWIndowActor.
- // This means we are reloading or navigating (same-process) a Target
- // which has not been created using the Watcher, but from the client (most likely
- // the initial target of a local-tab toolbox).
- // However, we force overriding the first message manager based target in case of
- // BFCache navigations.
- if (
- existingTarget &&
- !existingTarget.createdFromJsWindowActor &&
- !isBFCache
- ) {
- continue;
- }
-
- // If we decide to instantiate a new target and there was one before,
- // first destroy the previous one.
- // Otherwise its destroy sequence will be executed *after* the new one
- // is being initialized and may easily revert changes made against platform API.
- // (typically toggle platform boolean attributes back to default…)
- if (existingTarget) {
- existingTarget.destroy({ isTargetSwitching: true });
- }
-
- this._createTargetActor({
- watcherActorID,
- parentConnectionPrefix: connectionPrefix,
- sessionData,
- isDocumentCreation: true,
- });
- }
- }
- }
-
- /**
- * Instantiate a new WindowGlobalTarget for the given connection.
- *
- * @param Object options
- * @param String options.watcherActorID
- * The ID of the WatcherActor who requested to observe and create these target actors.
- * @param String options.parentConnectionPrefix
- * The prefix of the DevToolsServerConnection of the Watcher Actor.
- * This is used to compute a unique ID for the target actor.
- * @param Object options.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 Boolean options.isDocumentCreation
- * Set to true if the function is called from `instantiate`, i.e. when we're
- * handling a new document being created.
- * @param Boolean options.fromInstantiateAlreadyAvailable
- * Set to true if the function is called from handling `DevToolsFrameParent:instantiate-already-available`
- * query.
- */
- _createTargetActor({
- watcherActorID,
- parentConnectionPrefix,
- sessionData,
- isDocumentCreation,
- fromInstantiateAlreadyAvailable,
- }) {
- if (this._connections.get(watcherActorID)) {
- // If this function is called as a result of a `DevToolsFrameParent:instantiate-already-available`
- // message, we might have a legitimate race condition:
- // In frame-helper, we want to create the initial targets for a given browser element.
- // It might happen that the `DevToolsFrameParent:instantiate-already-available` is
- // aborted if the page navigates (and the document is destroyed) while the query is still pending.
- // In such case, frame-helper will try to send a new message. In the meantime,
- // the DevToolsFrameChild `DOMWindowCreated` handler may already have been called and
- // the new target already created.
- // We don't want to throw in such case, as our end-goal, having a target for the document,
- // is achieved.
- if (fromInstantiateAlreadyAvailable) {
- return;
- }
- throw new Error(
- "DevToolsFrameChild _createTargetActor was called more than once" +
- ` for the same Watcher (Actor ID: "${watcherActorID}")`
- );
- }
-
- // Compute a unique prefix, just for this WindowGlobal,
- // 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: WindowGlobal's innerWindowId should be unique across processes, I think. So that should be safe?
- // (this.manager == WindowGlobalChild interface)
- const forwardingPrefix =
- parentConnectionPrefix + "windowGlobal" + this.manager.innerWindowId;
-
- logWindowGlobal(
- this.manager,
- "Instantiate WindowGlobalTarget with prefix: " + forwardingPrefix
- );
-
- const { connection, targetActor } = this._createConnectionAndActor(
- forwardingPrefix,
- sessionData
- );
- const form = targetActor.form();
- // Ensure unregistering and destroying the related DevToolsServerConnection+Transport
- // on both content and parent process JSWindowActors.
- targetActor.once("destroyed", options => {
- // This will destroy the content process one
- this._destroyTargetActor(watcherActorID, options);
- // And this will destroy the parent process one
- try {
- this.sendAsyncMessage("DevToolsFrameChild:destroy", {
- actors: [
- {
- watcherActorID,
- form,
- },
- ],
- options,
- });
- } catch (e) {
- // Ignore exception when the JSWindowActorChild has already been destroyed.
- // We often try to emit this message while the WindowGlobal is in the process of being
- // destroyed. We eagerly destroy the target actor during reloads,
- // just before the WindowGlobal starts destroying, but sendAsyncMessage
- // doesn't have time to complete and throws.
- if (
- !e.message.includes("JSWindowActorChild cannot send at the moment")
- ) {
- throw e;
- }
- }
- });
- 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 WindowGlobalTargetActor may emit events in its constructor.
- // If it does, such RDP packets may be lost.
- // The important point here is to send this message before processing the sessionData,
- // which will start the Watcher and start emitting resources on the target actor.
- this.sendAsyncMessage("DevToolsFrameChild: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,
- entries,
- isDocumentCreation,
- "set"
- );
- }
- }
-
- /**
- * @param {string} watcherActorID
- * @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(watcherActorID, options) {
- 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}`
- );
- }
- connectionInfo.connection.close(options);
- this._connections.delete(watcherActorID);
- if (this._connections.size == 0) {
- this.didDestroy(options);
- }
- }
-
- _createConnectionAndActor(forwardingPrefix, sessionData) {
- this.useCustomLoader = this.document.nodePrincipal.isSystemPrincipal;
-
- if (!this.loader) {
- // When debugging chrome pages, use a new dedicated loader, using a distinct chrome compartment.
- this.loader = this.useCustomLoader
- ? lazy.useDistinctSystemPrincipalLoader(this)
- : Loader;
- }
- const { DevToolsServer } = this.loader.require(
- "resource://devtools/server/devtools-server.js"
- );
-
- const { WindowGlobalTargetActor } = this.loader.require(
- "resource://devtools/server/actors/targets/window-global.js"
- );
-
- DevToolsServer.init();
-
- // We want a special server without any root actor and only target-scoped actors.
- // We are going to spawn a WindowGlobalTargetActor instance in the next few lines,
- // it is going to act like a root actor without being one.
- DevToolsServer.registerActors({ target: true });
-
- const connection = DevToolsServer.connectToParentWindowActor(
- this,
- forwardingPrefix
- );
-
- // In the case of the browser toolbox, tab's BrowsingContext don't have
- // any parent BC and shouldn't be considered as top-level.
- // This is why we check for browserId's.
- const browsingContext = this.manager.browsingContext;
- const isTopLevelTarget =
- !browsingContext.parent &&
- browsingContext.browserId == sessionData.sessionContext.browserId;
-
- // Create the actual target actor.
- const targetActor = new WindowGlobalTargetActor(connection, {
- docShell: this.docShell,
- // Targets created from the server side, via Watcher actor and DevToolsFrame JSWindow
- // actor pair are following WindowGlobal lifecycle. i.e. will be destroyed on any
- // type of navigation/reload.
- followWindowGlobalLifeCycle: true,
- isTopLevelTarget,
- ignoreSubFrames: isEveryFrameTargetEnabled,
- 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);
- targetActor.createdFromJsWindowActor = true;
-
- return { connection, targetActor };
- }
-
- /**
- * Supported Queries
- */
-
- sendPacket(packet, prefix) {
- this.sendAsyncMessage("DevToolsFrameChild:packet", { packet, prefix });
- }
-
- /**
- * JsWindowActor API
- */
-
- async sendQuery(msg, args) {
- try {
- const res = await super.sendQuery(msg, args);
- return res;
- } catch (e) {
- console.error("Failed to sendQuery in DevToolsFrameChild", msg);
- console.error(e.toString());
- throw e;
- }
- }
-
- receiveMessage(message) {
- // Assert that the message is intended for this window global,
- // except for "packet" messages which use a dedicated routing
- if (
- message.name != "DevToolsFrameParent:packet" &&
- message.data.sessionContext.type == "browser-element"
- ) {
- const { browserId } = message.data.sessionContext;
- // Re-check here, just to ensure that both parent and content processes agree
- // on what should or should not be watched.
- if (
- this.manager.browsingContext.browserId != browserId &&
- !lazy.isWindowGlobalPartOfContext(
- this.manager,
- message.data.sessionContext,
- {
- forceAcceptTopLevelTarget: true,
- }
- )
- ) {
- throw new Error(
- "Mismatch between DevToolsFrameParent and DevToolsFrameChild " +
- (this.manager.browsingContext.browserId == browserId
- ? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)"
- : `expected browsing context with browserId ${browserId}, but got ${this.manager.browsingContext.browserId}`)
- );
- }
- }
- switch (message.name) {
- case "DevToolsFrameParent:instantiate-already-available": {
- const { watcherActorID, connectionPrefix, sessionData } = message.data;
-
- return this._createTargetActor({
- watcherActorID,
- parentConnectionPrefix: connectionPrefix,
- sessionData,
- fromInstantiateAlreadyAvailable: true,
- });
- }
- case "DevToolsFrameParent:destroy": {
- const { watcherActorID, options } = message.data;
- return this._destroyTargetActor(watcherActorID, options);
- }
- case "DevToolsFrameParent:addOrSetSessionDataEntry": {
- const { watcherActorID, sessionContext, type, entries, updateType } =
- message.data;
- return this._addOrSetSessionDataEntry(
- watcherActorID,
- sessionContext,
- type,
- entries,
- updateType
- );
- }
- case "DevToolsFrameParent:removeSessionDataEntry": {
- const { watcherActorID, sessionContext, type, entries } = message.data;
- return this._removeSessionDataEntry(
- watcherActorID,
- sessionContext,
- type,
- entries
- );
- }
- case "DevToolsFrameParent:packet":
- return this.emit("packet-received", message);
- default:
- throw new Error(
- "Unsupported message in DevToolsFrameParent: " + message.name
- );
- }
- }
-
- /**
- * Return an existing target given a WatcherActor id, a browserId and an optional
- * browsing context id.
- * /!\ Note that we may have multiple targets for a given (watcherActorId, browserId) couple,
- * for example if we have 2 remote iframes sharing the same origin, which is why you
- * might want to pass a specific browsing context id to filter the list down.
- *
- * @param {Object} options
- * @param {Object} options.watcherActorID
- * @param {Object} options.sessionContext
- * @param {Object} options.browsingContextId: Optional browsing context id to narrow the
- * search to a specific browsing context.
- *
- * @returns {WindowGlobalTargetActor|null}
- */
- _findTargetActor({ watcherActorID, sessionContext, browsingContextId }) {
- // First let's check if a target was created for this watcher actor in this specific
- // DevToolsFrameChild instance.
- const connectionInfo = this._connections.get(watcherActorID);
- const targetActor = connectionInfo ? connectionInfo.actor : null;
- if (targetActor) {
- return targetActor;
- }
-
- // If we couldn't find such target, we want to see if a target was created for this
- // (watcherActorId,browserId, {browsingContextId}) in another DevToolsFrameChild instance.
- // This might be the case if we're navigating to a new page with server side target
- // enabled and we want to retrieve the target of the page we're navigating from.
- if (
- lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, {
- forceAcceptTopLevelTarget: true,
- })
- ) {
- // Ensure retrieving the one target actor related to this connection.
- // This allows to distinguish actors created for various toolboxes.
- // For ex, regular toolbox versus browser console versus browser toolbox
- const connectionPrefix = watcherActorID.replace(/watcher\d+$/, "");
- const targetActors = lazy.TargetActorRegistry.getTargetActors(
- sessionContext,
- connectionPrefix
- );
-
- if (!browsingContextId) {
- return targetActors[0] || null;
- }
- return targetActors.find(
- actor => actor.browsingContextID === browsingContextId
- );
- }
- return null;
- }
-
- _addOrSetSessionDataEntry(
- watcherActorID,
- sessionContext,
- type,
- entries,
- updateType
- ) {
- // /!\ We may have an issue here as there could be multiple targets for a given
- // (watcherActorID,browserId) pair.
- // This should be clarified as part of Bug 1725623.
- const targetActor = this._findTargetActor({
- watcherActorID,
- sessionContext,
- });
-
- if (!targetActor) {
- throw new Error(
- `No target actor for this Watcher Actor ID:"${watcherActorID}" / BrowserId:${sessionContext.browserId}`
- );
- }
- return targetActor.addOrSetSessionDataEntry(
- type,
- entries,
- false,
- updateType
- );
- }
-
- _removeSessionDataEntry(watcherActorID, sessionContext, type, entries) {
- // /!\ We may have an issue here as there could be multiple targets for a given
- // (watcherActorID,browserId) pair.
- // This should be clarified as part of Bug 1725623.
- const targetActor = this._findTargetActor({
- watcherActorID,
- sessionContext,
- });
- // By the time we are calling this, the target may already have been destroyed.
- if (!targetActor) {
- return null;
- }
- return targetActor.removeSessionDataEntry(type, entries);
- }
-
- handleEvent({ type, persisted, target }) {
- // Ignore any event that may fire for children WindowGlobals/documents
- if (target != this.document) {
- return;
- }
-
- // DOMWindowCreated is registered from FrameWatcher via `ActorManagerParent.addJSWindowActors`
- // as a DOM event to be listened to and so is fired by JS Window Actor code platform code.
- if (type == "DOMWindowCreated") {
- this.instantiate();
- return;
- }
- // We might have ignored the DOMWindowCreated event because it was the initial about:blank document.
- // But when loading same-process iframes, we reuse the WindowGlobal of the about:bank document
- // to load the actual URL loaded in the iframe. This means we won't have a new DOMWindowCreated
- // for the actual document. There is a DOMDocElementInserted fired just after, that we can catch
- // to create a target for same-process iframes.
- // This means that we still do not create any target for the initial documents.
- // It is complex to instantiate targets for initial documents because:
- // - it would mean spawning two targets for the same WindowGlobal and sharing the same innerWindowId
- // - or have WindowGlobalTargets to handle more than one document (it would mean reusing will-navigate/window-ready events
- // both on client and server)
- if (type == "DOMDocElementInserted") {
- this.instantiate({ ignoreIfExisting: true });
- return;
- }
-
- // If persisted=true, this is a BFCache navigation.
- //
- // With Fission enabled and bfcacheInParent, BFCache navigations will spawn a new DocShell
- // in the same process:
- // * the previous page won't be destroyed, its JSWindowActor will stay alive (`didDestroy` won't be called)
- // and a 'pagehide' with persisted=true will be emitted on it.
- // * the new page page won't emit any DOMWindowCreated, but instead a pageshow with persisted=true
- // will be emitted.
-
- if (type === "pageshow" && persisted) {
- // Notify all bfcache navigations, even the one for which we don't create a new target
- // as that's being useful for parent process storage resource watchers.
- this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pageshow");
-
- // Here we are going to re-instantiate a target that got destroyed before while processing a pagehide event.
- // We force instantiating a new top level target, within `instantiate()` even if server targets are disabled.
- // But we only do that if bfcacheInParent is enabled. Otherwise for same-process, same-docshell bfcache navigation,
- // we don't want to spawn new targets.
- this.instantiate({
- isBFCache: true,
- });
- return;
- }
-
- if (type === "pagehide" && persisted) {
- // Notify all bfcache navigations, even the one for which we don't create a new target
- // as that's being useful for parent process storage resource watchers.
- this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pagehide");
-
- // We might navigate away for the first top level target,
- // which isn't using JSWindowActor (it still uses messages manager and is created by the client, via TabDescriptor.getTarget).
- // We have to unregister it from the TargetActorRegistry, otherwise,
- // if we navigate back to it, the next DOMWindowCreated won't create a new target for it.
- const { sharedData } = Services.cpmm;
- const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
- if (!sessionDataByWatcherActor) {
- throw new Error(
- "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets"
- );
- }
-
- const actors = [];
- // A flag to know if the following for loop ended up destroying all the actors.
- // It may not be the case if one Watcher isn't having server target switching enabled.
- let allActorsAreDestroyed = true;
- for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
- const { sessionContext } = sessionData;
-
- // /!\ We may have an issue here as there could be multiple targets for a given
- // (watcherActorID,browserId) pair.
- // This should be clarified as part of Bug 1725623.
- const existingTarget = this._findTargetActor({
- watcherActorID,
- sessionContext,
- });
-
- if (!existingTarget) {
- continue;
- }
-
- // Use `originalWindow` as `window` can be set when a document was selected from
- // the iframe picker in the toolbox toolbar.
- if (existingTarget.originalWindow.document != target) {
- throw new Error("Existing target actor is for a distinct document");
- }
- // Do not do anything if both bfcache in parent and server targets are disabled
- // As history navigations will be handled within the same DocShell and by the
- // same WindowGlobalTargetActor. The actor will listen to pageshow/pagehide by itself.
- // We should not destroy any target.
- if (
- !this.isBfcacheInParentEnabled &&
- !sessionContext.isServerTargetSwitchingEnabled
- ) {
- allActorsAreDestroyed = false;
- continue;
- }
-
- actors.push({
- watcherActorID,
- form: existingTarget.form(),
- });
- existingTarget.destroy();
- }
-
- if (actors.length) {
- // The most important is to unregister the actor from TargetActorRegistry,
- // so that it is no longer present in the list when new DOMWindowCreated fires.
- // This will also help notify the client that the target has been destroyed.
- // And if we navigate back to this target, the client will receive the same target actor ID,
- // so that it is really important to destroy it correctly on both server and client.
- this.sendAsyncMessage("DevToolsFrameChild:destroy", { actors });
- }
-
- if (allActorsAreDestroyed) {
- // Completely clear this JSWindow Actor.
- // Do this after having called _findTargetActor,
- // as it would clear the registered target actors.
- this.didDestroy();
- }
- }
- }
-
- didDestroy(options) {
- logWindowGlobal(this.manager, "Destroy WindowGlobalTarget");
- for (const { connection } of this._connections.values()) {
- connection.close(options);
- }
- this._connections.clear();
-
- if (this.loader) {
- if (this.useCustomLoader) {
- lazy.releaseDistinctSystemPrincipalLoader(this);
- }
- this.loader = null;
- }
- }
-}
diff --git a/devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs
deleted file mode 100644
index 31750d58e4..0000000000
--- a/devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs
+++ /dev/null
@@ -1,277 +0,0 @@
-/* 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/. */
-
-import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs";
-import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
-
-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.
- { global: "shared" }
-);
-
-const lazy = {};
-
-loader.lazyRequireGetter(
- lazy,
- "JsWindowActorTransport",
- "resource://devtools/shared/transport/js-window-actor-transport.js",
- true
-);
-
-export class DevToolsFrameParent extends JSWindowActorParent {
- constructor() {
- super();
-
- // Map of DevToolsServerConnection's used to forward the messages from/to
- // the client. The connections run in the parent process, as this code. We
- // may have more than one when there is more than one client debugging the
- // same frame. For example, a content toolbox and the browser toolbox.
- //
- // The map is indexed by the connection prefix.
- // The values are objects containing the following properties:
- // - actor: the frame target actor(as a form)
- // - connection: the DevToolsServerConnection used to communicate with the
- // frame target actor
- // - prefix: the forwarding prefix used by the connection to know
- // how to forward packets to the frame target
- // - transport: the JsWindowActorTransport
- //
- // Reminder about prefixes: all DevToolsServerConnections have a `prefix`
- // which can be considered as a kind of id. On top of this, parent process
- // DevToolsServerConnections also have forwarding prefixes because they are
- // responsible for forwarding messages to content process connections.
- this._connections = new Map();
-
- this._onConnectionClosed = this._onConnectionClosed.bind(this);
- EventEmitter.decorate(this);
- }
-
- /**
- * Request the content process to create the Frame Target if there is one
- * already available that matches the Browsing Context ID
- */
- async instantiateTarget({
- watcherActorID,
- connectionPrefix,
- sessionContext,
- sessionData,
- }) {
- await this.sendQuery("DevToolsFrameParent:instantiate-already-available", {
- watcherActorID,
- connectionPrefix,
- sessionContext,
- sessionData,
- });
- }
-
- /**
- * @param {object} arg
- * @param {object} arg.sessionContext
- * @param {object} arg.options
- * @param {boolean} arg.options.isModeSwitching
- * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
- */
- destroyTarget({ watcherActorID, sessionContext, options }) {
- this.sendAsyncMessage("DevToolsFrameParent:destroy", {
- watcherActorID,
- sessionContext,
- options,
- });
- }
-
- /**
- * Communicate to the content process that some data have been added.
- */
- async addOrSetSessionDataEntry({
- watcherActorID,
- sessionContext,
- type,
- entries,
- updateType,
- }) {
- try {
- await this.sendQuery("DevToolsFrameParent:addOrSetSessionDataEntry", {
- watcherActorID,
- sessionContext,
- type,
- entries,
- updateType,
- });
- } catch (e) {
- console.warn(
- "Failed to add session data entry for frame targets in browsing context",
- this.browsingContext.id
- );
- console.warn(e);
- }
- }
-
- /**
- * Communicate to the content process that some data have been removed.
- */
- removeSessionDataEntry({ watcherActorID, sessionContext, type, entries }) {
- this.sendAsyncMessage("DevToolsFrameParent:removeSessionDataEntry", {
- watcherActorID,
- sessionContext,
- type,
- entries,
- });
- }
-
- connectFromContent({ watcherActorID, forwardingPrefix, actor }) {
- const watcher = WatcherRegistry.getWatcher(watcherActorID);
-
- if (!watcher) {
- throw new Error(
- `Watcher Actor with ID '${watcherActorID}' can't be found.`
- );
- }
- const connection = watcher.conn;
-
- connection.on("closed", this._onConnectionClosed);
-
- // Create a js-window-actor based transport.
- const transport = new lazy.JsWindowActorTransport(this, forwardingPrefix);
- transport.hooks = {
- onPacket: connection.send.bind(connection),
- onTransportClosed() {},
- };
- transport.ready();
-
- connection.setForwarding(forwardingPrefix, transport);
-
- this._connections.set(watcher.conn.prefix, {
- watcher,
- connection,
- // This prefix is the prefix of the DevToolsServerConnection, running
- // in the content process, for which we should forward packets to, based on its prefix.
- // While `watcher.connection` is also a DevToolsServerConnection, but from this process,
- // the parent process. It is the one receiving Client packets and the one, from which
- // we should forward packets from.
- forwardingPrefix,
- transport,
- actor,
- });
-
- watcher.notifyTargetAvailable(actor);
- }
-
- _onConnectionClosed(status, connectionPrefix) {
- this._unregisterWatcher(connectionPrefix);
- }
-
- /**
- * Given a watcher connection prefix, unregister everything related to the Watcher
- * in this JSWindowActor.
- *
- * @param {String} connectionPrefix
- * The connection prefix of the watcher to unregister
- */
- async _unregisterWatcher(connectionPrefix) {
- const connectionInfo = this._connections.get(connectionPrefix);
- if (!connectionInfo) {
- return;
- }
- const { forwardingPrefix, transport, connection } = connectionInfo;
- this._connections.delete(connectionPrefix);
-
- connection.off("closed", this._onConnectionClosed);
- if (transport) {
- // If we have a child transport, the actor has already
- // been created. We need to stop using this transport.
- transport.close();
- }
-
- connection.cancelForwarding(forwardingPrefix);
- }
-
- /**
- * Destroy everything that we did related to the current WindowGlobal that
- * this JSWindow Actor represents:
- * - close all transports that were used as bridge to communicate with the
- * DevToolsFrameChild, running in the content process
- * - unregister these transports from DevToolsServer (cancelForwarding)
- * - notify the client, via the WatcherActor that all related targets,
- * one per client/connection are all destroyed
- *
- * Note that with bfcacheInParent, we may reuse a JSWindowActor pair after closing all connections.
- * This is can happen outside of the destruction of the actor.
- * We may reuse a DevToolsFrameParent and DevToolsFrameChild pair.
- * When navigating away, we will destroy them and call this method.
- * Then when navigating back, we will reuse the same instances.
- * So that we should be careful to keep the class fully function and only clear all its state.
- *
- * @param {object} options
- * @param {boolean} options.isModeSwitching
- * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
- */
- _closeAllConnections(options) {
- for (const { actor, watcher } of this._connections.values()) {
- watcher.notifyTargetDestroyed(actor, options);
- this._unregisterWatcher(watcher.conn.prefix);
- }
- this._connections.clear();
- }
-
- /**
- * Supported Queries
- */
-
- sendPacket(packet, prefix) {
- this.sendAsyncMessage("DevToolsFrameParent:packet", { packet, prefix });
- }
-
- /**
- * JsWindowActor API
- */
-
- receiveMessage(message) {
- switch (message.name) {
- case "DevToolsFrameChild:connectFromContent":
- return this.connectFromContent(message.data);
- case "DevToolsFrameChild:packet":
- return this.emit("packet-received", message);
- case "DevToolsFrameChild:destroy":
- for (const { form, watcherActorID } of message.data.actors) {
- const watcher = WatcherRegistry.getWatcher(watcherActorID);
- // As we instruct to destroy all targets when the watcher is destroyed,
- // we may easily receive the target destruction notification *after*
- // the watcher has been removed from the registry.
- if (watcher) {
- watcher.notifyTargetDestroyed(form, message.data.options);
- this._unregisterWatcher(watcher.conn.prefix);
- }
- }
- return null;
- case "DevToolsFrameChild:bf-cache-navigation-pageshow":
- for (const watcherActor of WatcherRegistry.getWatchersForBrowserId(
- this.browsingContext.browserId
- )) {
- watcherActor.emit("bf-cache-navigation-pageshow", {
- windowGlobal: this.browsingContext.currentWindowGlobal,
- });
- }
- return null;
- case "DevToolsFrameChild:bf-cache-navigation-pagehide":
- for (const watcherActor of WatcherRegistry.getWatchersForBrowserId(
- this.browsingContext.browserId
- )) {
- watcherActor.emit("bf-cache-navigation-pagehide", {
- windowGlobal: this.browsingContext.currentWindowGlobal,
- });
- }
- return null;
- default:
- throw new Error(
- "Unsupported message in DevToolsFrameParent: " + message.name
- );
- }
- }
-
- didDestroy() {
- this._closeAllConnections();
- }
-}
diff --git a/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs
deleted file mode 100644
index 6bbe4140c3..0000000000
--- a/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs
+++ /dev/null
@@ -1,571 +0,0 @@
-/* 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/. */
-
-import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
-
-import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
-
-const lazy = {};
-
-XPCOMUtils.defineLazyServiceGetter(
- lazy,
- "wdm",
- "@mozilla.org/dom/workers/workerdebuggermanager;1",
- "nsIWorkerDebuggerManager"
-);
-
-ChromeUtils.defineLazyGetter(lazy, "Loader", () =>
- ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs")
-);
-
-ChromeUtils.defineLazyGetter(lazy, "DevToolsUtils", () =>
- lazy.Loader.require("resource://devtools/shared/DevToolsUtils.js")
-);
-XPCOMUtils.defineLazyModuleGetters(lazy, {
- SessionDataHelpers:
- "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm",
-});
-ChromeUtils.defineESModuleGetters(lazy, {
- isWindowGlobalPartOfContext:
- "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
-});
-
-// Name of the attribute into which we save data in `sharedData` object.
-const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
-
-export class DevToolsWorkerChild extends JSWindowActorChild {
- constructor() {
- super();
-
- // The map is indexed by the Watcher Actor ID.
- // The values are objects containing the following properties:
- // - connection: the DevToolsServerConnection itself
- // - workers: An array of object containing the following properties:
- // - dbg: A WorkerDebuggerInstance
- // - workerTargetForm: The associated worker target instance form
- // - workerThreadServerForwardingPrefix: The prefix used to forward events to the
- // worker target on the worker thread ().
- // - forwardingPrefix: Prefix used by the JSWindowActorTransport pair to communicate
- // between content and parent processes.
- // - sessionData: Data (targets, resources, …) the watcher wants to be notified about.
- // See WatcherRegistry.getSessionData to see the full list of properties.
- this._connections = new Map();
-
- EventEmitter.decorate(this);
- }
-
- _onWorkerRegistered(dbg) {
- if (!this._shouldHandleWorker(dbg)) {
- return;
- }
-
- for (const [watcherActorID, { connection, forwardingPrefix }] of this
- ._connections) {
- this._createWorkerTargetActor({
- dbg,
- connection,
- forwardingPrefix,
- watcherActorID,
- });
- }
- }
-
- _onWorkerUnregistered(dbg) {
- for (const [watcherActorID, { workers, forwardingPrefix }] of this
- ._connections) {
- // Check if the worker registration was handled for this watcherActorID.
- const unregisteredActorIndex = workers.findIndex(worker => {
- try {
- // Accessing the WorkerDebugger id might throw (NS_ERROR_UNEXPECTED).
- return worker.dbg.id === dbg.id;
- } catch (e) {
- return false;
- }
- });
- if (unregisteredActorIndex === -1) {
- continue;
- }
-
- const { workerTargetForm, transport } = workers[unregisteredActorIndex];
- transport.close();
-
- try {
- this.sendAsyncMessage("DevToolsWorkerChild:workerTargetDestroyed", {
- watcherActorID,
- forwardingPrefix,
- workerTargetForm,
- });
- } catch (e) {
- return;
- }
-
- workers.splice(unregisteredActorIndex, 1);
- }
- }
-
- onDOMWindowCreated() {
- const { sharedData } = Services.cpmm;
- const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
- if (!sessionDataByWatcherActor) {
- throw new Error(
- "Request to instantiate the target(s) for the Worker, but `sharedData` is empty about watched targets"
- );
- }
-
- // Create one Target actor for each prefix/client which listen to workers
- for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
- const { targets, connectionPrefix, sessionContext } = sessionData;
- if (
- targets?.includes("worker") &&
- lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, {
- acceptInitialDocument: true,
- forceAcceptTopLevelTarget: true,
- acceptSameProcessIframes: true,
- })
- ) {
- this._watchWorkerTargets({
- watcherActorID,
- parentConnectionPrefix: connectionPrefix,
- sessionData,
- });
- }
- }
- }
-
- /**
- * Function handling messages sent by DevToolsWorkerParent (part of JSWindowActor API).
- *
- * @param {Object} message
- * @param {String} message.name
- * @param {*} message.data
- */
- receiveMessage(message) {
- // All messages pass `sessionContext` (except packet) and are expected
- // to match isWindowGlobalPartOfContext result.
- if (message.name != "DevToolsWorkerParent:packet") {
- const { browserId } = message.data.sessionContext;
- // Re-check here, just to ensure that both parent and content processes agree
- // on what should or should not be watched.
- if (
- this.manager.browsingContext.browserId != browserId &&
- !lazy.isWindowGlobalPartOfContext(
- this.manager,
- message.data.sessionContext,
- {
- acceptInitialDocument: true,
- }
- )
- ) {
- throw new Error(
- "Mismatch between DevToolsWorkerParent and DevToolsWorkerChild " +
- (this.manager.browsingContext.browserId == browserId
- ? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)"
- : `expected browsing context with ID ${browserId}, but got ${this.manager.browsingContext.browserId}`)
- );
- }
- }
-
- switch (message.name) {
- case "DevToolsWorkerParent:instantiate-already-available": {
- const { watcherActorID, connectionPrefix, sessionData } = message.data;
-
- return this._watchWorkerTargets({
- watcherActorID,
- parentConnectionPrefix: connectionPrefix,
- sessionData,
- });
- }
- case "DevToolsWorkerParent:destroy": {
- const { watcherActorID } = message.data;
- return this._destroyTargetActors(watcherActorID);
- }
- case "DevToolsWorkerParent:addOrSetSessionDataEntry": {
- const { watcherActorID, type, entries, updateType } = message.data;
- return this._addOrSetSessionDataEntry(
- watcherActorID,
- type,
- entries,
- updateType
- );
- }
- case "DevToolsWorkerParent:removeSessionDataEntry": {
- const { watcherActorID, type, entries } = message.data;
- return this._removeSessionDataEntry(watcherActorID, type, entries);
- }
- case "DevToolsWorkerParent:packet":
- return this.emit("packet-received", message);
- default:
- throw new Error(
- "Unsupported message in DevToolsWorkerParent: " + message.name
- );
- }
- }
-
- /**
- * Instantiate targets for existing workers, watch for worker registration and listen
- * for resources on those workers, for given connection and context. Targets are sent
- * to the DevToolsWorkerParent via the DevToolsWorkerChild:workerTargetAvailable message.
- *
- * @param {Object} options
- * @param {String} options.watcherActorID: The ID of the WatcherActor who requested to
- * observe and create these target actors.
- * @param {String} options.parentConnectionPrefix: The prefix of the DevToolsServerConnection
- * of the Watcher Actor. This is used to compute a unique ID for the target actor.
- * @param {Object} options.sessionData: Data (targets, resources, …) the watcher wants
- * to be notified about. See WatcherRegistry.getSessionData to see the full list
- * of properties.
- */
- async _watchWorkerTargets({
- watcherActorID,
- parentConnectionPrefix,
- sessionData,
- }) {
- if (this._connections.has(watcherActorID)) {
- throw new Error(
- "DevToolsWorkerChild _watchWorkerTargets was called more than once" +
- ` for the same Watcher (Actor ID: "${watcherActorID}")`
- );
- }
-
- // Listen for new workers that will be spawned.
- if (!this._workerDebuggerListener) {
- this._workerDebuggerListener = {
- onRegister: this._onWorkerRegistered.bind(this),
- onUnregister: this._onWorkerUnregistered.bind(this),
- };
- lazy.wdm.addListener(this._workerDebuggerListener);
- }
-
- // Compute a unique prefix, just for this WindowGlobal,
- // 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
- // WindowGlobalChild's innerWindowId should be unique across processes, so it should be safe?
- // (this.manager == WindowGlobalChild interface)
- const forwardingPrefix =
- parentConnectionPrefix + "workerGlobal" + this.manager.innerWindowId;
-
- const connection = this._createConnection(forwardingPrefix);
-
- this._connections.set(watcherActorID, {
- connection,
- workers: [],
- forwardingPrefix,
- sessionData,
- });
-
- const promises = [];
- for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) {
- if (!this._shouldHandleWorker(dbg)) {
- continue;
- }
- promises.push(
- this._createWorkerTargetActor({
- dbg,
- connection,
- forwardingPrefix,
- watcherActorID,
- })
- );
- }
- await Promise.all(promises);
- }
-
- _createConnection(forwardingPrefix) {
- const { DevToolsServer } = lazy.Loader.require(
- "resource://devtools/server/devtools-server.js"
- );
-
- DevToolsServer.init();
-
- // We want a special server without any root actor and only target-scoped actors.
- // We are going to spawn a WorkerTargetActor instance in the next few lines,
- // it is going to act like a root actor without being one.
- DevToolsServer.registerActors({ target: true });
-
- const connection = DevToolsServer.connectToParentWindowActor(
- this,
- forwardingPrefix
- );
-
- return connection;
- }
-
- /**
- * Indicates whether or not we should handle the worker debugger
- *
- * @param {WorkerDebugger} dbg: The worker debugger we want to check.
- * @returns {Boolean}
- */
- _shouldHandleWorker(dbg) {
- // We only want to create targets for non-closed dedicated worker, in the same document
- return (
- lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg) &&
- dbg.type === Ci.nsIWorkerDebugger.TYPE_DEDICATED &&
- dbg.windowIDs.includes(this.manager.innerWindowId)
- );
- }
-
- async _createWorkerTargetActor({
- dbg,
- connection,
- forwardingPrefix,
- watcherActorID,
- }) {
- // Prevent the debuggee from executing in this worker until the client has
- // finished attaching to it. This call will throw if the debugger is already "registered"
- // (i.e. if this is called outside of the register listener)
- // See https://searchfox.org/mozilla-central/rev/84922363f4014eae684aabc4f1d06380066494c5/dom/workers/nsIWorkerDebugger.idl#55-66
- try {
- dbg.setDebuggerReady(false);
- } catch (e) {}
-
- const watcherConnectionData = this._connections.get(watcherActorID);
- const { sessionData } = watcherConnectionData;
- const workerThreadServerForwardingPrefix =
- connection.allocID("workerTarget");
-
- // Create the actual worker target actor, in the worker thread.
- const { connectToWorker } = lazy.Loader.require(
- "resource://devtools/server/connectors/worker-connector.js"
- );
-
- const onConnectToWorker = connectToWorker(
- connection,
- dbg,
- workerThreadServerForwardingPrefix,
- {
- sessionData,
- sessionContext: sessionData.sessionContext,
- }
- );
-
- try {
- await onConnectToWorker;
- } catch (e) {
- // onConnectToWorker can reject if the Worker Debugger is closed; so we only want to
- // resume the debugger if it is not closed (otherwise it can cause crashes).
- if (!dbg.isClosed) {
- dbg.setDebuggerReady(true);
- }
- return;
- }
-
- const { workerTargetForm, transport } = await onConnectToWorker;
-
- try {
- this.sendAsyncMessage("DevToolsWorkerChild:workerTargetAvailable", {
- watcherActorID,
- forwardingPrefix,
- workerTargetForm,
- });
- } catch (e) {
- // If there was an error while sending the message, we are not going to use this
- // connection to communicate with the worker.
- transport.close();
- return;
- }
-
- // Only add data to the connection if we successfully send the
- // workerTargetAvailable message.
- watcherConnectionData.workers.push({
- dbg,
- transport,
- workerTargetForm,
- workerThreadServerForwardingPrefix,
- });
- }
-
- _destroyTargetActors(watcherActorID) {
- const watcherConnectionData = this._connections.get(watcherActorID);
- this._connections.delete(watcherActorID);
-
- // This connection has already been cleaned?
- if (!watcherConnectionData) {
- console.error(
- `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
- );
- return;
- }
-
- for (const {
- dbg,
- transport,
- workerThreadServerForwardingPrefix,
- } of watcherConnectionData.workers) {
- try {
- if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
- dbg.postMessage(
- JSON.stringify({
- type: "disconnect",
- forwardingPrefix: workerThreadServerForwardingPrefix,
- })
- );
- }
- } catch (e) {}
-
- transport.close();
- }
-
- watcherConnectionData.connection.close();
- }
-
- async sendPacket(packet, prefix) {
- return this.sendAsyncMessage("DevToolsWorkerChild:packet", {
- packet,
- prefix,
- });
- }
-
- async _addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) {
- const watcherConnectionData = this._connections.get(watcherActorID);
- if (!watcherConnectionData) {
- return;
- }
-
- lazy.SessionDataHelpers.addOrSetSessionDataEntry(
- watcherConnectionData.sessionData,
- type,
- entries,
- updateType
- );
-
- const promises = [];
- for (const {
- dbg,
- workerThreadServerForwardingPrefix,
- } of watcherConnectionData.workers) {
- promises.push(
- addOrSetSessionDataEntryInWorkerTarget({
- dbg,
- workerThreadServerForwardingPrefix,
- type,
- entries,
- updateType,
- })
- );
- }
- await Promise.all(promises);
- }
-
- _removeSessionDataEntry(watcherActorID, type, entries) {
- const watcherConnectionData = this._connections.get(watcherActorID);
-
- if (!watcherConnectionData) {
- return;
- }
-
- lazy.SessionDataHelpers.removeSessionDataEntry(
- watcherConnectionData.sessionData,
- type,
- entries
- );
-
- for (const {
- dbg,
- workerThreadServerForwardingPrefix,
- } of watcherConnectionData.workers) {
- if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
- dbg.postMessage(
- JSON.stringify({
- type: "remove-session-data-entry",
- forwardingPrefix: workerThreadServerForwardingPrefix,
- dataEntryType: type,
- entries,
- })
- );
- }
- }
- }
-
- handleEvent({ type }) {
- // DOMWindowCreated is registered from the WatcherRegistry via `ActorManagerParent.addJSWindowActors`
- // as a DOM event to be listened to and so is fired by JSWindowActor platform code.
- if (type == "DOMWindowCreated") {
- this.onDOMWindowCreated();
- }
- }
-
- _removeExistingWorkerDebuggerListener() {
- if (this._workerDebuggerListener) {
- lazy.wdm.removeListener(this._workerDebuggerListener);
- this._workerDebuggerListener = null;
- }
- }
-
- /**
- * Part of JSActor API
- * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
- *
- * > The didDestroy method, if present, will be called after the actor is no
- * > longer able to receive any more messages.
- */
- didDestroy() {
- this._removeExistingWorkerDebuggerListener();
-
- for (const [watcherActorID, watcherConnectionData] of this._connections) {
- const { connection } = watcherConnectionData;
- this._destroyTargetActors(watcherActorID);
-
- connection.close();
- }
-
- this._connections.clear();
- }
-}
-
-/**
- * Communicate the type and entries to the Worker Target actor, via the WorkerDebugger.
- *
- * @param {WorkerDebugger} dbg
- * @param {String} workerThreadServerForwardingPrefix
- * @param {String} type
- * Session data type name
- * @param {Array} entries
- * Session data entries to add or set.
- * @param {String} updateType
- * Either "add" or "set", to control if we should only add some items,
- * or replace the whole data set with the new entries.
- * @returns {Promise} Returns a Promise that resolves once the data entry were handled
- * by the worker target.
- */
-function addOrSetSessionDataEntryInWorkerTarget({
- dbg,
- workerThreadServerForwardingPrefix,
- type,
- entries,
- updateType,
-}) {
- if (!lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
- return Promise.resolve();
- }
-
- return new Promise(resolve => {
- // Wait until we're notified by the worker that the resources are watched.
- // This is important so we know existing resources were handled.
- const listener = {
- onMessage: message => {
- message = JSON.parse(message);
- if (message.type === "session-data-entry-added-or-set") {
- resolve();
- dbg.removeListener(listener);
- }
- },
- // Resolve if the worker is being destroyed so we don't have a dangling promise.
- onClose: () => resolve(),
- };
-
- dbg.addListener(listener);
-
- dbg.postMessage(
- JSON.stringify({
- type: "add-or-set-session-data-entry",
- forwardingPrefix: workerThreadServerForwardingPrefix,
- dataEntryType: type,
- entries,
- updateType,
- })
- );
- });
-}
diff --git a/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs
deleted file mode 100644
index cb9bffc2ca..0000000000
--- a/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs
+++ /dev/null
@@ -1,294 +0,0 @@
-/* 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/. */
-
-import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs";
-import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
-
-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.
- { global: "shared" }
-);
-
-const lazy = {};
-
-loader.lazyRequireGetter(
- lazy,
- "JsWindowActorTransport",
- "resource://devtools/shared/transport/js-window-actor-transport.js",
- true
-);
-
-export class DevToolsWorkerParent extends JSWindowActorParent {
- constructor() {
- super();
-
- this._destroyed = false;
-
- // Map of DevToolsServerConnection's used to forward the messages from/to
- // the client. The connections run in the parent process, as this code. We
- // may have more than one when there is more than one client debugging the
- // same worker. For example, a content toolbox and the browser toolbox.
- //
- // The map is indexed by the connection prefix, and the values are object with the
- // following properties:
- // - watcher: The WatcherActor
- // - actors: A Map of the worker target actors form, indexed by WorkerTarget actorID
- // - transport: the JsWindowActorTransport
- //
- // Reminder about prefixes: all DevToolsServerConnections have a `prefix`
- // which can be considered as a kind of id. On top of this, parent process
- // DevToolsServerConnections also have forwarding prefixes because they are
- // responsible for forwarding messages to content process connections.
- this._connections = new Map();
-
- this._onConnectionClosed = this._onConnectionClosed.bind(this);
- EventEmitter.decorate(this);
- }
-
- /**
- * Request the content process to create Worker Targets if workers matching the context
- * are already available.
- */
- async instantiateWorkerTargets({
- watcherActorID,
- connectionPrefix,
- sessionContext,
- sessionData,
- }) {
- try {
- await this.sendQuery(
- "DevToolsWorkerParent:instantiate-already-available",
- {
- watcherActorID,
- connectionPrefix,
- sessionContext,
- sessionData,
- }
- );
- } catch (e) {
- console.warn(
- "Failed to create DevTools Worker target for browsingContext",
- this.browsingContext.id,
- "and watcher actor id",
- watcherActorID
- );
- console.warn(e);
- }
- }
-
- destroyWorkerTargets({ watcherActorID, sessionContext }) {
- return this.sendAsyncMessage("DevToolsWorkerParent:destroy", {
- watcherActorID,
- sessionContext,
- });
- }
-
- /**
- * Communicate to the content process that some data have been added.
- */
- async addOrSetSessionDataEntry({
- watcherActorID,
- sessionContext,
- type,
- entries,
- updateType,
- }) {
- try {
- await this.sendQuery("DevToolsWorkerParent:addOrSetSessionDataEntry", {
- watcherActorID,
- sessionContext,
- type,
- entries,
- updateType,
- });
- } catch (e) {
- console.warn(
- "Failed to add session data entry for worker targets in browsing context",
- this.browsingContext.id,
- "and watcher actor id",
- watcherActorID
- );
- console.warn(e);
- }
- }
-
- /**
- * Communicate to the content process that some data have been removed.
- */
- removeSessionDataEntry({ watcherActorID, sessionContext, type, entries }) {
- this.sendAsyncMessage("DevToolsWorkerParent:removeSessionDataEntry", {
- watcherActorID,
- sessionContext,
- type,
- entries,
- });
- }
-
- workerTargetAvailable({
- watcherActorID,
- forwardingPrefix,
- workerTargetForm,
- }) {
- if (this._destroyed) {
- return;
- }
-
- const watcher = WatcherRegistry.getWatcher(watcherActorID);
-
- if (!watcher) {
- throw new Error(
- `Watcher Actor with ID '${watcherActorID}' can't be found.`
- );
- }
-
- const connection = watcher.conn;
- const { prefix } = connection;
- if (!this._connections.has(prefix)) {
- connection.on("closed", this._onConnectionClosed);
-
- // Create a js-window-actor based transport.
- const transport = new lazy.JsWindowActorTransport(this, forwardingPrefix);
- transport.hooks = {
- onPacket: connection.send.bind(connection),
- onTransportClosed() {},
- };
- transport.ready();
-
- connection.setForwarding(forwardingPrefix, transport);
-
- this._connections.set(prefix, {
- watcher,
- transport,
- actors: new Map(),
- });
- }
-
- const workerTargetActorId = workerTargetForm.actor;
- this._connections
- .get(prefix)
- .actors.set(workerTargetActorId, workerTargetForm);
- watcher.notifyTargetAvailable(workerTargetForm);
- }
-
- workerTargetDestroyed({ watcherActorID, workerTargetForm }) {
- const watcher = WatcherRegistry.getWatcher(watcherActorID);
-
- if (!watcher) {
- throw new Error(
- `Watcher Actor with ID '${watcherActorID}' can't be found.`
- );
- }
-
- const connection = watcher.conn;
- const { prefix } = connection;
- if (!this._connections.has(prefix)) {
- return;
- }
-
- const workerTargetActorId = workerTargetForm.actor;
- const { actors } = this._connections.get(prefix);
- if (!actors.has(workerTargetActorId)) {
- return;
- }
-
- actors.delete(workerTargetActorId);
- watcher.notifyTargetDestroyed(workerTargetForm);
- }
-
- _onConnectionClosed(status, prefix) {
- this._unregisterWatcher(prefix);
- }
-
- async _unregisterWatcher(connectionPrefix) {
- const connectionInfo = this._connections.get(connectionPrefix);
- if (!connectionInfo) {
- return;
- }
-
- const { watcher, transport } = connectionInfo;
- const connection = watcher.conn;
-
- connection.off("closed", this._onConnectionClosed);
- if (transport) {
- // If we have a child transport, the actor has already
- // been created. We need to stop using this transport.
- connection.cancelForwarding(transport._prefix);
- transport.close();
- }
-
- this._connections.delete(connectionPrefix);
-
- if (!this._connections.size) {
- this._destroy();
- }
- }
-
- _destroy() {
- if (this._destroyed) {
- return;
- }
- this._destroyed = true;
-
- for (const { actors, watcher } of this._connections.values()) {
- for (const actor of actors.values()) {
- watcher.notifyTargetDestroyed(actor);
- }
-
- this._unregisterWatcher(watcher.conn.prefix);
- }
- }
-
- /**
- * Part of JSActor API
- * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
- *
- * > The didDestroy method, if present, will be called after the (JSWindow)actor is no
- * > longer able to receive any more messages.
- */
- didDestroy() {
- this._destroy();
- }
-
- /**
- * Supported Queries
- */
-
- async sendPacket(packet, prefix) {
- return this.sendAsyncMessage("DevToolsWorkerParent:packet", {
- packet,
- prefix,
- });
- }
-
- /**
- * JsWindowActor API
- */
-
- async sendQuery(msg, args) {
- try {
- const res = await super.sendQuery(msg, args);
- return res;
- } catch (e) {
- console.error("Failed to sendQuery in DevToolsWorkerParent", msg, e);
- throw e;
- }
- }
-
- receiveMessage(message) {
- switch (message.name) {
- case "DevToolsWorkerChild:workerTargetAvailable":
- return this.workerTargetAvailable(message.data);
- case "DevToolsWorkerChild:workerTargetDestroyed":
- return this.workerTargetDestroyed(message.data);
- case "DevToolsWorkerChild:packet":
- return this.emit("packet-received", message);
- default:
- throw new Error(
- "Unsupported message in DevToolsWorkerParent: " + message.name
- );
- }
- }
-}
diff --git a/devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs b/devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs
deleted file mode 100644
index ae15c030fe..0000000000
--- a/devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs
+++ /dev/null
@@ -1,76 +0,0 @@
-/* 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/. */
-
-function getWindowGlobalUri(windowGlobal) {
- let windowGlobalUri = "";
-
- if (windowGlobal.documentURI) {
- // If windowGlobal is a WindowGlobalParent documentURI should be available.
- windowGlobalUri = windowGlobal.documentURI.spec;
- } else if (windowGlobal.browsingContext?.window) {
- // If windowGlobal is a WindowGlobalChild, this code runs in the same
- // process as the document and we can directly access the window.location
- // object.
- windowGlobalUri = windowGlobal.browsingContext.window.location.href;
- if (!windowGlobalUri) {
- windowGlobalUri =
- windowGlobal.browsingContext.window.document.documentURI;
- }
- }
-
- return windowGlobalUri;
-}
-
-export const WindowGlobalLogger = {
- /**
- * This logger can run from the content or parent process, and windowGlobal
- * will either be of type `WindowGlobalParent` or `WindowGlobalChild`.
- *
- * The interface for each type can be found in WindowGlobalActors.webidl
- * (https://searchfox.org/mozilla-central/source/dom/chrome-webidl/WindowGlobalActors.webidl)
- *
- * @param {WindowGlobalParent|WindowGlobalChild} windowGlobal
- * The window global to log. See WindowGlobalActors.webidl for details
- * about the types.
- * @param {String} message
- * A custom message that will be displayed at the beginning of the log.
- */
- logWindowGlobal(windowGlobal, message) {
- const { browsingContext } = windowGlobal;
- const { parent } = browsingContext;
- const windowGlobalUri = getWindowGlobalUri(windowGlobal);
- const isInitialDocument =
- "isInitialDocument" in windowGlobal
- ? windowGlobal.isInitialDocument
- : windowGlobal.browsingContext.window?.document.isInitialDocument;
-
- const details = [];
- details.push(
- "BrowsingContext.browserId: " + browsingContext.browserId,
- "BrowsingContext.id: " + browsingContext.id,
- "innerWindowId: " + windowGlobal.innerWindowId,
- "opener.id: " + browsingContext.opener?.id,
- "pid: " + windowGlobal.osPid,
- "isClosed: " + windowGlobal.isClosed,
- "isInProcess: " + windowGlobal.isInProcess,
- "isCurrentGlobal: " + windowGlobal.isCurrentGlobal,
- "isProcessRoot: " + windowGlobal.isProcessRoot,
- "currentRemoteType: " + browsingContext.currentRemoteType,
- "hasParent: " + (parent ? parent.id : "no"),
- "uri: " + (windowGlobalUri ? windowGlobalUri : "no uri"),
- "isProcessRoot: " + windowGlobal.isProcessRoot,
- "BrowsingContext.isContent: " + windowGlobal.browsingContext.isContent,
- "isInitialDocument: " + isInitialDocument
- );
-
- const header = "[WindowGlobalLogger] " + message;
-
- // Use a padding for multiline display.
- const padding = " ";
- const formattedDetails = details.map(s => padding + s);
- const detailsString = formattedDetails.join("\n");
-
- dump(header + "\n" + detailsString + "\n");
- },
-};
diff --git a/devtools/server/connectors/js-window-actor/moz.build b/devtools/server/connectors/js-window-actor/moz.build
deleted file mode 100644
index faaaa8dd54..0000000000
--- a/devtools/server/connectors/js-window-actor/moz.build
+++ /dev/null
@@ -1,13 +0,0 @@
-# -*- 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(
- "DevToolsFrameChild.sys.mjs",
- "DevToolsFrameParent.sys.mjs",
- "DevToolsWorkerChild.sys.mjs",
- "DevToolsWorkerParent.sys.mjs",
- "WindowGlobalLogger.sys.mjs",
-)
diff --git a/devtools/server/connectors/moz.build b/devtools/server/connectors/moz.build
index fd4baf81ff..ba0f17f63c 100644
--- a/devtools/server/connectors/moz.build
+++ b/devtools/server/connectors/moz.build
@@ -6,8 +6,6 @@
DIRS += [
"js-process-actor",
- "js-window-actor",
- "process-actor",
]
DevToolsModules(
diff --git a/devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs b/devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs
deleted file mode 100644
index 2e461cbd03..0000000000
--- a/devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs
+++ /dev/null
@@ -1,741 +0,0 @@
-/* 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/. */
-
-import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
-import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
-
-const lazy = {};
-ChromeUtils.defineESModuleGetters(lazy, {
- loader: "resource://devtools/shared/loader/Loader.sys.mjs",
-});
-
-XPCOMUtils.defineLazyServiceGetter(
- lazy,
- "wdm",
- "@mozilla.org/dom/workers/workerdebuggermanager;1",
- "nsIWorkerDebuggerManager"
-);
-
-XPCOMUtils.defineLazyModuleGetters(lazy, {
- SessionDataHelpers:
- "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm",
-});
-
-ChromeUtils.defineLazyGetter(lazy, "DevToolsUtils", () =>
- lazy.loader.require("devtools/shared/DevToolsUtils")
-);
-
-// Name of the attribute into which we save data in `sharedData` object.
-const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
-
-export class DevToolsServiceWorkerChild 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
- // - workers: An array of object containing the following properties:
- // - dbg: A WorkerDebuggerInstance
- // - serviceWorkerTargetForm: The associated worker target instance form
- // - workerThreadServerForwardingPrefix: The prefix used to forward events to the
- // worker target on the worker thread ().
- // - forwardingPrefix: Prefix used by the JSWindowActorTransport pair to communicate
- // between content and parent processes.
- // - sessionData: Data (targets, resources, …) the watcher wants to be notified about.
- // See WatcherRegistry.getSessionData to see the full list of properties.
- this._connections = new Map();
-
- this._onConnectionChange = this._onConnectionChange.bind(this);
-
- EventEmitter.decorate(this);
- }
-
- /**
- * Called by nsIWorkerDebuggerManager when a worker get created.
- *
- * Go through all registered connections (in case we have more than one client connected)
- * to eventually instantiate a target actor for this worker.
- *
- * @param {nsIWorkerDebugger} dbg
- */
- _onWorkerRegistered(dbg) {
- // Only consider service workers
- if (dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) {
- return;
- }
-
- for (const [
- watcherActorID,
- { connection, forwardingPrefix, sessionData },
- ] of this._connections) {
- if (this._shouldHandleWorker(sessionData, dbg)) {
- this._createWorkerTargetActor({
- dbg,
- connection,
- forwardingPrefix,
- watcherActorID,
- });
- }
- }
- }
-
- /**
- * Called by nsIWorkerDebuggerManager when a worker get destroyed.
- *
- * Go through all registered connections (in case we have more than one client connected)
- * to destroy the related target which may have been created for this worker.
- *
- * @param {nsIWorkerDebugger} dbg
- */
- _onWorkerUnregistered(dbg) {
- // Only consider service workers
- if (dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) {
- return;
- }
-
- for (const [watcherActorID, watcherConnectionData] of this._connections) {
- this._destroyServiceWorkerTargetForWatcher(
- watcherActorID,
- watcherConnectionData,
- dbg
- );
- }
- }
-
- /**
- * To be called when we know a Service Worker target should be destroyed for a specific connection
- * for which we pass the related "watcher connection data".
- *
- * @param {String} watcherActorID
- * Watcher actor ID for which we should unregister this service worker.
- * @param {Object} watcherConnectionData
- * The metadata object for a given watcher, stored in the _connections Map.
- * @param {nsIWorkerDebugger} dbg
- */
- _destroyServiceWorkerTargetForWatcher(
- watcherActorID,
- watcherConnectionData,
- dbg
- ) {
- const { workers, forwardingPrefix } = watcherConnectionData;
-
- // Check if the worker registration was handled for this watcher.
- const unregisteredActorIndex = workers.findIndex(worker => {
- try {
- // Accessing the WorkerDebugger id might throw (NS_ERROR_UNEXPECTED).
- return worker.dbg.id === dbg.id;
- } catch (e) {
- return false;
- }
- });
-
- // Ignore this worker if it wasn't registered for this watcher.
- if (unregisteredActorIndex === -1) {
- return;
- }
-
- const { serviceWorkerTargetForm, transport } =
- workers[unregisteredActorIndex];
-
- // Remove the entry from this._connection dictionnary
- workers.splice(unregisteredActorIndex, 1);
-
- // Close the transport made against the worker thread.
- transport.close();
-
- // Note that we do not need to post the "disconnect" message from this destruction codepath
- // as this method is only called when the worker is unregistered and so,
- // we can't send any message anyway, and the worker is being destroyed anyway.
-
- // Also notify the parent process that this worker target got destroyed.
- // As the worker thread may be already destroyed, it may not have time to send a destroy event.
- try {
- this.sendAsyncMessage(
- "DevToolsServiceWorkerChild:serviceWorkerTargetDestroyed",
- {
- watcherActorID,
- forwardingPrefix,
- serviceWorkerTargetForm,
- }
- );
- } catch (e) {
- // Ignore exception which may happen on content process destruction
- }
- }
-
- /**
- * Function handling messages sent by DevToolsServiceWorkerParent (part of ProcessActor API).
- *
- * @param {Object} message
- * @param {String} message.name
- * @param {*} message.data
- */
- receiveMessage(message) {
- switch (message.name) {
- case "DevToolsServiceWorkerParent:instantiate-already-available": {
- const { watcherActorID, connectionPrefix, sessionData } = message.data;
- return this._watchWorkerTargets({
- watcherActorID,
- parentConnectionPrefix: connectionPrefix,
- sessionData,
- });
- }
- case "DevToolsServiceWorkerParent:destroy": {
- const { watcherActorID } = message.data;
- return this._destroyTargetActors(watcherActorID);
- }
- case "DevToolsServiceWorkerParent:addOrSetSessionDataEntry": {
- const { watcherActorID, type, entries, updateType } = message.data;
- return this._addOrSetSessionDataEntry(
- watcherActorID,
- type,
- entries,
- updateType
- );
- }
- case "DevToolsServiceWorkerParent:removeSessionDataEntry": {
- const { watcherActorID, type, entries } = message.data;
- return this._removeSessionDataEntry(watcherActorID, type, entries);
- }
- case "DevToolsServiceWorkerParent:packet":
- return this.emit("packet-received", message);
- default:
- throw new Error(
- "Unsupported message in DevToolsServiceWorkerParent: " + message.name
- );
- }
- }
-
- /**
- * "chrome-event-target-created" event handler. Supposed to be fired very early when the process starts
- */
- observe() {
- const { sharedData } = Services.cpmm;
- const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
- if (!sessionDataByWatcherActor) {
- throw new Error(
- "Request to instantiate the target(s) for the Service Worker, but `sharedData` is empty about watched targets"
- );
- }
-
- // Create one Target actor for each prefix/client which listen to workers
- for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
- const { targets, connectionPrefix } = sessionData;
- if (targets?.includes("service_worker")) {
- this._watchWorkerTargets({
- watcherActorID,
- parentConnectionPrefix: connectionPrefix,
- sessionData,
- });
- }
- }
- }
-
- /**
- * Instantiate targets for existing workers, watch for worker registration and listen
- * for resources on those workers, for given connection and context. Targets are sent
- * to the DevToolsServiceWorkerParent via the DevToolsServiceWorkerChild:serviceWorkerTargetAvailable message.
- *
- * @param {Object} options
- * @param {String} options.watcherActorID: The ID of the WatcherActor who requested to
- * observe and create these target actors.
- * @param {String} options.parentConnectionPrefix: The prefix of the DevToolsServerConnection
- * of the Watcher Actor. This is used to compute a unique ID for the target actor.
- * @param {Object} options.sessionData: Data (targets, resources, …) the watcher wants
- * to be notified about. See WatcherRegistry.getSessionData to see the full list
- * of properties.
- */
- async _watchWorkerTargets({
- watcherActorID,
- parentConnectionPrefix,
- sessionData,
- }) {
- // We might already have been called from observe method if the process was initializing
- if (this._connections.has(watcherActorID)) {
- // In such case, wait for the promise in order to ensure resolving only after
- // we notified about the existing targets
- await this._connections.get(watcherActorID).watchPromise;
- return;
- }
-
- // Compute a unique prefix, just for this Service Worker,
- // 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
- // WindowGlobalChild's innerWindowId should be unique across processes, so it should be safe?
- // (this.manager == WindowGlobalChild interface)
- const forwardingPrefix =
- parentConnectionPrefix + "serviceWorkerProcess" + this.manager.childID;
-
- const connection = this._createConnection(forwardingPrefix);
-
- // This method will be concurrently called from `observe()` and `DevToolsServiceWorkerParent:instantiate-already-available`
- // When the JSprocessActor initializes itself and when the watcher want to force instantiating existing targets.
- // Wait for the existing promise when the second call arise.
- //
- // Also, _connections has to be populated *before* calling _createWorkerTargetActor,
- // so create a deferred promise right away.
- let resolveWatchPromise;
- const watchPromise = new Promise(
- resolve => (resolveWatchPromise = resolve)
- );
-
- this._connections.set(watcherActorID, {
- connection,
- watchPromise,
- workers: [],
- forwardingPrefix,
- sessionData,
- });
-
- // Listen for new workers that will be spawned.
- if (!this._workerDebuggerListener) {
- this._workerDebuggerListener = {
- onRegister: this._onWorkerRegistered.bind(this),
- onUnregister: this._onWorkerUnregistered.bind(this),
- };
- lazy.wdm.addListener(this._workerDebuggerListener);
- }
-
- const promises = [];
- for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) {
- if (!this._shouldHandleWorker(sessionData, dbg)) {
- continue;
- }
- promises.push(
- this._createWorkerTargetActor({
- dbg,
- connection,
- forwardingPrefix,
- watcherActorID,
- })
- );
- }
- await Promise.all(promises);
- resolveWatchPromise();
- }
-
- /**
- * Initialize a DevTools Server and return a new DevToolsServerConnection
- * using this server in order to communicate to the parent process via
- * the JSProcessActor message / queries.
- *
- * @param String forwardingPrefix
- * A unique prefix used to distinguish message coming from distinct service workers.
- * @return DevToolsServerConnection
- * A connection to communicate with the parent process.
- */
- _createConnection(forwardingPrefix) {
- const { DevToolsServer } = lazy.loader.require(
- "devtools/server/devtools-server"
- );
-
- DevToolsServer.init();
-
- // We want a special server without any root actor and only target-scoped actors.
- // We are going to spawn a WorkerTargetActor instance in the next few lines,
- // it is going to act like a root actor without being one.
- DevToolsServer.registerActors({ target: true });
- DevToolsServer.on("connectionchange", this._onConnectionChange);
-
- const connection = DevToolsServer.connectToParentWindowActor(
- this,
- forwardingPrefix
- );
-
- return connection;
- }
-
- /**
- * Indicates whether or not we should handle the worker debugger for a given
- * watcher's session data.
- *
- * @param {Object} sessionData
- * The session data for a given watcher, which includes metadata
- * about the debugged context.
- * @param {WorkerDebugger} dbg
- * The worker debugger we want to check.
- *
- * @returns {Boolean}
- */
- _shouldHandleWorker(sessionData, dbg) {
- if (dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) {
- return false;
- }
- // We only want to create targets for non-closed service worker
- if (!lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
- return false;
- }
-
- // Accessing `nsIPrincipal.host` may easily throw on non-http URLs.
- // Ignore all non-HTTP as they most likely don't have any valid host name.
- if (!dbg.principal.scheme.startsWith("http")) {
- return false;
- }
-
- const workerHost = dbg.principal.hostPort;
- return workerHost == sessionData["browser-element-host"][0];
- }
-
- async _createWorkerTargetActor({
- dbg,
- connection,
- forwardingPrefix,
- watcherActorID,
- }) {
- // Freeze the worker execution as soon as possible in order to wait for DevTools bootstrap.
- // We typically want to:
- // - startup the Thread Actor,
- // - pass the initial session data which includes breakpoints to the worker thread,
- // - register the breakpoints,
- // before release its execution.
- // `connectToWorker` is going to call setDebuggerReady(true) when all of this is done.
- try {
- dbg.setDebuggerReady(false);
- } catch (e) {
- // This call will throw if the debugger is already "registered"
- // (i.e. if this is called outside of the register listener)
- // See https://searchfox.org/mozilla-central/rev/84922363f4014eae684aabc4f1d06380066494c5/dom/workers/nsIWorkerDebugger.idl#55-66
- }
-
- const watcherConnectionData = this._connections.get(watcherActorID);
- const { sessionData } = watcherConnectionData;
- const workerThreadServerForwardingPrefix = connection.allocID(
- "serviceWorkerTarget"
- );
-
- // Create the actual worker target actor, in the worker thread.
- const { connectToWorker } = lazy.loader.require(
- "devtools/server/connectors/worker-connector"
- );
-
- const onConnectToWorker = connectToWorker(
- connection,
- dbg,
- workerThreadServerForwardingPrefix,
- {
- sessionData,
- sessionContext: sessionData.sessionContext,
- }
- );
-
- try {
- await onConnectToWorker;
- } catch (e) {
- // connectToWorker is supposed to call setDebuggerReady(true) to release the worker execution.
- // But if anything goes wrong and an exception is thrown, ensure releasing its execution,
- // otherwise if devtools is broken, it will freeze the worker indefinitely.
- //
- // onConnectToWorker can reject if the Worker Debugger is closed; so we only want to
- // resume the debugger if it is not closed (otherwise it can cause crashes).
- if (!dbg.isClosed) {
- dbg.setDebuggerReady(true);
- }
- return;
- }
-
- const { workerTargetForm, transport } = await onConnectToWorker;
-
- try {
- this.sendAsyncMessage(
- "DevToolsServiceWorkerChild:serviceWorkerTargetAvailable",
- {
- watcherActorID,
- forwardingPrefix,
- serviceWorkerTargetForm: workerTargetForm,
- }
- );
- } catch (e) {
- // If there was an error while sending the message, we are not going to use this
- // connection to communicate with the worker.
- transport.close();
- return;
- }
-
- // Only add data to the connection if we successfully send the
- // serviceWorkerTargetAvailable message.
- watcherConnectionData.workers.push({
- dbg,
- transport,
- serviceWorkerTargetForm: workerTargetForm,
- workerThreadServerForwardingPrefix,
- });
- }
-
- /**
- * Request the service worker threads to destroy all their service worker Targets currently registered for a given Watcher actor.
- *
- * @param {String} watcherActorID
- */
- _destroyTargetActors(watcherActorID) {
- const watcherConnectionData = this._connections.get(watcherActorID);
- this._connections.delete(watcherActorID);
-
- // This connection has already been cleaned?
- if (!watcherConnectionData) {
- console.error(
- `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
- );
- return;
- }
-
- for (const {
- dbg,
- transport,
- workerThreadServerForwardingPrefix,
- } of watcherConnectionData.workers) {
- try {
- if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
- dbg.postMessage(
- JSON.stringify({
- type: "disconnect",
- forwardingPrefix: workerThreadServerForwardingPrefix,
- })
- );
- }
- } catch (e) {}
-
- transport.close();
- }
-
- watcherConnectionData.connection.close();
- }
-
- /**
- * Destroy the server once its last connection closes. Note that multiple
- * worker scripts may be running in parallel and reuse the same server.
- */
- _onConnectionChange() {
- const { DevToolsServer } = lazy.loader.require(
- "devtools/server/devtools-server"
- );
-
- // 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;
- }
-
- if (this._destroyed) {
- return;
- }
- this._destroyed = true;
-
- DevToolsServer.off("connectionchange", this._onConnectionChange);
- DevToolsServer.destroy();
- }
-
- /**
- * Used by DevTools transport layer to communicate with the parent process.
- *
- * @param {String} packet
- * @param {String prefix
- */
- async sendPacket(packet, prefix) {
- return this.sendAsyncMessage("DevToolsServiceWorkerChild:packet", {
- packet,
- prefix,
- });
- }
-
- /**
- * Go through all registered service workers for a given watcher actor
- * to send them new session data entries.
- *
- * See addOrSetSessionDataEntryInWorkerTarget for more info about arguments.
- */
- async _addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) {
- const watcherConnectionData = this._connections.get(watcherActorID);
- if (!watcherConnectionData) {
- return;
- }
-
- lazy.SessionDataHelpers.addOrSetSessionDataEntry(
- watcherConnectionData.sessionData,
- type,
- entries,
- updateType
- );
-
- // This type is really specific to Service Workers and doesn't need to be transferred to the worker threads.
- // We only need to instantiate and destroy the target actors based on this new host.
- if (type == "browser-element-host") {
- this.updateBrowserElementHost(watcherActorID, watcherConnectionData);
- return;
- }
-
- const promises = [];
- for (const {
- dbg,
- workerThreadServerForwardingPrefix,
- } of watcherConnectionData.workers) {
- promises.push(
- addOrSetSessionDataEntryInWorkerTarget({
- dbg,
- workerThreadServerForwardingPrefix,
- type,
- entries,
- updateType,
- })
- );
- }
- await Promise.all(promises);
- }
-
- /**
- * Called whenever the debugged browser element navigates to a new page
- * and the URL's host changes.
- * This is used to maintain the list of active Service Worker targets
- * based on that host name.
- *
- * @param {String} watcherActorID
- * Watcher actor ID for which we should unregister this service worker.
- * @param {Object} watcherConnectionData
- * The metadata object for a given watcher, stored in the _connections Map.
- */
- async updateBrowserElementHost(watcherActorID, watcherConnectionData) {
- const { sessionData, connection, forwardingPrefix } = watcherConnectionData;
-
- // Create target actor matching this new host.
- // Note that we may be navigating to the same host name and the target will already exist.
- const dbgToInstantiate = [];
- for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) {
- const alreadyCreated = watcherConnectionData.workers.some(
- info => info.dbg === dbg
- );
- if (this._shouldHandleWorker(sessionData, dbg) && !alreadyCreated) {
- dbgToInstantiate.push(dbg);
- }
- }
- await Promise.all(
- dbgToInstantiate.map(dbg => {
- return this._createWorkerTargetActor({
- dbg,
- connection,
- forwardingPrefix,
- watcherActorID,
- });
- })
- );
- }
-
- /**
- * Go through all registered service workers for a given watcher actor
- * to send them request to clear some session data entries.
- *
- * See addOrSetSessionDataEntryInWorkerTarget for more info about arguments.
- */
- _removeSessionDataEntry(watcherActorID, type, entries) {
- const watcherConnectionData = this._connections.get(watcherActorID);
-
- if (!watcherConnectionData) {
- return;
- }
-
- lazy.SessionDataHelpers.removeSessionDataEntry(
- watcherConnectionData.sessionData,
- type,
- entries
- );
-
- for (const {
- dbg,
- workerThreadServerForwardingPrefix,
- } of watcherConnectionData.workers) {
- if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
- dbg.postMessage(
- JSON.stringify({
- type: "remove-session-data-entry",
- forwardingPrefix: workerThreadServerForwardingPrefix,
- dataEntryType: type,
- entries,
- })
- );
- }
- }
- }
-
- _removeExistingWorkerDebuggerListener() {
- if (this._workerDebuggerListener) {
- lazy.wdm.removeListener(this._workerDebuggerListener);
- this._workerDebuggerListener = null;
- }
- }
-
- /**
- * Part of JSActor API
- * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
- *
- * > The didDestroy method, if present, will be called after the actor is no
- * > longer able to receive any more messages.
- */
- didDestroy() {
- this._removeExistingWorkerDebuggerListener();
-
- for (const [watcherActorID, watcherConnectionData] of this._connections) {
- const { connection } = watcherConnectionData;
- this._destroyTargetActors(watcherActorID);
-
- connection.close();
- }
-
- this._connections.clear();
- }
-}
-
-/**
- * Communicate the type and entries to the Worker Target actor, via the WorkerDebugger.
- *
- * @param {WorkerDebugger} dbg
- * @param {String} workerThreadServerForwardingPrefix
- * @param {String} type
- * Session data type name
- * @param {Array} entries
- * Session data entries to add or set.
- * @param {String} updateType
- * Either "add" or "set", to control if we should only add some items,
- * or replace the whole data set with the new entries.
- * @returns {Promise} Returns a Promise that resolves once the data entry were handled
- * by the worker target.
- */
-function addOrSetSessionDataEntryInWorkerTarget({
- dbg,
- workerThreadServerForwardingPrefix,
- type,
- entries,
- updateType,
-}) {
- if (!lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
- return Promise.resolve();
- }
-
- return new Promise(resolve => {
- // Wait until we're notified by the worker that the resources are watched.
- // This is important so we know existing resources were handled.
- const listener = {
- onMessage: message => {
- message = JSON.parse(message);
- if (message.type === "session-data-entry-added-or-set") {
- resolve();
- dbg.removeListener(listener);
- }
- },
- // Resolve if the worker is being destroyed so we don't have a dangling promise.
- onClose: () => resolve(),
- };
-
- dbg.addListener(listener);
-
- dbg.postMessage(
- JSON.stringify({
- type: "add-or-set-session-data-entry",
- forwardingPrefix: workerThreadServerForwardingPrefix,
- dataEntryType: type,
- entries,
- updateType,
- })
- );
- });
-}
diff --git a/devtools/server/connectors/process-actor/DevToolsServiceWorkerParent.sys.mjs b/devtools/server/connectors/process-actor/DevToolsServiceWorkerParent.sys.mjs
deleted file mode 100644
index 2073f47e76..0000000000
--- a/devtools/server/connectors/process-actor/DevToolsServiceWorkerParent.sys.mjs
+++ /dev/null
@@ -1,308 +0,0 @@
-/* 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/. */
-
-import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
-
-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.
- { global: "shared" }
-);
-
-const lazy = {};
-ChromeUtils.defineESModuleGetters(lazy, {
- loader: "resource://devtools/shared/loader/Loader.sys.mjs",
-});
-
-ChromeUtils.defineLazyGetter(
- lazy,
- "JsWindowActorTransport",
- () =>
- lazy.loader.require("devtools/shared/transport/js-window-actor-transport")
- .JsWindowActorTransport
-);
-
-export class DevToolsServiceWorkerParent extends JSProcessActorParent {
- constructor() {
- super();
-
- this._destroyed = false;
-
- // Map of DevToolsServerConnection's used to forward the messages from/to
- // the client. The connections run in the parent process, as this code. We
- // may have more than one when there is more than one client debugging the
- // same worker. For example, a content toolbox and the browser toolbox.
- //
- // The map is indexed by the connection prefix, and the values are object with the
- // following properties:
- // - watcher: The WatcherActor
- // - actors: A Map of the worker target actors form, indexed by WorkerTarget actorID
- // - transport: the JsWindowActorTransport
- //
- // Reminder about prefixes: all DevToolsServerConnections have a `prefix`
- // which can be considered as a kind of id. On top of this, parent process
- // DevToolsServerConnections also have forwarding prefixes because they are
- // responsible for forwarding messages to content process connections.
- this._connections = new Map();
-
- this._onConnectionClosed = this._onConnectionClosed.bind(this);
- EventEmitter.decorate(this);
- }
-
- /**
- * Request the content process to create Service Worker Targets if workers matching the context
- * are already available.
- */
- async instantiateServiceWorkerTargets({
- watcherActorID,
- connectionPrefix,
- sessionContext,
- sessionData,
- }) {
- try {
- await this.sendQuery(
- "DevToolsServiceWorkerParent:instantiate-already-available",
- {
- watcherActorID,
- connectionPrefix,
- sessionContext,
- sessionData,
- }
- );
- } catch (e) {
- console.warn(
- "Failed to create DevTools Service Worker target for process",
- this.manager.osPid,
- "and watcher actor id",
- watcherActorID
- );
- console.warn(e);
- }
- }
-
- destroyServiceWorkerTargets({ watcherActorID, sessionContext }) {
- return this.sendAsyncMessage("DevToolsServiceWorkerParent:destroy", {
- watcherActorID,
- sessionContext,
- });
- }
-
- /**
- * Communicate to the content process that some data have been added.
- */
- async addOrSetSessionDataEntry({
- watcherActorID,
- sessionContext,
- type,
- entries,
- updateType,
- }) {
- try {
- await this.sendQuery(
- "DevToolsServiceWorkerParent:addOrSetSessionDataEntry",
- {
- watcherActorID,
- sessionContext,
- type,
- entries,
- updateType,
- }
- );
- } catch (e) {
- console.warn(
- "Failed to add session data entry for worker targets in process",
- this.manager.osPid,
- "and watcher actor id",
- watcherActorID
- );
- console.warn(e);
- }
- }
-
- /**
- * Communicate to the content process that some data have been removed.
- */
- removeSessionDataEntry({ watcherActorID, sessionContext, type, entries }) {
- this.sendAsyncMessage(
- "DevToolsServiceWorkerParent:removeSessionDataEntry",
- {
- watcherActorID,
- sessionContext,
- type,
- entries,
- }
- );
- }
-
- serviceWorkerTargetAvailable({
- watcherActorID,
- forwardingPrefix,
- serviceWorkerTargetForm,
- }) {
- if (this._destroyed) {
- return;
- }
-
- const watcher = WatcherRegistry.getWatcher(watcherActorID);
-
- if (!watcher) {
- throw new Error(
- `Watcher Actor with ID '${watcherActorID}' can't be found.`
- );
- }
-
- const connection = watcher.conn;
- const { prefix } = connection;
- if (!this._connections.has(prefix)) {
- connection.on("closed", this._onConnectionClosed);
-
- // Create a js-window-actor based transport.
- const transport = new lazy.JsWindowActorTransport(this, forwardingPrefix);
- transport.hooks = {
- onPacket: connection.send.bind(connection),
- onTransportClosed() {},
- };
- transport.ready();
-
- connection.setForwarding(forwardingPrefix, transport);
-
- this._connections.set(prefix, {
- connection,
- watcher,
- transport,
- actors: new Map(),
- });
- }
-
- const serviceWorkerTargetActorId = serviceWorkerTargetForm.actor;
- this._connections
- .get(prefix)
- .actors.set(serviceWorkerTargetActorId, serviceWorkerTargetForm);
- watcher.notifyTargetAvailable(serviceWorkerTargetForm);
- }
-
- serviceWorkerTargetDestroyed({ watcherActorID, serviceWorkerTargetForm }) {
- const watcher = WatcherRegistry.getWatcher(watcherActorID);
-
- if (!watcher) {
- throw new Error(
- `Watcher Actor with ID '${watcherActorID}' can't be found.`
- );
- }
-
- const connection = watcher.conn;
- const { prefix } = connection;
- if (!this._connections.has(prefix)) {
- return;
- }
-
- const serviceWorkerTargetActorId = serviceWorkerTargetForm.actor;
- const { actors } = this._connections.get(prefix);
- if (!actors.has(serviceWorkerTargetActorId)) {
- return;
- }
-
- actors.delete(serviceWorkerTargetActorId);
- watcher.notifyTargetDestroyed(serviceWorkerTargetForm);
- }
-
- _onConnectionClosed(status, prefix) {
- if (this._connections.has(prefix)) {
- const { connection } = this._connections.get(prefix);
- this._cleanupConnection(connection);
- }
- }
-
- async _cleanupConnection(connection) {
- if (!this._connections || !this._connections.has(connection.prefix)) {
- return;
- }
-
- const { transport } = this._connections.get(connection.prefix);
-
- connection.off("closed", this._onConnectionClosed);
- if (transport) {
- // If we have a child transport, the actor has already
- // been created. We need to stop using this transport.
- connection.cancelForwarding(transport._prefix);
- transport.close();
- }
-
- this._connections.delete(connection.prefix);
- if (!this._connections.size) {
- this._destroy();
- }
- }
-
- _destroy() {
- if (this._destroyed) {
- return;
- }
- this._destroyed = true;
-
- for (const { actors, watcher } of this._connections.values()) {
- for (const actor of actors.values()) {
- watcher.notifyTargetDestroyed(actor);
- }
-
- this._cleanupConnection(watcher.conn);
- }
- }
-
- /**
- * Part of JSActor API
- * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
- *
- * > The didDestroy method, if present, will be called after the ProcessActor is no
- * > longer able to receive any more messages.
- */
- didDestroy() {
- this._destroy();
- }
-
- /**
- * Supported Queries
- */
-
- async sendPacket(packet, prefix) {
- return this.sendAsyncMessage("DevToolsServiceWorkerParent:packet", {
- packet,
- prefix,
- });
- }
-
- /**
- * JsWindowActor API
- */
-
- async sendQuery(msg, args) {
- try {
- const res = await super.sendQuery(msg, args);
- return res;
- } catch (e) {
- console.error(
- "Failed to sendQuery in DevToolsServiceWorkerParent",
- msg,
- e
- );
- throw e;
- }
- }
-
- receiveMessage(message) {
- switch (message.name) {
- case "DevToolsServiceWorkerChild:serviceWorkerTargetAvailable":
- return this.serviceWorkerTargetAvailable(message.data);
- case "DevToolsServiceWorkerChild:serviceWorkerTargetDestroyed":
- return this.serviceWorkerTargetDestroyed(message.data);
- case "DevToolsServiceWorkerChild:packet":
- return this.emit("packet-received", message);
- default:
- throw new Error(
- "Unsupported message in DevToolsServiceWorkerParent: " + message.name
- );
- }
- }
-}
diff --git a/devtools/server/connectors/worker-connector.js b/devtools/server/connectors/worker-connector.js
index 90d55d7a69..dc72993d7a 100644
--- a/devtools/server/connectors/worker-connector.js
+++ b/devtools/server/connectors/worker-connector.js
@@ -46,9 +46,9 @@ function connectToWorker(connection, dbg, forwardingPrefix, options) {
onMessage: message => {
message = JSON.parse(message);
if (message.type !== "rpc") {
- if (message.type == "worker-thread-attached") {
- // The thread actor has finished attaching and can hit installed
- // breakpoints. Allow content to begin executing in the worker.
+ if (message.type == "session-data-processed") {
+ // The thread actor has finished processing session data, including breakpoints.
+ // Allow content to begin executing in the worker and possibly hit early breakpoints.
dbg.setDebuggerReady(true);
}
return;