summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/watcher
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/watcher')
-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
8 files changed, 1251 insertions, 0 deletions
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,
+};