summaryrefslogtreecommitdiffstats
path: root/devtools/server/connectors
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/connectors')
-rw-r--r--devtools/server/connectors/content-process-connector.js124
-rw-r--r--devtools/server/connectors/frame-connector.js216
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs691
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs272
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs551
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs293
-rw-r--r--devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs76
-rw-r--r--devtools/server/connectors/js-window-actor/moz.build13
-rw-r--r--devtools/server/connectors/moz.build15
-rw-r--r--devtools/server/connectors/worker-connector.js207
10 files changed, 2458 insertions, 0 deletions
diff --git a/devtools/server/connectors/content-process-connector.js b/devtools/server/connectors/content-process-connector.js
new file mode 100644
index 0000000000..5c8973f2c6
--- /dev/null
+++ b/devtools/server/connectors/content-process-connector.js
@@ -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";
+
+var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+var { dumpn } = DevToolsUtils;
+var {
+ createContentProcessSessionContext,
+} = require("resource://devtools/server/actors/watcher/session-context.js");
+
+loader.lazyRequireGetter(
+ this,
+ "ChildDebuggerTransport",
+ "resource://devtools/shared/transport/child-transport.js",
+ true
+);
+
+const CONTENT_PROCESS_SERVER_STARTUP_SCRIPT =
+ "resource://devtools/server/startup/content-process.js";
+
+loader.lazyRequireGetter(
+ this,
+ "EventEmitter",
+ "resource://devtools/shared/event-emitter.js"
+);
+
+/**
+ * Start a DevTools server in a content process (representing the entire process, not
+ * just a single frame) and add it as a child server for an active connection.
+ */
+function connectToContentProcess(connection, mm, onDestroy) {
+ return new Promise(resolve => {
+ const prefix = connection.allocID("content-process");
+ let actor, childTransport;
+
+ mm.addMessageListener("debug:content-process-actor", function listener(
+ msg
+ ) {
+ // Ignore actors being created by a Watcher actor,
+ // they will be handled by devtools/server/watcher/target-helpers/process.js
+ if (msg.watcherActorID) {
+ return;
+ }
+ mm.removeMessageListener("debug:content-process-actor", listener);
+
+ // Pipe Debugger message from/to parent/child via the message manager
+ childTransport = new ChildDebuggerTransport(mm, prefix);
+ childTransport.hooks = {
+ onPacket: connection.send.bind(connection),
+ };
+ childTransport.ready();
+
+ connection.setForwarding(prefix, childTransport);
+
+ dumpn(`Start forwarding for process with prefix ${prefix}`);
+
+ actor = msg.json.actor;
+
+ resolve(actor);
+ });
+
+ // Load the content process server startup script only once.
+ const isContentProcessServerStartupScripLoaded = Services.ppmm
+ .getDelayedProcessScripts()
+ .some(([uri]) => uri === CONTENT_PROCESS_SERVER_STARTUP_SCRIPT);
+ if (!isContentProcessServerStartupScripLoaded) {
+ // Load the process script that will receive the debug:init-content-server message
+ Services.ppmm.loadProcessScript(
+ CONTENT_PROCESS_SERVER_STARTUP_SCRIPT,
+ true
+ );
+ }
+
+ // Send a message to the content process server startup script to forward it the
+ // prefix.
+ mm.sendAsyncMessage("debug:init-content-server", {
+ prefix,
+ // This connector is only used for the Browser Content Toolbox,
+ // when creating the content process target from the Process Descriptor.
+ sessionContext: createContentProcessSessionContext(),
+ });
+
+ function onClose() {
+ Services.obs.removeObserver(
+ onMessageManagerClose,
+ "message-manager-close"
+ );
+ EventEmitter.off(connection, "closed", onClose);
+ if (childTransport) {
+ // If we have a child transport, the actor has already
+ // been created. We need to stop using this message manager.
+ childTransport.close();
+ childTransport = null;
+ connection.cancelForwarding(prefix);
+
+ // ... and notify the child process to clean the target-scoped actors.
+ try {
+ mm.sendAsyncMessage("debug:content-process-disconnect", { prefix });
+ } catch (e) {
+ // Nothing to do
+ }
+ }
+
+ if (onDestroy) {
+ onDestroy(mm);
+ }
+ }
+
+ const onMessageManagerClose = DevToolsUtils.makeInfallible(
+ (subject, topic, data) => {
+ if (subject == mm) {
+ onClose();
+ }
+ }
+ );
+ Services.obs.addObserver(onMessageManagerClose, "message-manager-close");
+
+ EventEmitter.on(connection, "closed", onClose);
+ });
+}
+
+exports.connectToContentProcess = connectToContentProcess;
diff --git a/devtools/server/connectors/frame-connector.js b/devtools/server/connectors/frame-connector.js
new file mode 100644
index 0000000000..4c16df4a79
--- /dev/null
+++ b/devtools/server/connectors/frame-connector.js
@@ -0,0 +1,216 @@
+/* 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";
+
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var { dumpn } = DevToolsUtils;
+
+loader.lazyRequireGetter(
+ this,
+ "DevToolsServer",
+ "resource://devtools/server/devtools-server.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "ChildDebuggerTransport",
+ "resource://devtools/shared/transport/child-transport.js",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "EventEmitter",
+ "resource://devtools/shared/event-emitter.js"
+);
+
+/**
+ * Start a DevTools server in a remote frame's process and add it as a child server for
+ * an active connection.
+ *
+ * @param object connection
+ * The devtools server connection to use.
+ * @param Element frame
+ * The frame element with remote content to connect to.
+ * @param function [onDestroy]
+ * Optional function to invoke when the child process closes or the connection
+ * shuts down. (Need to forget about the related target actor.)
+ * @return object
+ * A promise object that is resolved once the connection is established.
+ */
+function connectToFrame(
+ connection,
+ frame,
+ onDestroy,
+ { addonId, addonBrowsingContextGroupId } = {}
+) {
+ return new Promise(resolve => {
+ // Get messageManager from XUL browser (which might be a specialized tunnel for RDM)
+ // or else fallback to asking the frameLoader itself.
+ const mm = frame.messageManager || frame.frameLoader.messageManager;
+ mm.loadFrameScript("resource://devtools/server/startup/frame.js", false);
+
+ const trackMessageManager = () => {
+ mm.addMessageListener("debug:setup-in-parent", onSetupInParent);
+ if (!actor) {
+ mm.addMessageListener("debug:actor", onActorCreated);
+ }
+ };
+
+ const untrackMessageManager = () => {
+ mm.removeMessageListener("debug:setup-in-parent", onSetupInParent);
+ if (!actor) {
+ mm.removeMessageListener("debug:actor", onActorCreated);
+ }
+ };
+
+ let actor, childTransport;
+ const prefix = connection.allocID("child");
+ // Compute the same prefix that's used by DevToolsServerConnection
+ const connPrefix = prefix + "/";
+
+ // provides hook to actor modules that need to exchange messages
+ // between e10s parent and child processes
+ const parentModules = [];
+ const onSetupInParent = function(msg) {
+ // We may have multiple connectToFrame instance running for the same frame and
+ // need to filter the messages.
+ if (msg.json.prefix != connPrefix) {
+ return false;
+ }
+
+ const { module, setupParent } = msg.json;
+ let m;
+
+ try {
+ m = require(module);
+
+ if (!(setupParent in m)) {
+ dumpn(`ERROR: module '${module}' does not export '${setupParent}'`);
+ return false;
+ }
+
+ parentModules.push(m[setupParent]({ mm, prefix: connPrefix }));
+
+ return true;
+ } catch (e) {
+ const errorMessage =
+ "Exception during actor module setup running in the parent process: ";
+ DevToolsUtils.reportException(errorMessage + e);
+ dumpn(
+ `ERROR: ${errorMessage}\n\t module: '${module}'\n\t ` +
+ `setupParent: '${setupParent}'\n${DevToolsUtils.safeErrorString(e)}`
+ );
+ return false;
+ }
+ };
+
+ const onActorCreated = DevToolsUtils.makeInfallible(function(msg) {
+ if (msg.json.prefix != prefix) {
+ return;
+ }
+ mm.removeMessageListener("debug:actor", onActorCreated);
+
+ // Pipe Debugger message from/to parent/child via the message manager
+ childTransport = new ChildDebuggerTransport(mm, prefix);
+ childTransport.hooks = {
+ // Pipe all the messages from content process actors back to the client
+ // through the parent process connection.
+ onPacket: connection.send.bind(connection),
+ };
+ childTransport.ready();
+
+ connection.setForwarding(prefix, childTransport);
+
+ dumpn(`Start forwarding for frame with prefix ${prefix}`);
+
+ actor = msg.json.actor;
+ resolve(actor);
+ });
+
+ const destroy = DevToolsUtils.makeInfallible(function() {
+ EventEmitter.off(connection, "closed", destroy);
+ Services.obs.removeObserver(
+ onMessageManagerClose,
+ "message-manager-close"
+ );
+
+ // provides hook to actor modules that need to exchange messages
+ // between e10s parent and child processes
+ parentModules.forEach(mod => {
+ if (mod.onDisconnected) {
+ mod.onDisconnected();
+ }
+ });
+ // TODO: Remove this deprecated path once it's no longer needed by add-ons.
+ DevToolsServer.emit("disconnected-from-child:" + connPrefix, {
+ mm,
+ prefix: connPrefix,
+ });
+
+ if (actor) {
+ actor = null;
+ }
+
+ // Notify the tab descriptor about the destruction before the call to
+ // `cancelForwarding`, so that we notify about the target destruction
+ // *before* we purge all request for this prefix.
+ // When we purge the requests, we also destroy all related fronts,
+ // including the target front. This clears all event listeners
+ // and ultimately prevent target-destroyed from firing.
+ if (onDestroy) {
+ onDestroy(mm);
+ }
+
+ if (childTransport) {
+ // If we have a child transport, the actor has already
+ // been created. We need to stop using this message manager.
+ childTransport.close();
+ childTransport = null;
+ connection.cancelForwarding(prefix);
+
+ // ... and notify the child process to clean the target-scoped actors.
+ try {
+ // Bug 1169643: Ignore any exception as the child process
+ // may already be destroyed by now.
+ mm.sendAsyncMessage("debug:disconnect", { prefix });
+ } catch (e) {
+ // Nothing to do
+ }
+ } else {
+ // Otherwise, the frame has been closed before the actor
+ // had a chance to be created, so we are not able to create
+ // the actor.
+ resolve(null);
+ }
+
+ // Cleanup all listeners
+ untrackMessageManager();
+ });
+
+ // Listen for various messages and frame events
+ trackMessageManager();
+
+ // Listen for app process exit
+ const onMessageManagerClose = function(subject, topic, data) {
+ if (subject == mm) {
+ destroy();
+ }
+ };
+ Services.obs.addObserver(onMessageManagerClose, "message-manager-close");
+
+ // Listen for connection close to cleanup things
+ // when user unplug the device or we lose the connection somehow.
+ EventEmitter.on(connection, "closed", destroy);
+
+ mm.sendAsyncMessage("debug:connect", {
+ prefix,
+ addonId,
+ addonBrowsingContextGroupId,
+ });
+ });
+}
+
+exports.connectToFrame = connectToFrame;
diff --git a/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs
new file mode 100644
index 0000000000..e4f6b19b01
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs
@@ -0,0 +1,691 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
+import * as Loader from "resource://devtools/shared/loader/Loader.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ isWindowGlobalPartOfContext:
+ "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
+ releaseDistinctSystemPrincipalLoader:
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
+ TargetActorRegistry:
+ "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
+ useDistinctSystemPrincipalLoader:
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
+ WindowGlobalLogger:
+ "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs",
+});
+
+const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
+ "devtools.every-frame-target.enabled",
+ false
+);
+
+// Name of the attribute into which we save data in `sharedData` object.
+const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
+
+// If true, log info about WindowGlobal's being created.
+const DEBUG = false;
+function logWindowGlobal(windowGlobal, message) {
+ if (!DEBUG) {
+ return;
+ }
+ lazy.WindowGlobalLogger.logWindowGlobal(windowGlobal, message);
+}
+
+export class DevToolsFrameChild extends JSWindowActorChild {
+ constructor() {
+ super();
+
+ // The map is indexed by the Watcher Actor ID.
+ // The values are objects containing the following properties:
+ // - connection: the DevToolsServerConnection itself
+ // - actor: the WindowGlobalTargetActor instance
+ this._connections = new Map();
+
+ EventEmitter.decorate(this);
+
+ // Set the following preference on the constructor, so that we can easily
+ // toggle these preferences on and off from tests and have the new value being picked up.
+
+ // bfcache-in-parent changes significantly how navigation behaves.
+ // We may start reusing previously existing WindowGlobal and so reuse
+ // previous set of JSWindowActor pairs (i.e. DevToolsFrameParent/DevToolsFrameChild).
+ // When enabled, regular navigations may also change and spawn new BrowsingContexts.
+ // If the page we navigate from supports being stored in bfcache,
+ // the navigation will use a new BrowsingContext. And so force spawning
+ // a new top-level target.
+ XPCOMUtils.defineLazyGetter(
+ this,
+ "isBfcacheInParentEnabled",
+ () =>
+ Services.appinfo.sessionHistoryInParent &&
+ Services.prefs.getBoolPref("fission.bfcacheInParent", false)
+ );
+ }
+
+ /**
+ * Try to instantiate new target actors for the current WindowGlobal, and that,
+ * for all the currently registered Watcher actors.
+ *
+ * Read the `sharedData` to get metadata about all registered watcher actors.
+ * If these watcher actors are interested in the current WindowGlobal,
+ * instantiate a new dedicated target actor for each of the watchers.
+ *
+ * @param Object options
+ * @param Boolean options.isBFCache
+ * True, if the request to instantiate a new target comes from a bfcache navigation.
+ * i.e. when we receive a pageshow event with persisted=true.
+ * This will be true regardless of bfcacheInParent being enabled or disabled.
+ * @param Boolean options.ignoreIfExisting
+ * By default to false. If true is passed, we avoid instantiating a target actor
+ * if one already exists for this windowGlobal.
+ */
+ instantiate({ isBFCache = false, ignoreIfExisting = false } = {}) {
+ const { sharedData } = Services.cpmm;
+ const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
+ if (!sessionDataByWatcherActor) {
+ throw new Error(
+ "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets"
+ );
+ }
+
+ // Create one Target actor for each prefix/client which listen to frames
+ for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
+ const { connectionPrefix, sessionContext } = sessionData;
+ // For bfcache navigations, we only create new targets when bfcacheInParent is enabled,
+ // as this would be the only case where new DocShells will be created. This requires us to spawn a
+ // new WindowGlobalTargetActor as one such actor is bound to a unique DocShell.
+ const forceAcceptTopLevelTarget =
+ isBFCache && this.isBfcacheInParentEnabled;
+ if (
+ sessionData.targets?.includes("frame") &&
+ lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, {
+ forceAcceptTopLevelTarget,
+ })
+ ) {
+ // If this was triggered because of a navigation, we want to retrieve the existing
+ // target we were debugging so we can destroy it before creating the new target.
+ // This is important because we had cases where the destruction of an old target
+ // was unsetting a flag on the **new** target document, breaking the toolbox (See Bug 1721398).
+
+ // We're checking for an existing target given a watcherActorID + browserId + browsingContext
+ // Note that a target switch might create a new browsing context, so we wouldn't
+ // retrieve the existing target here. We are okay with this as:
+ // - this shouldn't happen much
+ // - in such case we weren't seeing the issue of Bug 1721398 (the old target can't access the new document)
+ const existingTarget = this._findTargetActor({
+ watcherActorID,
+ sessionContext,
+ browsingContextId: this.manager.browsingContext.id,
+ });
+
+ // See comment in handleEvent(DOMDocElementInserted) to know why we try to
+ // create targets if none already exists
+ if (existingTarget && ignoreIfExisting) {
+ continue;
+ }
+
+ // Bail if there is already an existing WindowGlobalTargetActor which wasn't
+ // created from a JSWIndowActor.
+ // This means we are reloading or navigating (same-process) a Target
+ // which has not been created using the Watcher, but from the client (most likely
+ // the initial target of a local-tab toolbox).
+ // However, we force overriding the first message manager based target in case of
+ // BFCache navigations.
+ if (
+ existingTarget &&
+ !existingTarget.createdFromJsWindowActor &&
+ !isBFCache
+ ) {
+ continue;
+ }
+
+ // If we decide to instantiate a new target and there was one before,
+ // first destroy the previous one.
+ // Otherwise its destroy sequence will be executed *after* the new one
+ // is being initialized and may easily revert changes made against platform API.
+ // (typically toggle platform boolean attributes back to default…)
+ if (existingTarget) {
+ existingTarget.destroy({ isTargetSwitching: true });
+ }
+
+ this._createTargetActor({
+ watcherActorID,
+ parentConnectionPrefix: connectionPrefix,
+ sessionData,
+ isDocumentCreation: true,
+ });
+ }
+ }
+ }
+
+ /**
+ * Instantiate a new WindowGlobalTarget for the given connection.
+ *
+ * @param Object options
+ * @param String options.watcherActorID
+ * The ID of the WatcherActor who requested to observe and create these target actors.
+ * @param String options.parentConnectionPrefix
+ * The prefix of the DevToolsServerConnection of the Watcher Actor.
+ * This is used to compute a unique ID for the target actor.
+ * @param Object options.sessionData
+ * All data managed by the Watcher Actor and WatcherRegistry.jsm, containing
+ * target types, resources types to be listened as well as breakpoints and any
+ * other data meant to be shared across processes and threads.
+ * @param Boolean options.isDocumentCreation
+ * Set to true if the function is called from `instantiate`, i.e. when we're
+ * handling a new document being created.
+ * @param Boolean options.fromInstantiateAlreadyAvailable
+ * Set to true if the function is called from handling `DevToolsFrameParent:instantiate-already-available`
+ * query.
+ */
+ _createTargetActor({
+ watcherActorID,
+ parentConnectionPrefix,
+ sessionData,
+ isDocumentCreation,
+ fromInstantiateAlreadyAvailable,
+ }) {
+ if (this._connections.get(watcherActorID)) {
+ // If this function is called as a result of a `DevToolsFrameParent:instantiate-already-available`
+ // message, we might have a legitimate race condition:
+ // In frame-helper, we want to create the initial targets for a given browser element.
+ // It might happen that the `DevToolsFrameParent:instantiate-already-available` is
+ // aborted if the page navigates (and the document is destroyed) while the query is still pending.
+ // In such case, frame-helper will try to send a new message. In the meantime,
+ // the DevToolsFrameChild `DOMWindowCreated` handler may already have been called and
+ // the new target already created.
+ // We don't want to throw in such case, as our end-goal, having a target for the document,
+ // is achieved.
+ if (fromInstantiateAlreadyAvailable) {
+ return;
+ }
+ throw new Error(
+ "DevToolsFrameChild _createTargetActor was called more than once" +
+ ` for the same Watcher (Actor ID: "${watcherActorID}")`
+ );
+ }
+
+ // Compute a unique prefix, just for this WindowGlobal,
+ // which will be used to create a JSWindowActorTransport pair between content and parent processes.
+ // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`,
+ // but here, we can't have access to any DevTools connection as we are really early in the content process startup
+ // XXX: WindowGlobal's innerWindowId should be unique across processes, I think. So that should be safe?
+ // (this.manager == WindowGlobalChild interface)
+ const forwardingPrefix =
+ parentConnectionPrefix + "windowGlobal" + this.manager.innerWindowId;
+
+ logWindowGlobal(
+ this.manager,
+ "Instantiate WindowGlobalTarget with prefix: " + forwardingPrefix
+ );
+
+ const { connection, targetActor } = this._createConnectionAndActor(
+ forwardingPrefix,
+ sessionData
+ );
+ const form = targetActor.form();
+ // Ensure unregistering and destroying the related DevToolsServerConnection+Transport
+ // on both content and parent process JSWindowActors.
+ targetActor.once("destroyed", options => {
+ // This will destroy the content process one
+ this._destroyTargetActor(watcherActorID, options);
+ // And this will destroy the parent process one
+ try {
+ this.sendAsyncMessage("DevToolsFrameChild:destroy", {
+ actors: [
+ {
+ watcherActorID,
+ form,
+ },
+ ],
+ options,
+ });
+ } catch (e) {
+ // Ignore exception when the JSWindowActorChild has already been destroyed.
+ // We often try to emit this message while the WindowGlobal is in the process of being
+ // destroyed. We eagerly destroy the target actor during reloads,
+ // just before the WindowGlobal starts destroying, but sendAsyncMessage
+ // doesn't have time to complete and throws.
+ if (
+ !e.message.includes("JSWindowActorChild cannot send at the moment")
+ ) {
+ throw e;
+ }
+ }
+ });
+ this._connections.set(watcherActorID, {
+ connection,
+ actor: targetActor,
+ });
+
+ // Immediately queue a message for the parent process,
+ // in order to ensure that the JSWindowActorTransport is instantiated
+ // before any packet is sent from the content process.
+ // As the order of messages is guaranteed to be delivered in the order they
+ // were queued, we don't have to wait for anything around this sendAsyncMessage call.
+ // In theory, the WindowGlobalTargetActor may emit events in its constructor.
+ // If it does, such RDP packets may be lost.
+ // The important point here is to send this message before processing the sessionData,
+ // which will start the Watcher and start emitting resources on the target actor.
+ this.sendAsyncMessage("DevToolsFrameChild:connectFromContent", {
+ watcherActorID,
+ forwardingPrefix,
+ actor: targetActor.form(),
+ });
+
+ // Pass initialization data to the target actor
+ for (const type in sessionData) {
+ // `sessionData` will also contain `browserId` as well as entries with empty arrays,
+ // which shouldn't be processed.
+ const entries = sessionData[type];
+ if (!Array.isArray(entries) || !entries.length) {
+ continue;
+ }
+ targetActor.addSessionDataEntry(type, entries, isDocumentCreation);
+ }
+ }
+
+ /**
+ * @param {string} watcherActorID
+ * @param {object} options
+ * @param {boolean} options.isModeSwitching
+ * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
+ */
+ _destroyTargetActor(watcherActorID, options) {
+ const connectionInfo = this._connections.get(watcherActorID);
+ // This connection has already been cleaned?
+ if (!connectionInfo) {
+ throw new Error(
+ `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
+ );
+ }
+ connectionInfo.connection.close(options);
+ this._connections.delete(watcherActorID);
+ if (this._connections.size == 0) {
+ this.didDestroy(options);
+ }
+ }
+
+ _createConnectionAndActor(forwardingPrefix, sessionData) {
+ this.useCustomLoader = this.document.nodePrincipal.isSystemPrincipal;
+
+ if (!this.loader) {
+ // When debugging chrome pages, use a new dedicated loader, using a distinct chrome compartment.
+ this.loader = this.useCustomLoader
+ ? lazy.useDistinctSystemPrincipalLoader(this)
+ : Loader;
+ }
+ const { DevToolsServer } = this.loader.require(
+ "resource://devtools/server/devtools-server.js"
+ );
+
+ const { WindowGlobalTargetActor } = this.loader.require(
+ "resource://devtools/server/actors/targets/window-global.js"
+ );
+
+ DevToolsServer.init();
+
+ // We want a special server without any root actor and only target-scoped actors.
+ // We are going to spawn a WindowGlobalTargetActor instance in the next few lines,
+ // it is going to act like a root actor without being one.
+ DevToolsServer.registerActors({ target: true });
+
+ const connection = DevToolsServer.connectToParentWindowActor(
+ this,
+ forwardingPrefix
+ );
+
+ // In the case of the browser toolbox, tab's BrowsingContext don't have
+ // any parent BC and shouldn't be considered as top-level.
+ // This is why we check for browserId's.
+ const browsingContext = this.manager.browsingContext;
+ const isTopLevelTarget =
+ !browsingContext.parent &&
+ browsingContext.browserId == sessionData.sessionContext.browserId;
+
+ // Create the actual target actor.
+ const targetActor = new WindowGlobalTargetActor(connection, {
+ docShell: this.docShell,
+ // Targets created from the server side, via Watcher actor and DevToolsFrame JSWindow
+ // actor pair are following WindowGlobal lifecycle. i.e. will be destroyed on any
+ // type of navigation/reload.
+ // Note that if devtools.target-switching.server.enabled is false, the top level target
+ // won't be created via the codepath. Except if we have a bfcache-in-parent navigation.
+ followWindowGlobalLifeCycle: true,
+ isTopLevelTarget,
+ ignoreSubFrames: isEveryFrameTargetEnabled,
+ sessionContext: sessionData.sessionContext,
+ });
+ targetActor.manage(targetActor);
+ targetActor.createdFromJsWindowActor = true;
+
+ return { connection, targetActor };
+ }
+
+ /**
+ * Supported Queries
+ */
+
+ sendPacket(packet, prefix) {
+ this.sendAsyncMessage("DevToolsFrameChild:packet", { packet, prefix });
+ }
+
+ /**
+ * JsWindowActor API
+ */
+
+ async sendQuery(msg, args) {
+ try {
+ const res = await super.sendQuery(msg, args);
+ return res;
+ } catch (e) {
+ console.error("Failed to sendQuery in DevToolsFrameChild", msg);
+ console.error(e.toString());
+ throw e;
+ }
+ }
+
+ receiveMessage(message) {
+ // Assert that the message is intended for this window global,
+ // except for "packet" messages which use a dedicated routing
+ if (
+ message.name != "DevToolsFrameParent:packet" &&
+ message.data.sessionContext.type == "browser-element"
+ ) {
+ const { browserId } = message.data.sessionContext;
+ // Re-check here, just to ensure that both parent and content processes agree
+ // on what should or should not be watched.
+ if (
+ this.manager.browsingContext.browserId != browserId &&
+ !lazy.isWindowGlobalPartOfContext(
+ this.manager,
+ message.data.sessionContext,
+ {
+ forceAcceptTopLevelTarget: true,
+ }
+ )
+ ) {
+ throw new Error(
+ "Mismatch between DevToolsFrameParent and DevToolsFrameChild " +
+ (this.manager.browsingContext.browserId == browserId
+ ? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)"
+ : `expected browsing context with browserId ${browserId}, but got ${this.manager.browsingContext.browserId}`)
+ );
+ }
+ }
+ switch (message.name) {
+ case "DevToolsFrameParent:instantiate-already-available": {
+ const { watcherActorID, connectionPrefix, sessionData } = message.data;
+
+ return this._createTargetActor({
+ watcherActorID,
+ parentConnectionPrefix: connectionPrefix,
+ sessionData,
+ fromInstantiateAlreadyAvailable: true,
+ });
+ }
+ case "DevToolsFrameParent:destroy": {
+ const { watcherActorID, options } = message.data;
+ return this._destroyTargetActor(watcherActorID, options);
+ }
+ case "DevToolsFrameParent:addSessionDataEntry": {
+ const { watcherActorID, sessionContext, type, entries } = message.data;
+ return this._addSessionDataEntry(
+ watcherActorID,
+ sessionContext,
+ type,
+ entries
+ );
+ }
+ case "DevToolsFrameParent:removeSessionDataEntry": {
+ const { watcherActorID, sessionContext, type, entries } = message.data;
+ return this._removeSessionDataEntry(
+ watcherActorID,
+ sessionContext,
+ type,
+ entries
+ );
+ }
+ case "DevToolsFrameParent:packet":
+ return this.emit("packet-received", message);
+ default:
+ throw new Error(
+ "Unsupported message in DevToolsFrameParent: " + message.name
+ );
+ }
+ }
+
+ /**
+ * Return an existing target given a WatcherActor id, a browserId and an optional
+ * browsing context id.
+ * /!\ Note that we may have multiple targets for a given (watcherActorId, browserId) couple,
+ * for example if we have 2 remote iframes sharing the same origin, which is why you
+ * might want to pass a specific browsing context id to filter the list down.
+ *
+ * @param {Object} options
+ * @param {Object} options.watcherActorID
+ * @param {Object} options.sessionContext
+ * @param {Object} options.browsingContextId: Optional browsing context id to narrow the
+ * search to a specific browsing context.
+ *
+ * @returns {WindowGlobalTargetActor|null}
+ */
+ _findTargetActor({ watcherActorID, sessionContext, browsingContextId }) {
+ // First let's check if a target was created for this watcher actor in this specific
+ // DevToolsFrameChild instance.
+ const connectionInfo = this._connections.get(watcherActorID);
+ const targetActor = connectionInfo ? connectionInfo.actor : null;
+ if (targetActor) {
+ return targetActor;
+ }
+
+ // If we couldn't find such target, we want to see if a target was created for this
+ // (watcherActorId,browserId, {browsingContextId}) in another DevToolsFrameChild instance.
+ // This might be the case if we're navigating to a new page with server side target
+ // enabled and we want to retrieve the target of the page we're navigating from.
+ if (
+ lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, {
+ forceAcceptTopLevelTarget: true,
+ })
+ ) {
+ // Ensure retrieving the one target actor related to this connection.
+ // This allows to distinguish actors created for various toolboxes.
+ // For ex, regular toolbox versus browser console versus browser toolbox
+ const connectionPrefix = watcherActorID.replace(/watcher\d+$/, "");
+ const targetActors = lazy.TargetActorRegistry.getTargetActors(
+ sessionContext,
+ connectionPrefix
+ );
+
+ if (!browsingContextId) {
+ return targetActors[0] || null;
+ }
+ return targetActors.find(
+ actor => actor.browsingContextID === browsingContextId
+ );
+ }
+ return null;
+ }
+
+ _addSessionDataEntry(watcherActorID, sessionContext, type, entries) {
+ // /!\ We may have an issue here as there could be multiple targets for a given
+ // (watcherActorID,browserId) pair.
+ // This should be clarified as part of Bug 1725623.
+ const targetActor = this._findTargetActor({
+ watcherActorID,
+ sessionContext,
+ });
+
+ if (!targetActor) {
+ throw new Error(
+ `No target actor for this Watcher Actor ID:"${watcherActorID}" / BrowserId:${sessionContext.browserId}`
+ );
+ }
+ return targetActor.addSessionDataEntry(type, entries);
+ }
+
+ _removeSessionDataEntry(watcherActorID, sessionContext, type, entries) {
+ // /!\ We may have an issue here as there could be multiple targets for a given
+ // (watcherActorID,browserId) pair.
+ // This should be clarified as part of Bug 1725623.
+ const targetActor = this._findTargetActor({
+ watcherActorID,
+ sessionContext,
+ });
+ // By the time we are calling this, the target may already have been destroyed.
+ if (targetActor) {
+ return targetActor.removeSessionDataEntry(type, entries);
+ }
+ return null;
+ }
+
+ handleEvent({ type, persisted, target }) {
+ // Ignore any event that may fire for children WindowGlobals/documents
+ if (target != this.document) {
+ return;
+ }
+
+ // DOMWindowCreated is registered from FrameWatcher via `ActorManagerParent.addJSWindowActors`
+ // as a DOM event to be listened to and so is fired by JS Window Actor code platform code.
+ if (type == "DOMWindowCreated") {
+ this.instantiate();
+ return;
+ }
+ // We might have ignored the DOMWindowCreated event because it was the initial about:blank document.
+ // But when loading same-process iframes, we reuse the WindowGlobal of the about:bank document
+ // to load the actual URL loaded in the iframe. This means we won't have a new DOMWindowCreated
+ // for the actual document. There is a DOMDocElementInserted fired just after, that we can catch
+ // to create a target for same-process iframes.
+ // This means that we still do not create any target for the initial documents.
+ // It is complex to instantiate targets for initial documents because:
+ // - it would mean spawning two targets for the same WindowGlobal and sharing the same innerWindowId
+ // - or have WindowGlobalTargets to handle more than one document (it would mean reusing will-navigate/window-ready events
+ // both on client and server)
+ if (type == "DOMDocElementInserted") {
+ this.instantiate({ ignoreIfExisting: true });
+ return;
+ }
+
+ // If persisted=true, this is a BFCache navigation.
+ //
+ // With Fission enabled and bfcacheInParent, BFCache navigations will spawn a new DocShell
+ // in the same process:
+ // * the previous page won't be destroyed, its JSWindowActor will stay alive (`didDestroy` won't be called)
+ // and a 'pagehide' with persisted=true will be emitted on it.
+ // * the new page page won't emit any DOMWindowCreated, but instead a pageshow with persisted=true
+ // will be emitted.
+
+ if (type === "pageshow" && persisted) {
+ // Notify all bfcache navigations, even the one for which we don't create a new target
+ // as that's being useful for parent process storage resource watchers.
+ this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pageshow");
+
+ // Here we are going to re-instantiate a target that got destroyed before while processing a pagehide event.
+ // We force instantiating a new top level target, within `instantiate()` even if server targets are disabled.
+ // But we only do that if bfcacheInParent is enabled. Otherwise for same-process, same-docshell bfcache navigation,
+ // we don't want to spawn new targets.
+ this.instantiate({
+ isBFCache: true,
+ });
+ return;
+ }
+
+ if (type === "pagehide" && persisted) {
+ // Notify all bfcache navigations, even the one for which we don't create a new target
+ // as that's being useful for parent process storage resource watchers.
+ this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pagehide");
+
+ // We might navigate away for the first top level target,
+ // which isn't using JSWindowActor (it still uses messages manager and is created by the client, via TabDescriptor.getTarget).
+ // We have to unregister it from the TargetActorRegistry, otherwise,
+ // if we navigate back to it, the next DOMWindowCreated won't create a new target for it.
+ const { sharedData } = Services.cpmm;
+ const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
+ if (!sessionDataByWatcherActor) {
+ throw new Error(
+ "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets"
+ );
+ }
+
+ const actors = [];
+ // A flag to know if the following for loop ended up destroying all the actors.
+ // It may not be the case if one Watcher isn't having server target switching enabled.
+ let allActorsAreDestroyed = true;
+ for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
+ const { sessionContext } = sessionData;
+
+ // /!\ We may have an issue here as there could be multiple targets for a given
+ // (watcherActorID,browserId) pair.
+ // This should be clarified as part of Bug 1725623.
+ const existingTarget = this._findTargetActor({
+ watcherActorID,
+ sessionContext,
+ });
+
+ if (!existingTarget) {
+ continue;
+ }
+
+ // Use `originalWindow` as `window` can be set when a document was selected from
+ // the iframe picker in the toolbox toolbar.
+ if (existingTarget.originalWindow.document != target) {
+ throw new Error("Existing target actor is for a distinct document");
+ }
+ // Do not do anything if both bfcache in parent and server targets are disabled
+ // As history navigations will be handled within the same DocShell and by the
+ // same WindowGlobalTargetActor. The actor will listen to pageshow/pagehide by itself.
+ // We should not destroy any target.
+ if (
+ !this.isBfcacheInParentEnabled &&
+ !sessionContext.isServerTargetSwitchingEnabled
+ ) {
+ allActorsAreDestroyed = false;
+ continue;
+ }
+
+ actors.push({
+ watcherActorID,
+ form: existingTarget.form(),
+ });
+ existingTarget.destroy();
+ }
+
+ if (actors.length) {
+ // The most important is to unregister the actor from TargetActorRegistry,
+ // so that it is no longer present in the list when new DOMWindowCreated fires.
+ // This will also help notify the client that the target has been destroyed.
+ // And if we navigate back to this target, the client will receive the same target actor ID,
+ // so that it is really important to destroy it correctly on both server and client.
+ this.sendAsyncMessage("DevToolsFrameChild:destroy", { actors });
+ }
+
+ if (allActorsAreDestroyed) {
+ // Completely clear this JSWindow Actor.
+ // Do this after having called _findTargetActor,
+ // as it would clear the registered target actors.
+ this.didDestroy();
+ }
+ }
+ }
+
+ didDestroy(options) {
+ logWindowGlobal(this.manager, "Destroy WindowGlobalTarget");
+ for (const [, connectionInfo] of this._connections) {
+ connectionInfo.connection.close(options);
+ }
+ this._connections.clear();
+
+ if (this.loader) {
+ if (this.useCustomLoader) {
+ lazy.releaseDistinctSystemPrincipalLoader(this);
+ }
+ this.loader = null;
+ }
+ }
+}
diff --git a/devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs
new file mode 100644
index 0000000000..867453a798
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs
@@ -0,0 +1,272 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs";
+import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
+
+const { WatcherRegistry } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs",
+ {
+ // WatcherRegistry needs to be a true singleton and loads ActorManagerParent
+ // which also has to be a true singleton.
+ loadInDevToolsLoader: false,
+ }
+);
+
+const lazy = {};
+
+loader.lazyRequireGetter(
+ lazy,
+ "JsWindowActorTransport",
+ "resource://devtools/shared/transport/js-window-actor-transport.js",
+ true
+);
+
+export class DevToolsFrameParent extends JSWindowActorParent {
+ constructor() {
+ super();
+
+ // Map of DevToolsServerConnection's used to forward the messages from/to
+ // the client. The connections run in the parent process, as this code. We
+ // may have more than one when there is more than one client debugging the
+ // same frame. For example, a content toolbox and the browser toolbox.
+ //
+ // The map is indexed by the connection prefix.
+ // The values are objects containing the following properties:
+ // - actor: the frame target actor(as a form)
+ // - connection: the DevToolsServerConnection used to communicate with the
+ // frame target actor
+ // - prefix: the forwarding prefix used by the connection to know
+ // how to forward packets to the frame target
+ // - transport: the JsWindowActorTransport
+ //
+ // Reminder about prefixes: all DevToolsServerConnections have a `prefix`
+ // which can be considered as a kind of id. On top of this, parent process
+ // DevToolsServerConnections also have forwarding prefixes because they are
+ // responsible for forwarding messages to content process connections.
+ this._connections = new Map();
+
+ this._onConnectionClosed = this._onConnectionClosed.bind(this);
+ EventEmitter.decorate(this);
+ }
+
+ /**
+ * Request the content process to create the Frame Target if there is one
+ * already available that matches the Browsing Context ID
+ */
+ async instantiateTarget({
+ watcherActorID,
+ connectionPrefix,
+ sessionContext,
+ sessionData,
+ }) {
+ await this.sendQuery("DevToolsFrameParent:instantiate-already-available", {
+ watcherActorID,
+ connectionPrefix,
+ sessionContext,
+ sessionData,
+ });
+ }
+
+ /**
+ * @param {object} arg
+ * @param {object} arg.sessionContext
+ * @param {object} arg.options
+ * @param {boolean} arg.options.isModeSwitching
+ * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
+ */
+ destroyTarget({ watcherActorID, sessionContext, options }) {
+ this.sendAsyncMessage("DevToolsFrameParent:destroy", {
+ watcherActorID,
+ sessionContext,
+ options,
+ });
+ }
+
+ /**
+ * Communicate to the content process that some data have been added.
+ */
+ async addSessionDataEntry({ watcherActorID, sessionContext, type, entries }) {
+ try {
+ await this.sendQuery("DevToolsFrameParent:addSessionDataEntry", {
+ watcherActorID,
+ sessionContext,
+ type,
+ entries,
+ });
+ } catch (e) {
+ console.warn(
+ "Failed to add session data entry for frame targets in browsing context",
+ this.browsingContext.id
+ );
+ console.warn(e);
+ }
+ }
+
+ /**
+ * Communicate to the content process that some data have been removed.
+ */
+ removeSessionDataEntry({ watcherActorID, sessionContext, type, entries }) {
+ this.sendAsyncMessage("DevToolsFrameParent:removeSessionDataEntry", {
+ watcherActorID,
+ sessionContext,
+ type,
+ entries,
+ });
+ }
+
+ connectFromContent({ watcherActorID, forwardingPrefix, actor }) {
+ const watcher = WatcherRegistry.getWatcher(watcherActorID);
+
+ if (!watcher) {
+ throw new Error(
+ `Watcher Actor with ID '${watcherActorID}' can't be found.`
+ );
+ }
+ const connection = watcher.conn;
+
+ connection.on("closed", this._onConnectionClosed);
+
+ // Create a js-window-actor based transport.
+ const transport = new lazy.JsWindowActorTransport(this, forwardingPrefix);
+ transport.hooks = {
+ onPacket: connection.send.bind(connection),
+ onTransportClosed() {},
+ };
+ transport.ready();
+
+ connection.setForwarding(forwardingPrefix, transport);
+
+ this._connections.set(watcher.conn.prefix, {
+ watcher,
+ connection,
+ // This prefix is the prefix of the DevToolsServerConnection, running
+ // in the content process, for which we should forward packets to, based on its prefix.
+ // While `watcher.connection` is also a DevToolsServerConnection, but from this process,
+ // the parent process. It is the one receiving Client packets and the one, from which
+ // we should forward packets from.
+ forwardingPrefix,
+ transport,
+ actor,
+ });
+
+ watcher.notifyTargetAvailable(actor);
+ }
+
+ _onConnectionClosed(status, connectionPrefix) {
+ this._unregisterWatcher(connectionPrefix);
+ }
+
+ /**
+ * Given a watcher connection prefix, unregister everything related to the Watcher
+ * in this JSWindowActor.
+ *
+ * @param {String} connectionPrefix
+ * The connection prefix of the watcher to unregister
+ */
+ async _unregisterWatcher(connectionPrefix) {
+ const connectionInfo = this._connections.get(connectionPrefix);
+ if (!connectionInfo) {
+ return;
+ }
+ const { forwardingPrefix, transport, connection } = connectionInfo;
+ this._connections.delete(connectionPrefix);
+
+ connection.off("closed", this._onConnectionClosed);
+ if (transport) {
+ // If we have a child transport, the actor has already
+ // been created. We need to stop using this transport.
+ transport.close();
+ }
+
+ connection.cancelForwarding(forwardingPrefix);
+ }
+
+ /**
+ * Destroy everything that we did related to the current WindowGlobal that
+ * this JSWindow Actor represents:
+ * - close all transports that were used as bridge to communicate with the
+ * DevToolsFrameChild, running in the content process
+ * - unregister these transports from DevToolsServer (cancelForwarding)
+ * - notify the client, via the WatcherActor that all related targets,
+ * one per client/connection are all destroyed
+ *
+ * Note that with bfcacheInParent, we may reuse a JSWindowActor pair after closing all connections.
+ * This is can happen outside of the destruction of the actor.
+ * We may reuse a DevToolsFrameParent and DevToolsFrameChild pair.
+ * When navigating away, we will destroy them and call this method.
+ * Then when navigating back, we will reuse the same instances.
+ * So that we should be careful to keep the class fully function and only clear all its state.
+ *
+ * @param {object} options
+ * @param {boolean} options.isModeSwitching
+ * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
+ */
+ _closeAllConnections(options) {
+ for (const { actor, watcher } of this._connections.values()) {
+ watcher.notifyTargetDestroyed(actor, options);
+ this._unregisterWatcher(watcher.conn.prefix);
+ }
+ this._connections.clear();
+ }
+
+ /**
+ * Supported Queries
+ */
+
+ sendPacket(packet, prefix) {
+ this.sendAsyncMessage("DevToolsFrameParent:packet", { packet, prefix });
+ }
+
+ /**
+ * JsWindowActor API
+ */
+
+ receiveMessage(message) {
+ switch (message.name) {
+ case "DevToolsFrameChild:connectFromContent":
+ return this.connectFromContent(message.data);
+ case "DevToolsFrameChild:packet":
+ return this.emit("packet-received", message);
+ case "DevToolsFrameChild:destroy":
+ for (const { form, watcherActorID } of message.data.actors) {
+ const watcher = WatcherRegistry.getWatcher(watcherActorID);
+ // As we instruct to destroy all targets when the watcher is destroyed,
+ // we may easily receive the target destruction notification *after*
+ // the watcher has been removed from the registry.
+ if (watcher) {
+ watcher.notifyTargetDestroyed(form, message.data.options);
+ this._unregisterWatcher(watcher.conn.prefix);
+ }
+ }
+ return null;
+ case "DevToolsFrameChild:bf-cache-navigation-pageshow":
+ for (const watcherActor of WatcherRegistry.getWatchersForBrowserId(
+ this.browsingContext.browserId
+ )) {
+ watcherActor.emit("bf-cache-navigation-pageshow", {
+ windowGlobal: this.browsingContext.currentWindowGlobal,
+ });
+ }
+ return null;
+ case "DevToolsFrameChild:bf-cache-navigation-pagehide":
+ for (const watcherActor of WatcherRegistry.getWatchersForBrowserId(
+ this.browsingContext.browserId
+ )) {
+ watcherActor.emit("bf-cache-navigation-pagehide", {
+ windowGlobal: this.browsingContext.currentWindowGlobal,
+ });
+ }
+ return null;
+ default:
+ throw new Error(
+ "Unsupported message in DevToolsFrameParent: " + message.name
+ );
+ }
+ }
+
+ didDestroy() {
+ this._closeAllConnections();
+ }
+}
diff --git a/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs
new file mode 100644
index 0000000000..48b6d4da0b
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs
@@ -0,0 +1,551 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "wdm",
+ "@mozilla.org/dom/workers/workerdebuggermanager;1",
+ "nsIWorkerDebuggerManager"
+);
+
+XPCOMUtils.defineLazyGetter(lazy, "Loader", () =>
+ ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs")
+);
+
+XPCOMUtils.defineLazyGetter(lazy, "DevToolsUtils", () =>
+ lazy.Loader.require("resource://devtools/shared/DevToolsUtils.js")
+);
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ SessionDataHelpers:
+ "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm",
+});
+ChromeUtils.defineESModuleGetters(lazy, {
+ isWindowGlobalPartOfContext:
+ "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
+});
+
+// Name of the attribute into which we save data in `sharedData` object.
+const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
+
+export class DevToolsWorkerChild extends JSWindowActorChild {
+ constructor() {
+ super();
+
+ // The map is indexed by the Watcher Actor ID.
+ // The values are objects containing the following properties:
+ // - connection: the DevToolsServerConnection itself
+ // - workers: An array of object containing the following properties:
+ // - dbg: A WorkerDebuggerInstance
+ // - workerTargetForm: The associated worker target instance form
+ // - workerThreadServerForwardingPrefix: The prefix used to forward events to the
+ // worker target on the worker thread ().
+ // - forwardingPrefix: Prefix used by the JSWindowActorTransport pair to communicate
+ // between content and parent processes.
+ // - sessionData: Data (targets, resources, …) the watcher wants to be notified about.
+ // See WatcherRegistry.getSessionData to see the full list of properties.
+ this._connections = new Map();
+
+ EventEmitter.decorate(this);
+ }
+
+ _onWorkerRegistered(dbg) {
+ if (!this._shouldHandleWorker(dbg)) {
+ return;
+ }
+
+ for (const [watcherActorID, { connection, forwardingPrefix }] of this
+ ._connections) {
+ this._createWorkerTargetActor({
+ dbg,
+ connection,
+ forwardingPrefix,
+ watcherActorID,
+ });
+ }
+ }
+
+ _onWorkerUnregistered(dbg) {
+ for (const [watcherActorID, { workers, forwardingPrefix }] of this
+ ._connections) {
+ // Check if the worker registration was handled for this watcherActorID.
+ const unregisteredActorIndex = workers.findIndex(worker => {
+ try {
+ // Accessing the WorkerDebugger id might throw (NS_ERROR_UNEXPECTED).
+ return worker.dbg.id === dbg.id;
+ } catch (e) {
+ return false;
+ }
+ });
+ if (unregisteredActorIndex === -1) {
+ continue;
+ }
+
+ const { workerTargetForm, transport } = workers[unregisteredActorIndex];
+ transport.close();
+
+ try {
+ this.sendAsyncMessage("DevToolsWorkerChild:workerTargetDestroyed", {
+ watcherActorID,
+ forwardingPrefix,
+ workerTargetForm,
+ });
+ } catch (e) {
+ return;
+ }
+
+ workers.splice(unregisteredActorIndex, 1);
+ }
+ }
+
+ onDOMWindowCreated() {
+ const { sharedData } = Services.cpmm;
+ const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
+ if (!sessionDataByWatcherActor) {
+ throw new Error(
+ "Request to instantiate the target(s) for the Worker, but `sharedData` is empty about watched targets"
+ );
+ }
+
+ // Create one Target actor for each prefix/client which listen to workers
+ for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
+ const { targets, connectionPrefix, sessionContext } = sessionData;
+ if (
+ targets?.includes("worker") &&
+ lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, {
+ acceptInitialDocument: true,
+ forceAcceptTopLevelTarget: true,
+ acceptSameProcessIframes: true,
+ })
+ ) {
+ this._watchWorkerTargets({
+ watcherActorID,
+ parentConnectionPrefix: connectionPrefix,
+ sessionData,
+ });
+ }
+ }
+ }
+
+ /**
+ * Function handling messages sent by DevToolsWorkerParent (part of JSWindowActor API).
+ *
+ * @param {Object} message
+ * @param {String} message.name
+ * @param {*} message.data
+ */
+ receiveMessage(message) {
+ // All messages pass `sessionContext` (except packet) and are expected
+ // to match isWindowGlobalPartOfContext result.
+ if (message.name != "DevToolsWorkerParent:packet") {
+ const { browserId } = message.data.sessionContext;
+ // Re-check here, just to ensure that both parent and content processes agree
+ // on what should or should not be watched.
+ if (
+ this.manager.browsingContext.browserId != browserId &&
+ !lazy.isWindowGlobalPartOfContext(
+ this.manager,
+ message.data.sessionContext,
+ {
+ acceptInitialDocument: true,
+ }
+ )
+ ) {
+ throw new Error(
+ "Mismatch between DevToolsWorkerParent and DevToolsWorkerChild " +
+ (this.manager.browsingContext.browserId == browserId
+ ? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)"
+ : `expected browsing context with ID ${browserId}, but got ${this.manager.browsingContext.browserId}`)
+ );
+ }
+ }
+
+ switch (message.name) {
+ case "DevToolsWorkerParent:instantiate-already-available": {
+ const { watcherActorID, connectionPrefix, sessionData } = message.data;
+
+ return this._watchWorkerTargets({
+ watcherActorID,
+ parentConnectionPrefix: connectionPrefix,
+ sessionData,
+ });
+ }
+ case "DevToolsWorkerParent:destroy": {
+ const { watcherActorID } = message.data;
+ return this._destroyTargetActors(watcherActorID);
+ }
+ case "DevToolsWorkerParent:addSessionDataEntry": {
+ const { watcherActorID, type, entries } = message.data;
+ return this._addSessionDataEntry(watcherActorID, type, entries);
+ }
+ case "DevToolsWorkerParent:removeSessionDataEntry": {
+ const { watcherActorID, type, entries } = message.data;
+ return this._removeSessionDataEntry(watcherActorID, type, entries);
+ }
+ case "DevToolsWorkerParent:packet":
+ return this.emit("packet-received", message);
+ default:
+ throw new Error(
+ "Unsupported message in DevToolsWorkerParent: " + message.name
+ );
+ }
+ }
+
+ /**
+ * Instantiate targets for existing workers, watch for worker registration and listen
+ * for resources on those workers, for given connection and context. Targets are sent
+ * to the DevToolsWorkerParent via the DevToolsWorkerChild:workerTargetAvailable message.
+ *
+ * @param {Object} options
+ * @param {Integer} options.watcherActorID: The ID of the WatcherActor who requested to
+ * observe and create these target actors.
+ * @param {String} options.parentConnectionPrefix: The prefix of the DevToolsServerConnection
+ * of the Watcher Actor. This is used to compute a unique ID for the target actor.
+ * @param {Object} options.sessionData: Data (targets, resources, …) the watcher wants
+ * to be notified about. See WatcherRegistry.getSessionData to see the full list
+ * of properties.
+ */
+ async _watchWorkerTargets({
+ watcherActorID,
+ parentConnectionPrefix,
+ sessionData,
+ }) {
+ if (this._connections.has(watcherActorID)) {
+ throw new Error(
+ "DevToolsWorkerChild _watchWorkerTargets was called more than once" +
+ ` for the same Watcher (Actor ID: "${watcherActorID}")`
+ );
+ }
+
+ // Listen for new workers that will be spawned.
+ if (!this._workerDebuggerListener) {
+ this._workerDebuggerListener = {
+ onRegister: this._onWorkerRegistered.bind(this),
+ onUnregister: this._onWorkerUnregistered.bind(this),
+ };
+ lazy.wdm.addListener(this._workerDebuggerListener);
+ }
+
+ // Compute a unique prefix, just for this WindowGlobal,
+ // which will be used to create a JSWindowActorTransport pair between content and parent processes.
+ // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`,
+ // but here, we can't have access to any DevTools connection as we are really early in the content process startup
+ // WindowGlobalChild's innerWindowId should be unique across processes, so it should be safe?
+ // (this.manager == WindowGlobalChild interface)
+ const forwardingPrefix =
+ parentConnectionPrefix + "workerGlobal" + this.manager.innerWindowId;
+
+ const connection = this._createConnection(forwardingPrefix);
+
+ this._connections.set(watcherActorID, {
+ connection,
+ workers: [],
+ forwardingPrefix,
+ sessionData,
+ });
+
+ await Promise.all(
+ Array.from(lazy.wdm.getWorkerDebuggerEnumerator())
+ .filter(dbg => this._shouldHandleWorker(dbg))
+ .map(dbg =>
+ this._createWorkerTargetActor({
+ dbg,
+ connection,
+ forwardingPrefix,
+ watcherActorID,
+ })
+ )
+ );
+ }
+
+ _createConnection(forwardingPrefix) {
+ const { DevToolsServer } = lazy.Loader.require(
+ "resource://devtools/server/devtools-server.js"
+ );
+
+ DevToolsServer.init();
+
+ // We want a special server without any root actor and only target-scoped actors.
+ // We are going to spawn a WorkerTargetActor instance in the next few lines,
+ // it is going to act like a root actor without being one.
+ DevToolsServer.registerActors({ target: true });
+
+ const connection = DevToolsServer.connectToParentWindowActor(
+ this,
+ forwardingPrefix
+ );
+
+ return connection;
+ }
+
+ /**
+ * Indicates whether or not we should handle the worker debugger
+ *
+ * @param {WorkerDebugger} dbg: The worker debugger we want to check.
+ * @returns {Boolean}
+ */
+ _shouldHandleWorker(dbg) {
+ // We only want to create targets for non-closed dedicated worker, in the same document
+ return (
+ lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg) &&
+ dbg.type === Ci.nsIWorkerDebugger.TYPE_DEDICATED &&
+ dbg.windowIDs.includes(this.manager.innerWindowId)
+ );
+ }
+
+ async _createWorkerTargetActor({
+ dbg,
+ connection,
+ forwardingPrefix,
+ watcherActorID,
+ }) {
+ // Prevent the debuggee from executing in this worker until the client has
+ // finished attaching to it. This call will throw if the debugger is already "registered"
+ // (i.e. if this is called outside of the register listener)
+ // See https://searchfox.org/mozilla-central/rev/84922363f4014eae684aabc4f1d06380066494c5/dom/workers/nsIWorkerDebugger.idl#55-66
+ try {
+ dbg.setDebuggerReady(false);
+ } catch (e) {}
+
+ const watcherConnectionData = this._connections.get(watcherActorID);
+ const { sessionData } = watcherConnectionData;
+ const workerThreadServerForwardingPrefix = connection.allocID(
+ "workerTarget"
+ );
+
+ // Create the actual worker target actor, in the worker thread.
+ const { connectToWorker } = lazy.Loader.require(
+ "resource://devtools/server/connectors/worker-connector.js"
+ );
+
+ const onConnectToWorker = connectToWorker(
+ connection,
+ dbg,
+ workerThreadServerForwardingPrefix,
+ {
+ sessionData,
+ sessionContext: sessionData.sessionContext,
+ }
+ );
+
+ try {
+ await onConnectToWorker;
+ } catch (e) {
+ // onConnectToWorker can reject if the Worker Debugger is closed; so we only want to
+ // resume the debugger if it is not closed (otherwise it can cause crashes).
+ if (!dbg.isClosed) {
+ dbg.setDebuggerReady(true);
+ }
+ return;
+ }
+
+ const { workerTargetForm, transport } = await onConnectToWorker;
+
+ try {
+ this.sendAsyncMessage("DevToolsWorkerChild:workerTargetAvailable", {
+ watcherActorID,
+ forwardingPrefix,
+ workerTargetForm,
+ });
+ } catch (e) {
+ // If there was an error while sending the message, we are not going to use this
+ // connection to communicate with the worker.
+ transport.close();
+ return;
+ }
+
+ // Only add data to the connection if we successfully send the
+ // workerTargetAvailable message.
+ watcherConnectionData.workers.push({
+ dbg,
+ transport,
+ workerTargetForm,
+ workerThreadServerForwardingPrefix,
+ });
+ }
+
+ _destroyTargetActors(watcherActorID) {
+ const watcherConnectionData = this._connections.get(watcherActorID);
+ this._connections.delete(watcherActorID);
+
+ // This connection has already been cleaned?
+ if (!watcherConnectionData) {
+ console.error(
+ `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
+ );
+ return;
+ }
+
+ for (const {
+ dbg,
+ transport,
+ workerThreadServerForwardingPrefix,
+ } of watcherConnectionData.workers) {
+ try {
+ if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
+ dbg.postMessage(
+ JSON.stringify({
+ type: "disconnect",
+ forwardingPrefix: workerThreadServerForwardingPrefix,
+ })
+ );
+ }
+ } catch (e) {}
+
+ transport.close();
+ }
+
+ watcherConnectionData.connection.close();
+ }
+
+ async sendPacket(packet, prefix) {
+ return this.sendAsyncMessage("DevToolsWorkerChild:packet", {
+ packet,
+ prefix,
+ });
+ }
+
+ async _addSessionDataEntry(watcherActorID, type, entries) {
+ const watcherConnectionData = this._connections.get(watcherActorID);
+ if (!watcherConnectionData) {
+ return;
+ }
+
+ lazy.SessionDataHelpers.addSessionDataEntry(
+ watcherConnectionData.sessionData,
+ type,
+ entries
+ );
+
+ const promises = [];
+ for (const {
+ dbg,
+ workerThreadServerForwardingPrefix,
+ } of watcherConnectionData.workers) {
+ promises.push(
+ addSessionDataEntryInWorkerTarget({
+ dbg,
+ workerThreadServerForwardingPrefix,
+ type,
+ entries,
+ })
+ );
+ }
+ await Promise.all(promises);
+ }
+
+ _removeSessionDataEntry(watcherActorID, type, entries) {
+ const watcherConnectionData = this._connections.get(watcherActorID);
+
+ if (!watcherConnectionData) {
+ return;
+ }
+
+ lazy.SessionDataHelpers.removeSessionDataEntry(
+ watcherConnectionData.sessionData,
+ type,
+ entries
+ );
+
+ for (const {
+ dbg,
+ workerThreadServerForwardingPrefix,
+ } of watcherConnectionData.workers) {
+ if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
+ dbg.postMessage(
+ JSON.stringify({
+ type: "remove-session-data-entry",
+ forwardingPrefix: workerThreadServerForwardingPrefix,
+ dataEntryType: type,
+ entries,
+ })
+ );
+ }
+ }
+ }
+
+ handleEvent({ type }) {
+ // DOMWindowCreated is registered from the WatcherRegistry via `ActorManagerParent.addJSWindowActors`
+ // as a DOM event to be listened to and so is fired by JSWindowActor platform code.
+ if (type == "DOMWindowCreated") {
+ this.onDOMWindowCreated();
+ }
+ }
+
+ _removeExistingWorkerDebuggerListener() {
+ if (this._workerDebuggerListener) {
+ lazy.wdm.removeListener(this._workerDebuggerListener);
+ this._workerDebuggerListener = null;
+ }
+ }
+
+ /**
+ * Part of JSActor API
+ * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
+ *
+ * > The didDestroy method, if present, will be called after the actor is no
+ * > longer able to receive any more messages.
+ */
+ didDestroy() {
+ this._removeExistingWorkerDebuggerListener();
+
+ for (const [watcherActorID, watcherConnectionData] of this._connections) {
+ const { connection } = watcherConnectionData;
+ this._destroyTargetActors(watcherActorID);
+
+ connection.close();
+ }
+
+ this._connections.clear();
+ }
+}
+
+/**
+ * Communicate the type and entries to the Worker Target actor, via the WorkerDebugger.
+ *
+ * @returns {Promise} Returns a Promise that resolves once the data entry were handled
+ * by the worker target.
+ */
+function addSessionDataEntryInWorkerTarget({
+ dbg,
+ workerThreadServerForwardingPrefix,
+ type,
+ entries,
+}) {
+ if (!lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ // Wait until we're notified by the worker that the resources are watched.
+ // This is important so we know existing resources were handled.
+ const listener = {
+ onMessage: message => {
+ message = JSON.parse(message);
+ if (message.type === "session-data-entry-added") {
+ resolve();
+ dbg.removeListener(listener);
+ }
+ },
+ // Resolve if the worker is being destroyed so we don't have a dangling promise.
+ onClose: () => resolve(),
+ };
+
+ dbg.addListener(listener);
+
+ dbg.postMessage(
+ JSON.stringify({
+ type: "add-session-data-entry",
+ forwardingPrefix: workerThreadServerForwardingPrefix,
+ dataEntryType: type,
+ entries,
+ })
+ );
+ });
+}
diff --git a/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs
new file mode 100644
index 0000000000..adf5ce175f
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs
@@ -0,0 +1,293 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs";
+import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
+
+const { WatcherRegistry } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs",
+ {
+ // WatcherRegistry needs to be a true singleton and loads ActorManagerParent
+ // which also has to be a true singleton.
+ loadInDevToolsLoader: false,
+ }
+);
+
+const lazy = {};
+
+loader.lazyRequireGetter(
+ lazy,
+ "JsWindowActorTransport",
+ "resource://devtools/shared/transport/js-window-actor-transport.js",
+ true
+);
+
+export class DevToolsWorkerParent extends JSWindowActorParent {
+ constructor() {
+ super();
+
+ this._destroyed = false;
+
+ // Map of DevToolsServerConnection's used to forward the messages from/to
+ // the client. The connections run in the parent process, as this code. We
+ // may have more than one when there is more than one client debugging the
+ // same worker. For example, a content toolbox and the browser toolbox.
+ //
+ // The map is indexed by the connection prefix, and the values are object with the
+ // following properties:
+ // - watcher: The WatcherActor
+ // - actors: A Map of the worker target actors form, indexed by WorkerTarget actorID
+ // - transport: the JsWindowActorTransport
+ //
+ // Reminder about prefixes: all DevToolsServerConnections have a `prefix`
+ // which can be considered as a kind of id. On top of this, parent process
+ // DevToolsServerConnections also have forwarding prefixes because they are
+ // responsible for forwarding messages to content process connections.
+ this._connections = new Map();
+
+ this._onConnectionClosed = this._onConnectionClosed.bind(this);
+ EventEmitter.decorate(this);
+ }
+
+ /**
+ * Request the content process to create Worker Targets if workers matching the context
+ * are already available.
+ */
+ async instantiateWorkerTargets({
+ watcherActorID,
+ connectionPrefix,
+ sessionContext,
+ sessionData,
+ }) {
+ try {
+ await this.sendQuery(
+ "DevToolsWorkerParent:instantiate-already-available",
+ {
+ watcherActorID,
+ connectionPrefix,
+ sessionContext,
+ sessionData,
+ }
+ );
+ } catch (e) {
+ console.warn(
+ "Failed to create DevTools Worker target for browsingContext",
+ this.browsingContext.id,
+ "and watcher actor id",
+ watcherActorID
+ );
+ console.warn(e);
+ }
+ }
+
+ destroyWorkerTargets({ watcherActorID, sessionContext }) {
+ return this.sendAsyncMessage("DevToolsWorkerParent:destroy", {
+ watcherActorID,
+ sessionContext,
+ });
+ }
+
+ /**
+ * Communicate to the content process that some data have been added.
+ */
+ async addSessionDataEntry({ watcherActorID, sessionContext, type, entries }) {
+ try {
+ await this.sendQuery("DevToolsWorkerParent:addSessionDataEntry", {
+ watcherActorID,
+ sessionContext,
+ type,
+ entries,
+ });
+ } catch (e) {
+ console.warn(
+ "Failed to add session data entry for worker targets in browsing context",
+ this.browsingContext.id,
+ "and watcher actor id",
+ watcherActorID
+ );
+ console.warn(e);
+ }
+ }
+
+ /**
+ * Communicate to the content process that some data have been removed.
+ */
+ removeSessionDataEntry({ watcherActorID, sessionContext, type, entries }) {
+ this.sendAsyncMessage("DevToolsWorkerParent:removeSessionDataEntry", {
+ watcherActorID,
+ sessionContext,
+ type,
+ entries,
+ });
+ }
+
+ workerTargetAvailable({
+ watcherActorID,
+ forwardingPrefix,
+ workerTargetForm,
+ }) {
+ if (this._destroyed) {
+ return;
+ }
+
+ const watcher = WatcherRegistry.getWatcher(watcherActorID);
+
+ if (!watcher) {
+ throw new Error(
+ `Watcher Actor with ID '${watcherActorID}' can't be found.`
+ );
+ }
+
+ const connection = watcher.conn;
+ const { prefix } = connection;
+ if (!this._connections.has(prefix)) {
+ connection.on("closed", this._onConnectionClosed);
+
+ // Create a js-window-actor based transport.
+ const transport = new lazy.JsWindowActorTransport(this, forwardingPrefix);
+ transport.hooks = {
+ onPacket: connection.send.bind(connection),
+ onTransportClosed() {},
+ };
+ transport.ready();
+
+ connection.setForwarding(forwardingPrefix, transport);
+
+ this._connections.set(prefix, {
+ watcher,
+ transport,
+ actors: new Map(),
+ });
+ }
+
+ const workerTargetActorId = workerTargetForm.actor;
+ this._connections
+ .get(prefix)
+ .actors.set(workerTargetActorId, workerTargetForm);
+ watcher.notifyTargetAvailable(workerTargetForm);
+ }
+
+ workerTargetDestroyed({
+ watcherActorID,
+ forwardingPrefix,
+ workerTargetForm,
+ }) {
+ const watcher = WatcherRegistry.getWatcher(watcherActorID);
+
+ if (!watcher) {
+ throw new Error(
+ `Watcher Actor with ID '${watcherActorID}' can't be found.`
+ );
+ }
+
+ const connection = watcher.conn;
+ const { prefix } = connection;
+ if (!this._connections.has(prefix)) {
+ return;
+ }
+
+ const workerTargetActorId = workerTargetForm.actor;
+ const { actors } = this._connections.get(prefix);
+ if (!actors.has(workerTargetActorId)) {
+ return;
+ }
+
+ actors.delete(workerTargetActorId);
+ watcher.notifyTargetDestroyed(workerTargetForm);
+ }
+
+ _onConnectionClosed(status, prefix) {
+ this._unregisterWatcher(prefix);
+ }
+
+ async _unregisterWatcher(connectionPrefix) {
+ const connectionInfo = this._connections.get(connectionPrefix);
+ if (!connectionInfo) {
+ return;
+ }
+
+ const { watcher, transport } = connectionInfo;
+ const connection = watcher.conn;
+
+ connection.off("closed", this._onConnectionClosed);
+ if (transport) {
+ // If we have a child transport, the actor has already
+ // been created. We need to stop using this transport.
+ connection.cancelForwarding(transport._prefix);
+ transport.close();
+ }
+
+ this._connections.delete(connectionPrefix);
+
+ if (!this._connections.size) {
+ this._destroy();
+ }
+ }
+
+ _destroy() {
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ for (const { actors, watcher } of this._connections.values()) {
+ for (const actor of actors.values()) {
+ watcher.notifyTargetDestroyed(actor);
+ }
+
+ this._unregisterWatcher(watcher.conn.prefix);
+ }
+ }
+
+ /**
+ * Part of JSActor API
+ * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
+ *
+ * > The didDestroy method, if present, will be called after the (JSWindow)actor is no
+ * > longer able to receive any more messages.
+ */
+ didDestroy() {
+ this._destroy();
+ }
+
+ /**
+ * Supported Queries
+ */
+
+ async sendPacket(packet, prefix) {
+ return this.sendAsyncMessage("DevToolsWorkerParent:packet", {
+ packet,
+ prefix,
+ });
+ }
+
+ /**
+ * JsWindowActor API
+ */
+
+ async sendQuery(msg, args) {
+ try {
+ const res = await super.sendQuery(msg, args);
+ return res;
+ } catch (e) {
+ console.error("Failed to sendQuery in DevToolsWorkerParent", msg, e);
+ throw e;
+ }
+ }
+
+ receiveMessage(message) {
+ switch (message.name) {
+ case "DevToolsWorkerChild:workerTargetAvailable":
+ return this.workerTargetAvailable(message.data);
+ case "DevToolsWorkerChild:workerTargetDestroyed":
+ return this.workerTargetDestroyed(message.data);
+ case "DevToolsWorkerChild:packet":
+ return this.emit("packet-received", message);
+ default:
+ throw new Error(
+ "Unsupported message in DevToolsWorkerParent: " + message.name
+ );
+ }
+ }
+}
diff --git a/devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs b/devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs
new file mode 100644
index 0000000000..ae15c030fe
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function getWindowGlobalUri(windowGlobal) {
+ let windowGlobalUri = "";
+
+ if (windowGlobal.documentURI) {
+ // If windowGlobal is a WindowGlobalParent documentURI should be available.
+ windowGlobalUri = windowGlobal.documentURI.spec;
+ } else if (windowGlobal.browsingContext?.window) {
+ // If windowGlobal is a WindowGlobalChild, this code runs in the same
+ // process as the document and we can directly access the window.location
+ // object.
+ windowGlobalUri = windowGlobal.browsingContext.window.location.href;
+ if (!windowGlobalUri) {
+ windowGlobalUri =
+ windowGlobal.browsingContext.window.document.documentURI;
+ }
+ }
+
+ return windowGlobalUri;
+}
+
+export const WindowGlobalLogger = {
+ /**
+ * This logger can run from the content or parent process, and windowGlobal
+ * will either be of type `WindowGlobalParent` or `WindowGlobalChild`.
+ *
+ * The interface for each type can be found in WindowGlobalActors.webidl
+ * (https://searchfox.org/mozilla-central/source/dom/chrome-webidl/WindowGlobalActors.webidl)
+ *
+ * @param {WindowGlobalParent|WindowGlobalChild} windowGlobal
+ * The window global to log. See WindowGlobalActors.webidl for details
+ * about the types.
+ * @param {String} message
+ * A custom message that will be displayed at the beginning of the log.
+ */
+ logWindowGlobal(windowGlobal, message) {
+ const { browsingContext } = windowGlobal;
+ const { parent } = browsingContext;
+ const windowGlobalUri = getWindowGlobalUri(windowGlobal);
+ const isInitialDocument =
+ "isInitialDocument" in windowGlobal
+ ? windowGlobal.isInitialDocument
+ : windowGlobal.browsingContext.window?.document.isInitialDocument;
+
+ const details = [];
+ details.push(
+ "BrowsingContext.browserId: " + browsingContext.browserId,
+ "BrowsingContext.id: " + browsingContext.id,
+ "innerWindowId: " + windowGlobal.innerWindowId,
+ "opener.id: " + browsingContext.opener?.id,
+ "pid: " + windowGlobal.osPid,
+ "isClosed: " + windowGlobal.isClosed,
+ "isInProcess: " + windowGlobal.isInProcess,
+ "isCurrentGlobal: " + windowGlobal.isCurrentGlobal,
+ "isProcessRoot: " + windowGlobal.isProcessRoot,
+ "currentRemoteType: " + browsingContext.currentRemoteType,
+ "hasParent: " + (parent ? parent.id : "no"),
+ "uri: " + (windowGlobalUri ? windowGlobalUri : "no uri"),
+ "isProcessRoot: " + windowGlobal.isProcessRoot,
+ "BrowsingContext.isContent: " + windowGlobal.browsingContext.isContent,
+ "isInitialDocument: " + isInitialDocument
+ );
+
+ const header = "[WindowGlobalLogger] " + message;
+
+ // Use a padding for multiline display.
+ const padding = " ";
+ const formattedDetails = details.map(s => padding + s);
+ const detailsString = formattedDetails.join("\n");
+
+ dump(header + "\n" + detailsString + "\n");
+ },
+};
diff --git a/devtools/server/connectors/js-window-actor/moz.build b/devtools/server/connectors/js-window-actor/moz.build
new file mode 100644
index 0000000000..faaaa8dd54
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "DevToolsFrameChild.sys.mjs",
+ "DevToolsFrameParent.sys.mjs",
+ "DevToolsWorkerChild.sys.mjs",
+ "DevToolsWorkerParent.sys.mjs",
+ "WindowGlobalLogger.sys.mjs",
+)
diff --git a/devtools/server/connectors/moz.build b/devtools/server/connectors/moz.build
new file mode 100644
index 0000000000..060f022131
--- /dev/null
+++ b/devtools/server/connectors/moz.build
@@ -0,0 +1,15 @@
+# -*- 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 += [
+ "js-window-actor",
+]
+
+DevToolsModules(
+ "content-process-connector.js",
+ "frame-connector.js",
+ "worker-connector.js",
+)
diff --git a/devtools/server/connectors/worker-connector.js b/devtools/server/connectors/worker-connector.js
new file mode 100644
index 0000000000..91398733ef
--- /dev/null
+++ b/devtools/server/connectors/worker-connector.js
@@ -0,0 +1,207 @@
+/* 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";
+
+var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+
+loader.lazyRequireGetter(
+ this,
+ "MainThreadWorkerDebuggerTransport",
+ "resource://devtools/shared/transport/worker-transport.js",
+ true
+);
+
+/**
+ * Start a DevTools server in a worker and add it as a child server for a given active connection.
+ *
+ * @params {DevToolsConnection} connection
+ * @params {WorkerDebugger} dbg: The WorkerDebugger we want to create a target actor for.
+ * @params {String} forwardingPrefix: The prefix that will be used to forward messages
+ * to the DevToolsServer on the worker thread.
+ * @params {Object} options: An option object that will be passed with the "connect" packet.
+ * @params {Object} options.sessionData: The sessionData object that will be passed to the
+ * worker target actor.
+ */
+function connectToWorker(connection, dbg, forwardingPrefix, options) {
+ return new Promise((resolve, reject) => {
+ if (!DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
+ reject("closed");
+ return;
+ }
+
+ // Step 1: Ensure the worker debugger is initialized.
+ if (!dbg.isInitialized) {
+ dbg.initialize("resource://devtools/server/startup/worker.js");
+
+ // Create a listener for rpc requests from the worker debugger. Only do
+ // this once, when the worker debugger is first initialized, rather than
+ // for each connection.
+ const listener = {
+ onClose: () => {
+ dbg.removeListener(listener);
+ },
+
+ onMessage: message => {
+ message = JSON.parse(message);
+ if (message.type !== "rpc") {
+ if (message.type == "worker-thread-attached") {
+ // The thread actor has finished attaching and can hit installed
+ // breakpoints. Allow content to begin executing in the worker.
+ dbg.setDebuggerReady(true);
+ }
+ return;
+ }
+
+ Promise.resolve()
+ .then(() => {
+ const method = {
+ fetch: DevToolsUtils.fetch,
+ }[message.method];
+ if (!method) {
+ throw Error("Unknown method: " + message.method);
+ }
+
+ return method.apply(undefined, message.params);
+ })
+ .then(
+ value => {
+ dbg.postMessage(
+ JSON.stringify({
+ type: "rpc",
+ result: value,
+ error: null,
+ id: message.id,
+ })
+ );
+ },
+ reason => {
+ dbg.postMessage(
+ JSON.stringify({
+ type: "rpc",
+ result: null,
+ error: reason,
+ id: message.id,
+ })
+ );
+ }
+ );
+ },
+ };
+
+ dbg.addListener(listener);
+ }
+
+ if (!DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
+ reject("closed");
+ return;
+ }
+
+ // WorkerDebugger.url isn't always an absolute URL.
+ // Use the related document URL in order to make it absolute.
+ const absoluteURL = dbg.window?.location?.href
+ ? new URL(dbg.url, dbg.window.location.href).href
+ : dbg.url;
+
+ // Step 2: Send a connect request to the worker debugger.
+ dbg.postMessage(
+ JSON.stringify({
+ type: "connect",
+ forwardingPrefix,
+ options,
+ workerDebuggerData: {
+ id: dbg.id,
+ type: dbg.type,
+ url: absoluteURL,
+ // We don't have access to Services.prefs in Worker thread, so pass its value
+ // from here.
+ workerConsoleApiMessagesDispatchedToMainThread: Services.prefs.getBoolPref(
+ "dom.worker.console.dispatch_events_to_main_thread"
+ ),
+ },
+ })
+ );
+
+ // Steps 3-5 are performed on the worker thread (see worker.js).
+
+ // Step 6: Wait for a connection response from the worker debugger.
+ const listener = {
+ onClose: () => {
+ dbg.removeListener(listener);
+
+ reject("closed");
+ },
+
+ onMessage: message => {
+ message = JSON.parse(message);
+ if (
+ message.type !== "connected" ||
+ message.forwardingPrefix !== forwardingPrefix
+ ) {
+ return;
+ }
+
+ // The initial connection message has been received, don't
+ // need to listen any longer
+ dbg.removeListener(listener);
+
+ // Step 7: Create a transport for the connection to the worker.
+ const transport = new MainThreadWorkerDebuggerTransport(
+ dbg,
+ forwardingPrefix
+ );
+ transport.ready();
+ transport.hooks = {
+ onTransportClosed: () => {
+ if (DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
+ // If the worker happens to be shutting down while we are trying
+ // to close the connection, there is a small interval during
+ // which no more runnables can be dispatched to the worker, but
+ // the worker debugger has not yet been closed. In that case,
+ // the call to postMessage below will fail. The onTransportClosed hook on
+ // DebuggerTransport is not supposed to throw exceptions, so we
+ // need to make sure to catch these early.
+ try {
+ dbg.postMessage(
+ JSON.stringify({
+ type: "disconnect",
+ forwardingPrefix,
+ })
+ );
+ } catch (e) {
+ // We can safely ignore these exceptions. The only time the
+ // call to postMessage can fail is if the worker is either
+ // shutting down, or has finished shutting down. In both
+ // cases, there is nothing to clean up, so we don't care
+ // whether this message arrives or not.
+ }
+ }
+
+ connection.cancelForwarding(forwardingPrefix);
+ },
+
+ onPacket: packet => {
+ // Ensure that any packets received from the server on the worker
+ // thread are forwarded to the client on the main thread, as if
+ // they had been sent by the server on the main thread.
+ connection.send(packet);
+ },
+ };
+
+ // Ensure that any packets received from the client on the main thread
+ // to actors on the worker thread are forwarded to the server on the
+ // worker thread.
+ connection.setForwarding(forwardingPrefix, transport);
+
+ resolve({
+ workerTargetForm: message.workerTargetForm,
+ transport,
+ });
+ },
+ };
+ dbg.addListener(listener);
+ });
+}
+
+exports.connectToWorker = connectToWorker;