summaryrefslogtreecommitdiffstats
path: root/devtools/server/connectors/js-process-actor/target-watchers
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/js-process-actor/target-watchers
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/js-process-actor/target-watchers')
-rw-r--r--devtools/server/connectors/js-process-actor/target-watchers/moz.build12
-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
5 files changed, 1189 insertions, 0 deletions
diff --git a/devtools/server/connectors/js-process-actor/target-watchers/moz.build b/devtools/server/connectors/js-process-actor/target-watchers/moz.build
new file mode 100644
index 0000000000..0574b0399e
--- /dev/null
+++ b/devtools/server/connectors/js-process-actor/target-watchers/moz.build
@@ -0,0 +1,12 @@
+# -*- 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(
+ "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();