summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/watcher
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/server/actors/watcher
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--devtools/server/actors/watcher.js530
-rw-r--r--devtools/server/actors/watcher/WatchedDataHelpers.jsm124
-rw-r--r--devtools/server/actors/watcher/WatcherRegistry.jsm346
-rw-r--r--devtools/server/actors/watcher/moz.build14
-rw-r--r--devtools/server/actors/watcher/target-helpers/frame-helper.js204
-rw-r--r--devtools/server/actors/watcher/target-helpers/moz.build12
-rw-r--r--devtools/server/actors/watcher/target-helpers/process-helper.js281
-rw-r--r--devtools/server/actors/watcher/target-helpers/utils.js126
-rw-r--r--devtools/server/actors/watcher/target-helpers/worker-helper.js144
9 files changed, 1781 insertions, 0 deletions
diff --git a/devtools/server/actors/watcher.js b/devtools/server/actors/watcher.js
new file mode 100644
index 0000000000..9ddc365c4d
--- /dev/null
+++ b/devtools/server/actors/watcher.js
@@ -0,0 +1,530 @@
+/* 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";
+const protocol = require("devtools/shared/protocol");
+const { watcherSpec } = require("devtools/shared/specs/watcher");
+const Services = require("Services");
+
+const Resources = require("devtools/server/actors/resources/index");
+const {
+ TargetActorRegistry,
+} = require("devtools/server/actors/targets/target-actor-registry.jsm");
+const {
+ WatcherRegistry,
+} = require("devtools/server/actors/watcher/WatcherRegistry.jsm");
+const Targets = require("devtools/server/actors/targets/index");
+
+const TARGET_HELPERS = {};
+loader.lazyRequireGetter(
+ TARGET_HELPERS,
+ Targets.TYPES.FRAME,
+ "devtools/server/actors/watcher/target-helpers/frame-helper"
+);
+loader.lazyRequireGetter(
+ TARGET_HELPERS,
+ Targets.TYPES.PROCESS,
+ "devtools/server/actors/watcher/target-helpers/process-helper"
+);
+loader.lazyRequireGetter(
+ TARGET_HELPERS,
+ Targets.TYPES.WORKER,
+ "devtools/server/actors/watcher/target-helpers/worker-helper"
+);
+
+loader.lazyRequireGetter(
+ this,
+ "NetworkParentActor",
+ "devtools/server/actors/network-monitor/network-parent",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "BreakpointListActor",
+ "devtools/server/actors/breakpoint-list",
+ true
+);
+
+exports.WatcherActor = protocol.ActorClassWithSpec(watcherSpec, {
+ /**
+ * Optionally pass a `browser` in the second argument
+ * in order to focus only on targets related to a given <browser> element.
+ */
+ initialize: function(conn, options) {
+ protocol.Actor.prototype.initialize.call(this, conn);
+ this._browser = options && options.browser;
+
+ this.notifyResourceAvailable = this.notifyResourceAvailable.bind(this);
+ this.notifyResourceDestroyed = this.notifyResourceDestroyed.bind(this);
+ this.notifyResourceUpdated = this.notifyResourceUpdated.bind(this);
+ },
+
+ /**
+ * If we are debugging only one Tab or Document, returns its BrowserElement.
+ * For Tabs, it will be the <browser> element used to load the web page.
+ *
+ * This is typicaly used to fetch:
+ * - its `browserId` attribute, which uniquely defines it,
+ * - its `browsingContextID` or `browsingContext`, which helps inspecting its content.
+ */
+ get browserElement() {
+ return this._browser;
+ },
+
+ /**
+ * Unique identifier, which helps designates one precise browser element, the one
+ * we may debug. This is only set if we actually debug a browser element.
+ * So, that will be typically set when we debug a tab, but not when we debug
+ * a process, or a worker.
+ */
+ get browserId() {
+ return this._browser?.browserId;
+ },
+
+ destroy: function() {
+ // Force unwatching for all types, even if we weren't watching.
+ // This is fine as unwatchTarget is NOOP if we weren't already watching for this target type.
+ for (const targetType of Object.values(Targets.TYPES)) {
+ this.unwatchTargets(targetType);
+ }
+ this.unwatchResources(Object.values(Resources.TYPES));
+
+ WatcherRegistry.unregisterWatcher(this);
+
+ // Destroy the actor at the end so that its actorID keeps being defined.
+ protocol.Actor.prototype.destroy.call(this);
+ },
+
+ /*
+ * Get the list of the currently watched resources for this watcher.
+ *
+ * @return Array<String>
+ * Returns the list of currently watched resource types.
+ */
+ get watchedData() {
+ return WatcherRegistry.getWatchedData(this);
+ },
+
+ form() {
+ const enableServerWatcher = Services.prefs.getBoolPref(
+ "devtools.testing.enableServerWatcherSupport",
+ false
+ );
+
+ const hasBrowserElement = !!this.browserElement;
+
+ return {
+ actor: this.actorID,
+ // The resources and target traits should be removed all at the same time since the
+ // client has generic ways to deal with all of them (See Bug 1680280).
+ traits: {
+ [Targets.TYPES.FRAME]: true,
+ [Targets.TYPES.PROCESS]: true,
+ [Targets.TYPES.WORKER]: hasBrowserElement,
+ resources: {
+ // In Firefox 81 we added support for:
+ // - CONSOLE_MESSAGE
+ // - CSS_CHANGE
+ // - CSS_MESSAGE
+ // - DOCUMENT_EVENT
+ // - ERROR_MESSAGE
+ // - PLATFORM_MESSAGE
+ //
+ // We enabled them for content toolboxes only because we don't support
+ // content process targets yet. Bug 1620248 should help supporting
+ // them and enable this more broadly.
+ //
+ // New server-side resources can be gated behind
+ // `devtools.testing.enableServerWatcherSupport` if needed.
+ [Resources.TYPES.CONSOLE_MESSAGE]: hasBrowserElement,
+ [Resources.TYPES.CSS_CHANGE]: hasBrowserElement,
+ [Resources.TYPES.CSS_MESSAGE]: hasBrowserElement,
+ [Resources.TYPES.DOCUMENT_EVENT]: hasBrowserElement,
+ [Resources.TYPES.ERROR_MESSAGE]: hasBrowserElement,
+ [Resources.TYPES.LOCAL_STORAGE]: hasBrowserElement,
+ [Resources.TYPES.SESSION_STORAGE]: hasBrowserElement,
+ [Resources.TYPES.PLATFORM_MESSAGE]: true,
+ [Resources.TYPES.NETWORK_EVENT]: hasBrowserElement,
+ [Resources.TYPES.NETWORK_EVENT_STACKTRACE]: hasBrowserElement,
+ [Resources.TYPES.STYLESHEET]:
+ enableServerWatcher && hasBrowserElement,
+ [Resources.TYPES.SOURCE]: hasBrowserElement,
+ },
+ // @backward-compat { version 85 } When removing this trait, consumers using
+ // the TargetList to retrieve the Breakpoints front should still be careful to check
+ // that the Watcher is available
+ "set-breakpoints": true,
+ },
+ };
+ },
+
+ /**
+ * Start watching for a new target type.
+ *
+ * This will instantiate Target Actors for existing debugging context of this type,
+ * but will also create actors as context of this type get created.
+ * The actors are notified to the client via "target-available-form" RDP events.
+ * We also notify about target actors destruction via "target-destroyed-form".
+ * Note that we are guaranteed to receive all existing target actor by the time this method
+ * resolves.
+ *
+ * @param {string} targetType
+ * Type of context to observe. See Targets.TYPES object.
+ */
+ async watchTargets(targetType) {
+ WatcherRegistry.watchTargets(this, targetType);
+
+ const targetHelperModule = TARGET_HELPERS[targetType];
+ // Await the registration in order to ensure receiving the already existing targets
+ await targetHelperModule.createTargets(this);
+ },
+
+ /**
+ * Stop watching for a given target type.
+ *
+ * @param {string} targetType
+ * Type of context to observe. See Targets.TYPES object.
+ */
+ unwatchTargets(targetType) {
+ const isWatchingTargets = WatcherRegistry.unwatchTargets(this, targetType);
+ if (!isWatchingTargets) {
+ return;
+ }
+
+ const targetHelperModule = TARGET_HELPERS[targetType];
+ targetHelperModule.destroyTargets(this);
+
+ // Unregister the JS Window Actor if there is no more DevTools code observing any target/resource
+ WatcherRegistry.maybeUnregisteringJSWindowActor();
+ },
+
+ /**
+ * Called by a Watcher module, whenever a new target is available
+ */
+ notifyTargetAvailable(actor) {
+ this.emit("target-available-form", actor);
+ },
+
+ /**
+ * Called by a Watcher module, whenever a target has been destroyed
+ */
+ notifyTargetDestroyed(actor) {
+ this.emit("target-destroyed-form", actor);
+ },
+
+ /**
+ * Given a browsingContextID, returns its parent browsingContextID. Returns null if a
+ * parent browsing context couldn't be found. Throws if the browsing context
+ * corresponding to the passed browsingContextID couldn't be found.
+ *
+ * @param {Integer} browsingContextID
+ * @returns {Integer|null}
+ */
+ getParentBrowsingContextID(browsingContextID) {
+ const browsingContext = BrowsingContext.get(browsingContextID);
+ if (!browsingContext) {
+ throw new Error(
+ `BrowsingContext with ID=${browsingContextID} doesn't exist.`
+ );
+ }
+ // Top-level documents of tabs, loaded in a <browser> element expose a null `parent`.
+ // i.e. Their BrowsingContext has no parent and is considered top level.
+ // But... in the context of the Browser Toolbox, we still consider them as child of the browser window.
+ // So, for them, fallback on `embedderWindowGlobal`, which will typically be the WindowGlobal for browser.xhtml.
+ if (browsingContext.parent) {
+ return browsingContext.parent.id;
+ }
+ if (browsingContext.embedderWindowGlobal) {
+ return browsingContext.embedderWindowGlobal.browsingContext.id;
+ }
+ return null;
+ },
+
+ /**
+ * Called by Resource Watchers, when new resources are available.
+ *
+ * @param Array<json> resources
+ * List of all available resources. A resource is a JSON object piped over to the client.
+ * It may contain actor IDs, actor forms, to be manually marshalled by the client.
+ */
+ notifyResourceAvailable(resources) {
+ this._emitResourcesForm("resource-available-form", resources);
+ },
+
+ notifyResourceDestroyed(resources) {
+ this._emitResourcesForm("resource-destroyed-form", resources);
+ },
+
+ notifyResourceUpdated(resources) {
+ this._emitResourcesForm("resource-updated-form", resources);
+ },
+
+ /**
+ * Wrapper around emit for resource forms.
+ */
+ _emitResourcesForm(name, resources) {
+ if (resources.length === 0) {
+ // Don't try to emit if the resources array is empty.
+ return;
+ }
+ this.emit(name, resources);
+ },
+
+ /**
+ * Try to retrieve a parent process TargetActor:
+ * - either when debugging a parent process page (when browserElement is set to the page's tab),
+ * - or when debugging the main process (when browserElement is null).
+ *
+ * See comment in `watchResources`, this will handle targets which are ignored by Frame and Process
+ * target helpers. (and only those which are ignored)
+ */
+ _getTargetActorInParentProcess() {
+ return this.browserElement
+ ? // Note: if any, the BrowsingContextTargetActor returned here is created for a parent process
+ // page and lives in the parent process.
+ TargetActorRegistry.getTargetActor(this.browserId)
+ : TargetActorRegistry.getParentProcessTargetActor();
+ },
+
+ /**
+ * Start watching for a list of resource types.
+ * This should only resolve once all "already existing" resources of these types
+ * are notified to the client via resource-available-form event on related target actors.
+ *
+ * @param {Array<string>} resourceTypes
+ * List of all types to listen to.
+ */
+ async watchResources(resourceTypes) {
+ // First process resources which have to be listened from the parent process
+ // (the watcher actor always runs in the parent process)
+ await Resources.watchResources(
+ this,
+ Resources.getParentProcessResourceTypes(resourceTypes)
+ );
+
+ // Bail out early if all resources were watched from parent process.
+ // In this scenario, we do not need to update these resource types in the WatcherRegistry
+ // as targets do not care about them.
+ if (!Resources.hasResourceTypesForTargets(resourceTypes)) {
+ return;
+ }
+
+ WatcherRegistry.watchResources(this, resourceTypes);
+
+ // Fetch resources from all existing targets
+ for (const targetType in TARGET_HELPERS) {
+ // We process frame targets even if we aren't watching them,
+ // because frame target helper codepath handles the top level target, if it runs in the *content* process.
+ // It will do another check to `isWatchingTargets(FRAME)` internally.
+ // Note that the workaround at the end of this method, using TargetActorRegistry
+ // is specific to top level target running in the *parent* process.
+ if (
+ !WatcherRegistry.isWatchingTargets(this, targetType) &&
+ targetType != Targets.TYPES.FRAME
+ ) {
+ continue;
+ }
+ const targetResourceTypes = Resources.getResourceTypesForTargetType(
+ resourceTypes,
+ targetType
+ );
+ if (targetResourceTypes.length == 0) {
+ continue;
+ }
+ const targetHelperModule = TARGET_HELPERS[targetType];
+ await targetHelperModule.addWatcherDataEntry({
+ watcher: this,
+ type: "resources",
+ entries: targetResourceTypes,
+ });
+ }
+
+ /*
+ * The Watcher actor doesn't support watching the top level target
+ * (bug 1644397 and possibly some other followup).
+ *
+ * Because of that, we miss reaching these targets in the previous lines of this function.
+ * Since all BrowsingContext target actors register themselves to the TargetActorRegistry,
+ * we use it here in order to reach those missing targets, which are running in the
+ * parent process (where this WatcherActor lives as well):
+ * - the parent process target (which inherits from BrowsingContextTargetActor)
+ * - top level tab target for documents loaded in the parent process (e.g. about:robots).
+ * When the tab loads document in the content process, the FrameTargetHelper will
+ * reach it via the JSWindowActor API. Even if it uses MessageManager for anything
+ * else (RDP packet forwarding, creation and destruction).
+ *
+ * We will eventually get rid of this code once all targets are properly supported by
+ * the Watcher Actor and we have target helpers for all of them.
+ */
+ const frameResourceTypes = Resources.getResourceTypesForTargetType(
+ resourceTypes,
+ Targets.TYPES.FRAME
+ );
+ if (frameResourceTypes.length > 0) {
+ const targetActor = this._getTargetActorInParentProcess();
+ if (targetActor) {
+ await targetActor.addWatcherDataEntry("resources", frameResourceTypes);
+ }
+ }
+ },
+
+ /**
+ * Stop watching for a list of resource types.
+ *
+ * @param {Array<string>} resourceTypes
+ * List of all types to listen to.
+ */
+ unwatchResources(resourceTypes) {
+ // First process resources which are listened from the parent process
+ // (the watcher actor always runs in the parent process)
+ Resources.unwatchResources(
+ this,
+ Resources.getParentProcessResourceTypes(resourceTypes)
+ );
+
+ // Bail out early if all resources were all watched from parent process.
+ // In this scenario, we do not need to update these resource types in the WatcherRegistry
+ // as targets do not care about them.
+ if (!Resources.hasResourceTypesForTargets(resourceTypes)) {
+ return;
+ }
+
+ const isWatchingResources = WatcherRegistry.unwatchResources(
+ this,
+ resourceTypes
+ );
+ if (!isWatchingResources) {
+ return;
+ }
+
+ // Prevent trying to unwatch when the related BrowsingContext has already
+ // been destroyed
+ if (!this.browserElement || this.browserElement.browsingContext) {
+ for (const targetType in TARGET_HELPERS) {
+ // Frame target helper handles the top level target, if it runs in the content process
+ // so we should always process it. It does a second check to isWatchingTargets.
+ if (
+ !WatcherRegistry.isWatchingTargets(this, targetType) &&
+ targetType != Targets.TYPES.FRAME
+ ) {
+ continue;
+ }
+ const targetResourceTypes = Resources.getResourceTypesForTargetType(
+ resourceTypes,
+ targetType
+ );
+ if (targetResourceTypes.length == 0) {
+ continue;
+ }
+ const targetHelperModule = TARGET_HELPERS[targetType];
+ targetHelperModule.removeWatcherDataEntry({
+ watcher: this,
+ type: "resources",
+ entries: targetResourceTypes,
+ });
+ }
+ }
+
+ // See comment in watchResources.
+ const frameResourceTypes = Resources.getResourceTypesForTargetType(
+ resourceTypes,
+ Targets.TYPES.FRAME
+ );
+ if (frameResourceTypes.length > 0) {
+ const targetActor = this._getTargetActorInParentProcess();
+ if (targetActor) {
+ targetActor.removeWatcherDataEntry("resources", frameResourceTypes);
+ }
+ }
+
+ // Unregister the JS Window Actor if there is no more DevTools code observing any target/resource
+ WatcherRegistry.maybeUnregisteringJSWindowActor();
+ },
+
+ /**
+ * Returns the network actor.
+ *
+ * @return {Object} actor
+ * The network actor.
+ */
+ getNetworkParentActor() {
+ return new NetworkParentActor(this);
+ },
+
+ /**
+ * Returns the breakpoint list actor.
+ *
+ * @return {Object} actor
+ * The breakpoint list actor.
+ */
+ getBreakpointListActor() {
+ return new BreakpointListActor(this);
+ },
+
+ /**
+ * Server internal API, called by other actors, but not by the client.
+ * Used to agrement some new entries for a given data type (watchers target, resources,
+ * breakpoints,...)
+ *
+ * @param {String} type
+ * Data type to contribute to.
+ * @param {Array<*>} entries
+ * List of values to add for this data type.
+ */
+ async addDataEntry(type, entries) {
+ WatcherRegistry.addWatcherDataEntry(this, type, entries);
+
+ await Promise.all(
+ Object.values(Targets.TYPES)
+ .filter(targetType =>
+ WatcherRegistry.isWatchingTargets(this, targetType)
+ )
+ .map(async targetType => {
+ const targetHelperModule = TARGET_HELPERS[targetType];
+ await targetHelperModule.addWatcherDataEntry({
+ watcher: this,
+ type,
+ entries,
+ });
+ })
+ );
+
+ // See comment in watchResources
+ const targetActor = this._getTargetActorInParentProcess();
+ if (targetActor) {
+ await targetActor.addWatcherDataEntry(type, entries);
+ }
+ },
+
+ /**
+ * Server internal API, called by other actors, but not by the client.
+ * Used to remve some existing entries for a given data type (watchers target, resources,
+ * breakpoints,...)
+ *
+ * @param {String} type
+ * Data type to modify.
+ * @param {Array<*>} entries
+ * List of values to remove from this data type.
+ */
+ removeDataEntry(type, entries) {
+ WatcherRegistry.removeWatcherDataEntry(this, type, entries);
+
+ Object.values(Targets.TYPES)
+ .filter(targetType => WatcherRegistry.isWatchingTargets(this, targetType))
+ .forEach(targetType => {
+ const targetHelperModule = TARGET_HELPERS[targetType];
+ targetHelperModule.removeWatcherDataEntry({
+ watcher: this,
+ type,
+ entries,
+ });
+ });
+
+ // See comment in watchResources
+ const targetActor = this._getTargetActorInParentProcess();
+ if (targetActor) {
+ targetActor.removeWatcherDataEntry(type, entries);
+ }
+ },
+});
diff --git a/devtools/server/actors/watcher/WatchedDataHelpers.jsm b/devtools/server/actors/watcher/WatchedDataHelpers.jsm
new file mode 100644
index 0000000000..9acd5a612c
--- /dev/null
+++ b/devtools/server/actors/watcher/WatchedDataHelpers.jsm
@@ -0,0 +1,124 @@
+/* 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";
+
+/**
+ * Helper module alongside WatcherRegistry, which focus on updating the "watchedData" object.
+ * This object is shared across processes and threads and have to be maintained in all these runtimes.
+ */
+
+var EXPORTED_SYMBOLS = ["WatchedDataHelpers"];
+
+// List of all arrays stored in `watchedData`, which are replicated across processes and threads
+const SUPPORTED_DATA = {
+ BREAKPOINTS: "breakpoints",
+ RESOURCES: "resources",
+ TARGETS: "targets",
+};
+
+// Optional function, if data isn't a primitive data type in order to produce a key
+// for the given data entry
+const DATA_KEY_FUNCTION = {
+ [SUPPORTED_DATA.BREAKPOINTS]: function({
+ location: { sourceUrl, sourceId, line, column },
+ }) {
+ if (!sourceUrl && !sourceId) {
+ throw new Error(
+ `Breakpoints expect to have either a sourceUrl or a sourceId.`
+ );
+ }
+ if (sourceUrl && typeof sourceUrl != "string") {
+ throw new Error(
+ `Breakpoints expect to have sourceUrl string, got ${typeof sourceUrl} instead.`
+ );
+ }
+ // sourceId may be undefined for some sources keyed by URL
+ if (sourceId && typeof sourceId != "string") {
+ throw new Error(
+ `Breakpoints expect to have sourceId string, got ${typeof sourceId} instead.`
+ );
+ }
+ if (typeof line != "number") {
+ throw new Error(
+ `Breakpoints expect to have line number, got ${typeof line} instead.`
+ );
+ }
+ if (typeof column != "number") {
+ throw new Error(
+ `Breakpoints expect to have column number, got ${typeof column} instead.`
+ );
+ }
+ return `${sourceUrl}:${sourceId}:${line}:${column}`;
+ },
+};
+
+function idFunction(v) {
+ if (typeof v != "string") {
+ throw new Error(
+ `Expect data entry values to be string, or be using custom data key functions. Got ${typeof v} type instead.`
+ );
+ }
+ return v;
+}
+
+const WatchedDataHelpers = {
+ SUPPORTED_DATA,
+
+ /**
+ * Add new values to the shared "watchedData" object.
+ *
+ * @param Object watchedData
+ * The data object to update.
+ * @param string type
+ * The type of data to be added
+ * @param Array<Object> entries
+ * The values to be added to this type of data
+ */
+ addWatchedDataEntry(watchedData, type, entries) {
+ const toBeAdded = [];
+ const keyFunction = DATA_KEY_FUNCTION[type] || idFunction;
+ for (const entry of entries) {
+ const alreadyExists = watchedData[type].some(existingEntry => {
+ return keyFunction(existingEntry) === keyFunction(entry);
+ });
+ if (!alreadyExists) {
+ toBeAdded.push(entry);
+ }
+ }
+ watchedData[type].push(...toBeAdded);
+ },
+
+ /**
+ * Remove values from the shared "watchedData" object.
+ *
+ * @param Object watchedData
+ * The data object to update.
+ * @param string type
+ * The type of data to be remove
+ * @param Array<Object> entries
+ * The values to be removed from this type of data
+ * @return Boolean
+ * True, if at least one entries existed and has been removed.
+ * False, if none of the entries existed and none has been removed.
+ */
+ removeWatchedDataEntry(watchedData, type, entries) {
+ let includesAtLeastOne = false;
+ const keyFunction = DATA_KEY_FUNCTION[type] || idFunction;
+ for (const entry of entries) {
+ const idx = watchedData[type].findIndex(existingEntry => {
+ return keyFunction(existingEntry) === keyFunction(entry);
+ });
+ if (idx !== -1) {
+ watchedData[type].splice(idx, 1);
+ includesAtLeastOne = true;
+ }
+ }
+ if (!includesAtLeastOne) {
+ return false;
+ }
+
+ return true;
+ },
+};
diff --git a/devtools/server/actors/watcher/WatcherRegistry.jsm b/devtools/server/actors/watcher/WatcherRegistry.jsm
new file mode 100644
index 0000000000..f8fc2aed49
--- /dev/null
+++ b/devtools/server/actors/watcher/WatcherRegistry.jsm
@@ -0,0 +1,346 @@
+/* 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";
+
+/**
+ * Helper module around `sharedData` object that helps storing the state
+ * of all observed Targets and Resources, that, for all DevTools connections.
+ * Here is a few words about the C++ implementation of sharedData:
+ * https://searchfox.org/mozilla-central/rev/bc3600def806859c31b2c7ac06e3d69271052a89/dom/ipc/SharedMap.h#30-55
+ *
+ * We may have more than one DevToolsServer and one server may have more than one
+ * client. This module will be the single source of truth in the parent process,
+ * in order to know which targets/resources are currently observed. It will also
+ * be used to declare when something starts/stops being observed.
+ *
+ * `sharedData` is a platform API that helps sharing JS Objects across processes.
+ * We use it in order to communicate to the content process which targets and resources
+ * should be observed. Content processes read this data only once, as soon as they are created.
+ * It isn't used beyond this point. Content processes are not going to update it.
+ * We will notify about changes in observed targets and resources for already running
+ * processes by some other means. (Via JS Window Actor queries "DevTools:(un)watch(Resources|Target)")
+ * This means that only this module will update the "DevTools:watchedPerWatcher" value.
+ * From the parent process, we should be going through this module to fetch the data,
+ * while from the content process, we will read `sharedData` directly.
+ */
+
+var EXPORTED_SYMBOLS = ["WatcherRegistry"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { ActorManagerParent } = ChromeUtils.import(
+ "resource://gre/modules/ActorManagerParent.jsm"
+);
+const { WatchedDataHelpers } = ChromeUtils.import(
+ "resource://devtools/server/actors/watcher/WatchedDataHelpers.jsm"
+);
+const { SUPPORTED_DATA } = WatchedDataHelpers;
+
+// Define the Map that will be saved in `sharedData`.
+// It is keyed by WatcherActor ID and values contains following attributes:
+// - targets: Set of strings, refering to target types to be listened to
+// - resources: Set of strings, refering to resource types to be observed
+// - browserId: Optional, if set, restrict the observation to one specific Browser Element tree.
+// It can be a tab, a top-level window or a top-level iframe (e.g. special privileged iframe)
+// See https://searchfox.org/mozilla-central/rev/31d8600b73dc85b4cdbabf45ac3f1a9c11700d8e/dom/chrome-webidl/BrowsingContext.webidl#114-121
+// for more information.
+// - connectionPrefix: The DevToolsConnection prefix of the watcher actor. Used to compute new actor ID in the content processes.
+//
+// Unfortunately, `sharedData` is subject to race condition and may have side effect
+// when read/written from multiple places in the same process,
+// which is why this map should be considered as the single source of truth.
+const watchedDataByWatcherActor = new Map();
+
+// In parallel to the previous map, keep all the WatcherActor keyed by the same WatcherActor ID,
+// the WatcherActor ID. We don't (and can't) propagate the WatcherActor instances to the content
+// processes, but still would like to match them by their ID.
+const watcherActors = new Map();
+
+// Name of the attribute into which we save this Map in `sharedData` object.
+const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
+
+/**
+ * Use `sharedData` to allow processes, early during their creation,
+ * to know which resources should be listened to. This will be read
+ * from the Target actor, when it gets created early during process start,
+ * in order to start listening to the expected resource types.
+ */
+function persistMapToSharedData() {
+ Services.ppmm.sharedData.set(SHARED_DATA_KEY_NAME, watchedDataByWatcherActor);
+ // Request to immediately flush the data to the content processes in order to prevent
+ // races (bug 1644649). Otherwise content process may have outdated sharedData
+ // and try to create targets for Watcher actor that already stopped watching for targets.
+ Services.ppmm.sharedData.flush();
+}
+
+const WatcherRegistry = {
+ /**
+ * Tells if a given watcher currently watches for a given target type.
+ *
+ * @param WatcherActor watcher
+ * The WatcherActor which should be listening.
+ * @param string targetType
+ * The new target type to query.
+ * @return boolean
+ * Returns true if already watching.
+ */
+ isWatchingTargets(watcher, targetType) {
+ const watchedData = this.getWatchedData(watcher);
+ return watchedData && watchedData.targets.includes(targetType);
+ },
+
+ /**
+ * Retrieve the data saved into `sharedData` that is used to know
+ * about which type of targets and resources we care listening about.
+ * `watchedDataByWatcherActor` is saved into `sharedData` after each mutation,
+ * but `watchedDataByWatcherActor` is the source of truth.
+ *
+ * @param WatcherActor watcher
+ * The related WatcherActor which starts/stops observing.
+ * @param object options (optional)
+ * A dictionary object with `createData` boolean attribute.
+ * If this attribute is set to true, we create the data structure in the Map
+ * if none exists for this prefix.
+ */
+ getWatchedData(watcher, { createData = false } = {}) {
+ // Use WatcherActor ID as a key as we may have multiple clients willing to watch for targets.
+ // For example, a Browser Toolbox debugging everything and a Content Toolbox debugging
+ // just one tab. We might also have multiple watchers, on the same connection when using about:debugging.
+ const watcherActorID = watcher.actorID;
+ let watchedData = watchedDataByWatcherActor.get(watcherActorID);
+ if (!watchedData && createData) {
+ watchedData = {
+ // The Browser ID will be helpful to identify which BrowsingContext should be considered
+ // when running code in the content process. Browser ID, compared to BrowsingContext ID won't change
+ // if we navigate to the parent process or if a new BrowsingContext is used for the <browser> element
+ // we are currently inspecting.
+ browserId: watcher.browserId,
+ // The DevToolsServerConnection prefix will be used to compute actor IDs created in the content process
+ connectionPrefix: watcher.conn.prefix,
+ };
+ // Define empty default array for all data
+ for (const name of Object.values(SUPPORTED_DATA)) {
+ watchedData[name] = [];
+ }
+ watchedDataByWatcherActor.set(watcherActorID, watchedData);
+ watcherActors.set(watcherActorID, watcher);
+ }
+ return watchedData;
+ },
+
+ /**
+ * Given a Watcher Actor ID, return the related Watcher Actor instance.
+ *
+ * @param String actorID
+ * The Watcher Actor ID to search for.
+ * @return WatcherActor
+ * The Watcher Actor instance.
+ */
+ getWatcher(actorID) {
+ return watcherActors.get(actorID);
+ },
+
+ /**
+ * Notify that a given watcher added an entry in a given data type.
+ *
+ * @param WatcherActor watcher
+ * The WatcherActor which starts observing.
+ * @param string type
+ * The type of data to be added
+ * @param Array<Object> entries
+ * The values to be added to this type of data
+ */
+ addWatcherDataEntry(watcher, type, entries) {
+ const watchedData = this.getWatchedData(watcher, {
+ createData: true,
+ });
+
+ if (!(type in watchedData)) {
+ throw new Error(`Unsupported watcher data type: ${type}`);
+ }
+
+ WatchedDataHelpers.addWatchedDataEntry(watchedData, type, entries);
+
+ // Register the JS Window Actor the first time we start watching for something (e.g. resource, target, …).
+ registerJSWindowActor();
+
+ persistMapToSharedData();
+ },
+
+ /**
+ * Notify that a given watcher removed an entry in a given data type.
+ *
+ * See `addWatcherDataEntry` for argument definition.
+ *
+ * @return boolean
+ * True if we such entry was already registered, for this watcher actor.
+ */
+ removeWatcherDataEntry(watcher, type, entries) {
+ const watchedData = this.getWatchedData(watcher);
+ if (!watchedData) {
+ return false;
+ }
+
+ if (!(type in watchedData)) {
+ throw new Error(`Unsupported watcher data type: ${type}`);
+ }
+
+ if (
+ !WatchedDataHelpers.removeWatchedDataEntry(watchedData, type, entries)
+ ) {
+ return false;
+ }
+
+ const isWatchingSomething = Object.values(SUPPORTED_DATA).some(
+ dataType => watchedData[dataType].length > 0
+ );
+ if (!isWatchingSomething) {
+ watchedDataByWatcherActor.delete(watcher.actorID);
+ watcherActors.delete(watcher.actorID);
+ }
+
+ persistMapToSharedData();
+
+ return true;
+ },
+
+ /**
+ * Cleanup everything about a given watcher actor.
+ * Remove it from any registry so that we stop interacting with it.
+ *
+ * The watcher would be automatically unregistered from removeWatcherEntry,
+ * if we remove all entries. But we aren't removing all breakpoints.
+ * So here, we force clearing any reference to the watcher actor when it destroys.
+ */
+ unregisterWatcher(watcher) {
+ watchedDataByWatcherActor.delete(watcher.actorID);
+ watcherActors.delete(watcher.actorID);
+ },
+
+ /**
+ * Notify that a given watcher starts observing a new target type.
+ *
+ * @param WatcherActor watcher
+ * The WatcherActor which starts observing.
+ * @param string targetType
+ * The new target type to start listening to.
+ */
+ watchTargets(watcher, targetType) {
+ this.addWatcherDataEntry(watcher, SUPPORTED_DATA.TARGETS, [targetType]);
+ },
+
+ /**
+ * Notify that a given watcher stops observing a given target type.
+ *
+ * See `watchTargets` for argument definition.
+ *
+ * @return boolean
+ * True if we were watching for this target type, for this watcher actor.
+ */
+ unwatchTargets(watcher, targetType) {
+ return this.removeWatcherDataEntry(watcher, SUPPORTED_DATA.TARGETS, [
+ targetType,
+ ]);
+ },
+
+ /**
+ * Notify that a given watcher starts observing new resource types.
+ *
+ * @param WatcherActor watcher
+ * The WatcherActor which starts observing.
+ * @param Array<string> resourceTypes
+ * The new resource types to start listening to.
+ */
+ watchResources(watcher, resourceTypes) {
+ this.addWatcherDataEntry(watcher, SUPPORTED_DATA.RESOURCES, resourceTypes);
+ },
+
+ /**
+ * Notify that a given watcher stops observing given resource types.
+ *
+ * See `watchResources` for argument definition.
+ *
+ * @return boolean
+ * True if we were watching for this resource type, for this watcher actor.
+ */
+ unwatchResources(watcher, resourceTypes) {
+ return this.removeWatcherDataEntry(
+ watcher,
+ SUPPORTED_DATA.RESOURCES,
+ resourceTypes
+ );
+ },
+
+ /**
+ * Unregister the JS Window Actor if there is no more DevTools code observing any target/resource.
+ */
+ maybeUnregisteringJSWindowActor() {
+ if (watchedDataByWatcherActor.size == 0) {
+ unregisterJSWindowActor();
+ }
+ },
+};
+
+// Boolean flag to know if the DevToolsFrame JS Window Actor is currently registered
+let isJSWindowActorRegistered = false;
+
+/**
+ * Register the JSWindowActor pair "DevToolsFrame".
+ *
+ * We should call this method before we try to use this JS Window Actor from the parent process
+ * (via `WindowGlobal.getActor("DevToolsFrame")` or `WindowGlobal.getActor("DevToolsWorker")`).
+ * Also, registering it will automatically force spawing the content process JSWindow Actor
+ * anytime a new document is opened (via DOMWindowCreated event).
+ */
+
+const JSWindowActorsConfig = {
+ DevToolsFrame: {
+ parent: {
+ moduleURI:
+ "resource://devtools/server/connectors/js-window-actor/DevToolsFrameParent.jsm",
+ },
+ child: {
+ moduleURI:
+ "resource://devtools/server/connectors/js-window-actor/DevToolsFrameChild.jsm",
+ events: {
+ DOMWindowCreated: {},
+ },
+ },
+ allFrames: true,
+ },
+ DevToolsWorker: {
+ parent: {
+ moduleURI:
+ "resource://devtools/server/connectors/js-window-actor/DevToolsWorkerParent.jsm",
+ },
+ child: {
+ moduleURI:
+ "resource://devtools/server/connectors/js-window-actor/DevToolsWorkerChild.jsm",
+ events: {
+ DOMWindowCreated: {},
+ },
+ },
+ allFrames: true,
+ },
+};
+
+function registerJSWindowActor() {
+ if (isJSWindowActorRegistered) {
+ return;
+ }
+ isJSWindowActorRegistered = true;
+ ActorManagerParent.addJSWindowActors(JSWindowActorsConfig);
+}
+
+function unregisterJSWindowActor() {
+ if (!isJSWindowActorRegistered) {
+ return;
+ }
+ isJSWindowActorRegistered = false;
+
+ for (const JSWindowActorName of Object.keys(JSWindowActorsConfig)) {
+ // ActorManagerParent doesn't expose a "removeActors" method, but it would be equivalent to that:
+ ChromeUtils.unregisterWindowActor(JSWindowActorName);
+ }
+}
diff --git a/devtools/server/actors/watcher/moz.build b/devtools/server/actors/watcher/moz.build
new file mode 100644
index 0000000000..227184cf3b
--- /dev/null
+++ b/devtools/server/actors/watcher/moz.build
@@ -0,0 +1,14 @@
+# -*- 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/.
+
+DIRS += [
+ "target-helpers",
+]
+
+DevToolsModules(
+ "WatchedDataHelpers.jsm",
+ "WatcherRegistry.jsm",
+)
diff --git a/devtools/server/actors/watcher/target-helpers/frame-helper.js b/devtools/server/actors/watcher/target-helpers/frame-helper.js
new file mode 100644
index 0000000000..612febab9e
--- /dev/null
+++ b/devtools/server/actors/watcher/target-helpers/frame-helper.js
@@ -0,0 +1,204 @@
+/* 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";
+
+const {
+ WatcherRegistry,
+} = require("devtools/server/actors/watcher/WatcherRegistry.jsm");
+const {
+ WindowGlobalLogger,
+} = require("devtools/server/connectors/js-window-actor/WindowGlobalLogger.jsm");
+const Targets = require("devtools/server/actors/targets/index");
+const {
+ getAllRemoteBrowsingContexts,
+ shouldNotifyWindowGlobal,
+} = require("devtools/server/actors/watcher/target-helpers/utils.js");
+
+/**
+ * Force creating targets for all existing BrowsingContext, that, for a given Watcher Actor.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to watch for new targets.
+ */
+async function createTargets(watcher) {
+ // Go over all existing BrowsingContext in order to:
+ // - Force the instantiation of a DevToolsFrameChild
+ // - Have the DevToolsFrameChild to spawn the BrowsingContextTargetActor
+ const browsingContexts = getFilteredRemoteBrowsingContext(
+ watcher.browserElement
+ );
+ const promises = [];
+ for (const browsingContext of browsingContexts) {
+ logWindowGlobal(
+ browsingContext.currentWindowGlobal,
+ "Existing WindowGlobal"
+ );
+
+ // Await for the query in order to try to resolve only *after* we received these
+ // already available targets.
+ const promise = browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .instantiateTarget({
+ watcherActorID: watcher.actorID,
+ connectionPrefix: watcher.conn.prefix,
+ browserId: watcher.browserId,
+ watchedData: watcher.watchedData,
+ });
+ promises.push(promise);
+ }
+ return Promise.all(promises);
+}
+
+/**
+ * Force destroying all BrowsingContext targets which were related to a given watcher.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to stop watching for new targets.
+ */
+function destroyTargets(watcher) {
+ // Go over all existing BrowsingContext in order to destroy all targets
+ const browsingContexts = getFilteredRemoteBrowsingContext(
+ watcher.browserElement
+ );
+ for (const browsingContext of browsingContexts) {
+ logWindowGlobal(
+ browsingContext.currentWindowGlobal,
+ "Existing WindowGlobal"
+ );
+
+ browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .destroyTarget({
+ watcherActorID: watcher.actorID,
+ browserId: watcher.browserId,
+ });
+ }
+}
+
+/**
+ * Go over all existing BrowsingContext in order to communicate about new data entries
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to stop watching for new targets.
+ * @param string type
+ * The type of data to be added
+ * @param Array<Object> entries
+ * The values to be added to this type of data
+ */
+async function addWatcherDataEntry({ watcher, type, entries }) {
+ const browsingContexts = getWatchingBrowsingContexts(watcher);
+ const promises = [];
+ for (const browsingContext of browsingContexts) {
+ logWindowGlobal(
+ browsingContext.currentWindowGlobal,
+ "Existing WindowGlobal"
+ );
+
+ const promise = browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .addWatcherDataEntry({
+ watcherActorID: watcher.actorID,
+ browserId: watcher.browserId,
+ type,
+ entries,
+ });
+ promises.push(promise);
+ }
+ // Await for the queries in order to try to resolve only *after* the remote code processed the new data
+ return Promise.all(promises);
+}
+
+/**
+ * Notify all existing frame targets that some data entries have been removed
+ *
+ * See addWatcherDataEntry for argument documentation.
+ */
+function removeWatcherDataEntry({ watcher, type, entries }) {
+ const browsingContexts = getWatchingBrowsingContexts(watcher);
+ for (const browsingContext of browsingContexts) {
+ logWindowGlobal(
+ browsingContext.currentWindowGlobal,
+ "Existing WindowGlobal"
+ );
+
+ browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .removeWatcherDataEntry({
+ watcherActorID: watcher.actorID,
+ browserId: watcher.browserId,
+ type,
+ entries,
+ });
+ }
+}
+
+module.exports = {
+ createTargets,
+ destroyTargets,
+ addWatcherDataEntry,
+ removeWatcherDataEntry,
+};
+
+/**
+ * Return the list of BrowsingContexts which should be targeted in order to communicate
+ * a new list of resource types to listen or stop listening to.
+ *
+ * @param WatcherActor watcher
+ * The watcher actor will be used to know which target we debug
+ * and what BrowsingContext should be considered.
+ */
+function getWatchingBrowsingContexts(watcher) {
+ // If we are watching for additional frame targets, it means that fission mode is enabled,
+ // either for a content toolbox or a BrowserToolbox via devtools.browsertoolbox.fission pref.
+ const watchingAdditionalTargets = WatcherRegistry.isWatchingTargets(
+ watcher,
+ Targets.TYPES.FRAME
+ );
+ const { browserElement } = watcher;
+ const browsingContexts = watchingAdditionalTargets
+ ? getFilteredRemoteBrowsingContext(browserElement)
+ : [];
+ // Even if we aren't watching additional target, we want to process the top level target.
+ // The top level target isn't returned by getFilteredRemoteBrowsingContext, so add it in both cases.
+ if (browserElement) {
+ const topBrowsingContext = browserElement.browsingContext;
+ // Ignore if we are against a page running in the parent process,
+ // which would not support JSWindowActor API
+ // XXX May be we should toggle `includeChrome` and ensure watch/unwatch works
+ // with such page?
+ if (topBrowsingContext.currentWindowGlobal.osPid != -1) {
+ browsingContexts.push(topBrowsingContext);
+ }
+ }
+ return browsingContexts;
+}
+
+/**
+ * Get the list of all BrowsingContext we should interact with.
+ * The precise condition of which BrowsingContext we should interact with are defined
+ * in `shouldNotifyWindowGlobal`
+ *
+ * @param BrowserElement browserElement (optional)
+ * If defined, this will restrict to only the Browsing Context matching this
+ * Browser Element and any of its (nested) children iframes.
+ */
+function getFilteredRemoteBrowsingContext(browserElement) {
+ return getAllRemoteBrowsingContexts(
+ browserElement?.browsingContext
+ ).filter(browsingContext =>
+ shouldNotifyWindowGlobal(browsingContext, browserElement?.browserId)
+ );
+}
+
+// Set to true to log info about about WindowGlobal's being watched.
+const DEBUG = false;
+
+function logWindowGlobal(windowGlobal, message) {
+ if (!DEBUG) {
+ return;
+ }
+
+ WindowGlobalLogger.logWindowGlobal(windowGlobal, message);
+}
diff --git a/devtools/server/actors/watcher/target-helpers/moz.build b/devtools/server/actors/watcher/target-helpers/moz.build
new file mode 100644
index 0000000000..92413d1f52
--- /dev/null
+++ b/devtools/server/actors/watcher/target-helpers/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(
+ "frame-helper.js",
+ "process-helper.js",
+ "utils.js",
+ "worker-helper.js",
+)
diff --git a/devtools/server/actors/watcher/target-helpers/process-helper.js b/devtools/server/actors/watcher/target-helpers/process-helper.js
new file mode 100644
index 0000000000..6a3e76f818
--- /dev/null
+++ b/devtools/server/actors/watcher/target-helpers/process-helper.js
@@ -0,0 +1,281 @@
+/* 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";
+
+const Services = require("Services");
+const {
+ WatcherRegistry,
+} = require("devtools/server/actors/watcher/WatcherRegistry.jsm");
+
+loader.lazyRequireGetter(
+ this,
+ "ChildDebuggerTransport",
+ "devtools/shared/transport/child-transport",
+ true
+);
+
+const CONTENT_PROCESS_SCRIPT =
+ "resource://devtools/server/startup/content-process-script.js";
+
+/**
+ * Map a MessageManager key to an Array of ContentProcessTargetActor "description" objects.
+ * A single MessageManager might be linked to several ContentProcessTargetActors if there are several
+ * Watcher actors instantiated on the DevToolsServer, via a single connection (in theory), but rather
+ * via distinct connections (ex: a content toolbox and the browser toolbox).
+ * Note that if we spawn two DevToolsServer, this module will be instantiated twice.
+ *
+ * Each ContentProcessTargetActor "description" object is structured as follows
+ * - {Object} actor: form of the content process target actor
+ * - {String} prefix: forwarding prefix used to redirect all packet to the right content process's transport
+ * - {ChildDebuggerTransport} childTransport: Transport forwarding all packets to the target's content process
+ * - {WatcherActor} watcher: The Watcher actor for which we instantiated this content process target actor
+ */
+const actors = new WeakMap();
+
+// Save the list of all watcher actors that are watching for processes
+const watchers = new Set();
+
+function onContentProcessActorCreated(msg) {
+ const { watcherActorID, prefix, actor } = msg.data;
+ const watcher = WatcherRegistry.getWatcher(watcherActorID);
+ if (!watcher) {
+ throw new Error(
+ `Receiving a content process actor without a watcher actor ${watcherActorID}`
+ );
+ }
+ // Ignore watchers of other connections.
+ // We may have two browser toolbox connected to the same process.
+ // This will spawn two distinct Watcher actor and two distinct process target helper module.
+ // Avoid processing the event many times, otherwise we will notify about the same target
+ // multiple times.
+ if (!watchers.has(watcher)) {
+ return;
+ }
+ const messageManager = msg.target;
+ const connection = watcher.conn;
+
+ // Pipe Debugger message from/to parent/child via the message manager
+ const childTransport = new ChildDebuggerTransport(messageManager, prefix);
+ childTransport.hooks = {
+ onPacket: connection.send.bind(connection),
+ };
+ childTransport.ready();
+
+ connection.setForwarding(prefix, childTransport);
+
+ const list = actors.get(messageManager) || [];
+ list.push({
+ prefix,
+ childTransport,
+ actor,
+ watcher,
+ });
+ actors.set(messageManager, list);
+
+ watcher.notifyTargetAvailable(actor);
+}
+
+function onMessageManagerClose(messageManager, topic, data) {
+ const list = actors.get(messageManager);
+ if (!list || list.length == 0) {
+ return;
+ }
+ for (const { prefix, childTransport, actor, watcher } of list) {
+ watcher.notifyTargetDestroyed(actor);
+
+ // If we have a child transport, the actor has already
+ // been created. We need to stop using this message manager.
+ childTransport.close();
+ watcher.conn.cancelForwarding(prefix);
+ }
+ actors.delete(messageManager);
+}
+
+function closeWatcherTransports(watcher) {
+ for (let i = 0; i < Services.ppmm.childCount; i++) {
+ const messageManager = Services.ppmm.getChildAt(i);
+ let list = actors.get(messageManager);
+ if (!list || list.length == 0) {
+ continue;
+ }
+ list = list.filter(item => item.watcher != watcher);
+ for (const item of list) {
+ // If we have a child transport, the actor has already
+ // been created. We need to stop using this message manager.
+ item.childTransport.close();
+ watcher.conn.cancelForwarding(item.prefix);
+ }
+ if (list.length == 0) {
+ actors.delete(messageManager);
+ } else {
+ actors.set(messageManager, list);
+ }
+ }
+}
+
+function maybeRegisterMessageListeners(watcher) {
+ const sizeBefore = watchers.size;
+ watchers.add(watcher);
+ if (sizeBefore == 0 && watchers.size == 1) {
+ Services.ppmm.addMessageListener(
+ "debug:content-process-actor",
+ onContentProcessActorCreated
+ );
+ Services.obs.addObserver(onMessageManagerClose, "message-manager-close");
+
+ // Load the content process server startup script only once,
+ // otherwise it will be evaluated twice, listen to events twice and create
+ // target actors twice.
+ // We may try to load it twice when opening one Browser Toolbox via about:debugging
+ // and another regular Browser Toolbox. Both will spawn a WatcherActor and watch for processes.
+ const isContentProcessScripLoaded = Services.ppmm
+ .getDelayedProcessScripts()
+ .some(([uri]) => uri === CONTENT_PROCESS_SCRIPT);
+ if (!isContentProcessScripLoaded) {
+ Services.ppmm.loadProcessScript(CONTENT_PROCESS_SCRIPT, true);
+ }
+ }
+}
+function maybeUnregisterMessageListeners(watcher) {
+ const sizeBefore = watchers.size;
+ watchers.delete(watcher);
+ closeWatcherTransports(watcher);
+
+ if (sizeBefore == 1 && watchers.size == 0) {
+ Services.ppmm.removeMessageListener(
+ "debug:content-process-actor",
+ onContentProcessActorCreated
+ );
+ Services.obs.removeObserver(onMessageManagerClose, "message-manager-close");
+
+ // We inconditionally remove the process script, while we should only remove it
+ // once the last DevToolsServer stop watching for processes.
+ // We might have many server, using distinct loaders, so that this module
+ // will be spawn many times and we should remove the script only once the last
+ // module unregister the last watcher of all.
+ Services.ppmm.removeDelayedProcessScript(CONTENT_PROCESS_SCRIPT);
+
+ Services.ppmm.broadcastAsyncMessage("debug:destroy-process-script");
+ }
+}
+
+async function createTargets(watcher) {
+ // XXX: Should this move to WatcherRegistry??
+ maybeRegisterMessageListeners(watcher);
+
+ // Bug 1648499: This could be simplified when migrating to JSProcessActor by using sendQuery.
+ // For now, hack into WatcherActor in order to know when we created one target
+ // actor for each existing content process.
+ // Also, we substract one as the parent process has a message manager and is counted
+ // in `childCount`, but we ignore it from the process script and it won't reply.
+ const contentProcessCount = Services.ppmm.childCount - 1;
+ if (contentProcessCount == 0) {
+ return;
+ }
+ const onTargetsCreated = new Promise(resolve => {
+ let receivedTargetCount = 0;
+ const listener = () => {
+ if (++receivedTargetCount == contentProcessCount) {
+ watcher.off("target-available-form", listener);
+ resolve();
+ }
+ };
+ watcher.on("target-available-form", listener);
+ });
+
+ Services.ppmm.broadcastAsyncMessage("debug:instantiate-already-available", {
+ watcherActorID: watcher.actorID,
+ connectionPrefix: watcher.conn.prefix,
+ watchedData: watcher.watchedData,
+ });
+
+ await onTargetsCreated;
+}
+
+function destroyTargets(watcher) {
+ maybeUnregisterMessageListeners(watcher);
+
+ Services.ppmm.broadcastAsyncMessage("debug:destroy-target", {
+ watcherActorID: watcher.actorID,
+ });
+}
+
+/**
+ * Go over all existing content processes in order to communicate about new data entries
+ *
+ * @param {Object} options
+ * @param {WatcherActor} options.watcher
+ * The Watcher Actor providing new data entries
+ * @param {string} options.type
+ * The type of data to be added
+ * @param {Array<Object>} options.entries
+ * The values to be added to this type of data
+ */
+async function addWatcherDataEntry({ watcher, type, entries }) {
+ let expectedCount = Services.ppmm.childCount - 1;
+ if (expectedCount == 0) {
+ return;
+ }
+ const onAllReplied = new Promise(resolve => {
+ let count = 0;
+ const listener = msg => {
+ if (msg.data.watcherActorID != watcher.actorID) {
+ return;
+ }
+ count++;
+ maybeResolve();
+ };
+ Services.ppmm.addMessageListener(
+ "debug:add-watcher-data-entry-done",
+ listener
+ );
+ const onContentProcessClosed = (messageManager, topic, data) => {
+ expectedCount--;
+ maybeResolve();
+ };
+ const maybeResolve = () => {
+ if (count == expectedCount) {
+ Services.ppmm.removeMessageListener(
+ "debug:add-watcher-data-entry-done",
+ listener
+ );
+ Services.obs.removeObserver(
+ onContentProcessClosed,
+ "message-manager-close"
+ );
+ resolve();
+ }
+ };
+ Services.obs.addObserver(onContentProcessClosed, "message-manager-close");
+ });
+
+ Services.ppmm.broadcastAsyncMessage("debug:add-watcher-data-entry", {
+ watcherActorID: watcher.actorID,
+ type,
+ entries,
+ });
+
+ await onAllReplied;
+}
+
+/**
+ * Notify all existing content processes that some data entries have been removed
+ *
+ * See addWatcherDataEntry for argument documentation.
+ */
+function removeWatcherDataEntry({ watcher, type, entries }) {
+ Services.ppmm.broadcastAsyncMessage("debug:remove-watcher-data-entry", {
+ watcherActorID: watcher.actorID,
+ type,
+ entries,
+ });
+}
+
+module.exports = {
+ createTargets,
+ destroyTargets,
+ addWatcherDataEntry,
+ removeWatcherDataEntry,
+};
diff --git a/devtools/server/actors/watcher/target-helpers/utils.js b/devtools/server/actors/watcher/target-helpers/utils.js
new file mode 100644
index 0000000000..f59bbceed3
--- /dev/null
+++ b/devtools/server/actors/watcher/target-helpers/utils.js
@@ -0,0 +1,126 @@
+/* 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";
+
+const Services = require("Services");
+
+/**
+ * Helper function to know if a given WindowGlobal should be exposed via watchTargets API
+ * XXX: We probably want to share this function with DevToolsFrameChild,
+ * but may be not, it looks like the checks are really differents because WindowGlobalParent and WindowGlobalChild
+ * expose very different attributes. (WindowGlobalChild exposes much less!)
+ *
+ * @param {BrowsingContext} browsingContext: The browsing context we want to check the window global for
+ * @param {String} watchedBrowserId
+ * @param {Object} options
+ * @param {Boolean} options.acceptNonRemoteFrame: Set to true to not restrict to remote frame only
+ */
+function shouldNotifyWindowGlobal(
+ browsingContext,
+ watchedBrowserId,
+ options = {}
+) {
+ const windowGlobal = browsingContext.currentWindowGlobal;
+ // Loading or destroying BrowsingContext won't have any associated WindowGlobal.
+ // Ignore them. They should be either handled via DOMWindowCreated event or JSWindowActor destroy
+ if (!windowGlobal) {
+ return false;
+ }
+ // Ignore extension for now as attaching to them is special.
+ if (browsingContext.currentRemoteType == "extension") {
+ return false;
+ }
+ // Ignore globals running in the parent process for now as they won't be in a distinct process anyway.
+ // And JSWindowActor will most likely only be created if we toggle includeChrome
+ // on the JSWindowActor registration.
+ if (windowGlobal.osPid == -1 && windowGlobal.isInProcess) {
+ return false;
+ }
+ // Ignore about:blank which are quickly replaced and destroyed by the final URI
+ // bug 1625026 aims at removing this workaround and allow debugging any about:blank load
+ if (
+ windowGlobal.documentURI &&
+ windowGlobal.documentURI.spec == "about:blank"
+ ) {
+ return false;
+ }
+
+ if (watchedBrowserId && browsingContext.browserId != watchedBrowserId) {
+ return false;
+ }
+
+ if (options.acceptNonRemoteFrame) {
+ return true;
+ }
+
+ // If `acceptNonRemoteFrame` options isn't true, only mention the "remote frames".
+ // i.e. the frames which are in a distinct process compared to their parent document
+ return (
+ !browsingContext.parent ||
+ windowGlobal.osPid != browsingContext.parent.currentWindowGlobal.osPid
+ );
+}
+
+/**
+ * Get all the BrowsingContexts.
+ *
+ * Really all of them:
+ * - For all the privileged windows (browser.xhtml, browser console, ...)
+ * - For all chrome *and* content contexts (privileged windows, as well as <browser> elements and their inner content documents)
+ * - For all nested browsing context. We fetch the contexts recursively.
+ *
+ * @param BrowsingContext topBrowsingContext (optional)
+ * If defined, this will restrict to this Browsing Context only
+ * and any of its (nested) children.
+ */
+function getAllRemoteBrowsingContexts(topBrowsingContext) {
+ const browsingContexts = [];
+
+ // For a given BrowsingContext, add the `browsingContext`
+ // all of its children, that, recursively.
+ function walk(browsingContext) {
+ if (browsingContexts.includes(browsingContext)) {
+ return;
+ }
+ browsingContexts.push(browsingContext);
+
+ for (const child of browsingContext.children) {
+ walk(child);
+ }
+
+ if (browsingContext.window) {
+ // If the document is in the parent process, also iterate over each <browser>'s browsing context.
+ // BrowsingContext.children doesn't cross chrome to content boundaries,
+ // so we have to cross these boundaries by ourself.
+ for (const browser of browsingContext.window.document.querySelectorAll(
+ `browser[remote="true"]`
+ )) {
+ walk(browser.browsingContext);
+ }
+ }
+ }
+
+ // If a Browsing Context is passed, only walk through the given BrowsingContext
+ if (topBrowsingContext) {
+ walk(topBrowsingContext);
+ // Remove the top level browsing context we just added by calling walk.
+ browsingContexts.shift();
+ } else {
+ // Fetch all top level window's browsing contexts
+ // Note that getWindowEnumerator works from all processes, including the content process.
+ for (const window of Services.ww.getWindowEnumerator()) {
+ if (window.docShell.browsingContext) {
+ walk(window.docShell.browsingContext);
+ }
+ }
+ }
+
+ return browsingContexts;
+}
+
+module.exports = {
+ getAllRemoteBrowsingContexts,
+ shouldNotifyWindowGlobal,
+};
diff --git a/devtools/server/actors/watcher/target-helpers/worker-helper.js b/devtools/server/actors/watcher/target-helpers/worker-helper.js
new file mode 100644
index 0000000000..1f0fa00b44
--- /dev/null
+++ b/devtools/server/actors/watcher/target-helpers/worker-helper.js
@@ -0,0 +1,144 @@
+/* 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";
+
+const {
+ getAllRemoteBrowsingContexts,
+ shouldNotifyWindowGlobal,
+} = require("devtools/server/actors/watcher/target-helpers/utils.js");
+
+const DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME = "DevToolsWorker";
+
+/**
+ * Force creating targets for all existing workers for a given Watcher Actor.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to watch for new targets.
+ */
+async function createTargets(watcher) {
+ // Go over all existing BrowsingContext in order to:
+ // - Force the instantiation of a DevToolsWorkerChild
+ // - Have the DevToolsWorkerChild to spawn the WorkerTargetActors
+ const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
+ const promises = [];
+ for (const browsingContext of browsingContexts) {
+ const promise = browsingContext.currentWindowGlobal
+ .getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME)
+ .instantiateWorkerTargets({
+ watcherActorID: watcher.actorID,
+ connectionPrefix: watcher.conn.prefix,
+ browserId: watcher.browserId,
+ watchedData: watcher.watchedData,
+ });
+ promises.push(promise);
+ }
+
+ // Await for the different queries in order to try to resolve only *after* we received
+ // the already available worker targets.
+ return Promise.all(promises);
+}
+
+/**
+ * Force destroying all worker targets which were related to a given watcher.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to stop watching for new targets.
+ */
+async function destroyTargets(watcher) {
+ // Go over all existing BrowsingContext in order to destroy all targets
+ const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
+ for (const browsingContext of browsingContexts) {
+ let windowActor;
+ try {
+ windowActor = browsingContext.currentWindowGlobal.getActor(
+ DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME
+ );
+ } catch (e) {
+ continue;
+ }
+
+ windowActor.destroyWorkerTargets({
+ watcher,
+ browserId: watcher.browserId,
+ });
+ }
+}
+
+/**
+ * Go over all existing BrowsingContext in order to communicate about new data entries
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to stop watching for new targets.
+ * @param string type
+ * The type of data to be added
+ * @param Array<Object> entries
+ * The values to be added to this type of data
+ */
+async function addWatcherDataEntry({ watcher, type, entries }) {
+ const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
+ const promises = [];
+ for (const browsingContext of browsingContexts) {
+ const promise = browsingContext.currentWindowGlobal
+ .getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME)
+ .addWatcherDataEntry({
+ watcherActorID: watcher.actorID,
+ browserId: watcher.browserId,
+ type,
+ entries,
+ });
+ promises.push(promise);
+ }
+ // Await for the queries in order to try to resolve only *after* the remote code processed the new data
+ return Promise.all(promises);
+}
+
+/**
+ * Notify all existing frame targets that some data entries have been removed
+ *
+ * See addWatcherDataEntry for argument documentation.
+ */
+function removeWatcherDataEntry({ watcher, type, entries }) {
+ const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
+ for (const browsingContext of browsingContexts) {
+ browsingContext.currentWindowGlobal
+ .getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME)
+ .removeWatcherDataEntry({
+ watcherActorID: watcher.actorID,
+ browserId: watcher.browserId,
+ type,
+ entries,
+ });
+ }
+}
+
+/**
+ * Get the list of all BrowsingContext we should interact with.
+ * The precise condition of which BrowsingContext we should interact with are defined
+ * in `shouldNotifyWindowGlobal`
+ *
+ * @param BrowserElement browserElement (optional)
+ * If defined, this will restrict to only the Browsing Context matching this
+ * Browser Element and any of its (nested) children iframes.
+ */
+function getFilteredBrowsingContext(browserElement) {
+ const browsingContexts = getAllRemoteBrowsingContexts(
+ browserElement?.browsingContext
+ );
+ if (browserElement?.browsingContext) {
+ browsingContexts.push(browserElement?.browsingContext);
+ }
+ return browsingContexts.filter(browsingContext =>
+ shouldNotifyWindowGlobal(browsingContext, browserElement?.browserId, {
+ acceptNonRemoteFrame: true,
+ })
+ );
+}
+
+module.exports = {
+ createTargets,
+ destroyTargets,
+ addWatcherDataEntry,
+ removeWatcherDataEntry,
+};