summaryrefslogtreecommitdiffstats
path: root/devtools/server/connectors/js-window-actor
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/connectors/js-window-actor')
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsFrameChild.jsm443
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsFrameParent.jsm234
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsWorkerChild.jsm608
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsWorkerParent.jsm288
-rw-r--r--devtools/server/connectors/js-window-actor/WindowGlobalLogger.jsm67
-rw-r--r--devtools/server/connectors/js-window-actor/moz.build13
6 files changed, 1653 insertions, 0 deletions
diff --git a/devtools/server/connectors/js-window-actor/DevToolsFrameChild.jsm b/devtools/server/connectors/js-window-actor/DevToolsFrameChild.jsm
new file mode 100644
index 0000000000..cb285484c2
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/DevToolsFrameChild.jsm
@@ -0,0 +1,443 @@
+/* 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 EXPORTED_SYMBOLS = ["DevToolsFrameChild"];
+
+const { EventEmitter } = ChromeUtils.import(
+ "resource://gre/modules/EventEmitter.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const Loader = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ TargetActorRegistry:
+ "resource://devtools/server/actors/targets/target-actor-registry.jsm",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ WindowGlobalLogger:
+ "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.jsm",
+});
+
+// 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;
+
+/**
+ * Helper function to know if a given WindowGlobal should be exposed via watchTargets("frame") API
+ */
+function shouldNotifyWindowGlobal(windowGlobal, watchedBrowserId) {
+ const browsingContext = windowGlobal.browsingContext;
+
+ // Ignore about:blank loads, which spawn a document that never finishes loading
+ // and would require somewhat useless Target and all its related overload.
+ const window = Services.wm.getCurrentInnerWindowWithId(
+ windowGlobal.innerWindowId
+ );
+
+ // For some unknown reason, the print preview of PDFs generates an about:blank
+ // document, which, on the parent process, has windowGlobal.documentURI.spec
+ // set to the pdf's URL. So that Frame target helper accepts this WindowGlobal
+ // and instantiates a target for it.
+ // Which is great as this is a valuable document to debug.
+ // But in the content process, this ends up being an about:blank document, and
+ // hasLoadedNonBlankURI is always false. Nonetheless, this isn't a real empty
+ // about:blank. We end up loading resource://pdf.js/web/viewer.html.
+ // But `window.location` is set to about:blank, while `document.documentURI`
+ // is set to the pretty printed PDF...
+ // So we end up checking the documentURI in order to see if that's a special
+ // not-really-blank about:blank document!
+ if (
+ !window.docShell.hasLoadedNonBlankURI &&
+ window.document?.documentURI === "about:blank"
+ ) {
+ return false;
+ }
+
+ // If we are focusing only on a sub-tree of Browsing Element,
+ // Ignore the out of the sub tree elements.
+ if (watchedBrowserId && browsingContext.browserId != watchedBrowserId) {
+ return false;
+ }
+
+ // For now, we only mention the "remote frames".
+ // i.e. the frames which are in a distinct process compared to their parent document
+ // If there is no parent, this is most likely the top level document.
+ // Ignore it only if this is the top level target we are watching.
+ // For now we don't expect a target to be created, but we will as TabDescriptors arise.
+ if (
+ !browsingContext.parent &&
+ browsingContext.browserId == watchedBrowserId
+ ) {
+ return false;
+ }
+
+ // `isInProcess` is always false, even if the window runs in the same process.
+ // `osPid` attribute is not set on WindowGlobalChild
+ // so it is hard to guess if the given WindowGlobal runs in this process or not,
+ // which is what we want to know here. Here is a workaround way to know it :/
+ // ---
+ // Also. It might be a bit surprising to have a DevToolsFrameChild/JSWindowActorChild
+ // to be instantiated for WindowGlobals that aren't from this process... Is that expected?
+ if (Cu.isRemoteProxy(windowGlobal.window)) {
+ return false;
+ }
+
+ // When Fission is turned off, we still process here the iframes that are running in the
+ // same process.
+ // As we can't use isInProcess, nor osPid (see previous block), we have
+ // to fallback to other checks. Here we check if we are able to access the parent document's window.
+ // If we can, it means that it runs in the same process as the current iframe we are processing.
+ if (
+ browsingContext.parent &&
+ browsingContext.parent.window &&
+ !Cu.isRemoteProxy(browsingContext.parent.window)
+ ) {
+ return false;
+ }
+
+ return true;
+}
+
+function logWindowGlobal(windowGlobal, message) {
+ if (!DEBUG) {
+ return;
+ }
+ WindowGlobalLogger.logWindowGlobal(windowGlobal, message);
+}
+
+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 FrameTargetActor instance
+ this._connections = new Map();
+
+ this._onConnectionChange = this._onConnectionChange.bind(this);
+ EventEmitter.decorate(this);
+ }
+
+ instantiate() {
+ const { sharedData } = Services.cpmm;
+ const watchedDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
+ if (!watchedDataByWatcherActor) {
+ throw new Error(
+ "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets"
+ );
+ }
+
+ // Create one Target actor for each prefix/client which listen to frames
+ for (const [watcherActorID, watchedData] of watchedDataByWatcherActor) {
+ const { connectionPrefix, browserId } = watchedData;
+ if (
+ watchedData.targets.includes("frame") &&
+ shouldNotifyWindowGlobal(this.manager, browserId)
+ ) {
+ this._createTargetActor(watcherActorID, connectionPrefix, watchedData);
+ }
+ }
+ }
+
+ /**
+ * Instantiate a new WindowGlobalTarget for the given connection.
+ *
+ * @param String watcherActorID
+ * The ID of the WatcherActor who requested to observe and create these target actors.
+ * @param String parentConnectionPrefix
+ * The prefix of the DevToolsServerConnection of the Watcher Actor.
+ * This is used to compute a unique ID for the target actor.
+ * @param Object initialData
+ * 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.
+ */
+ _createTargetActor(watcherActorID, parentConnectionPrefix, initialData) {
+ if (this._connections.get(watcherActorID)) {
+ 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
+ );
+ this._connections.set(watcherActorID, {
+ connection,
+ actor: targetActor,
+ });
+
+ // Pass initialization data to the target actor
+ for (const type in initialData) {
+ targetActor.addWatcherDataEntry(type, initialData[type]);
+ }
+
+ // 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 quaranteed to be delivered in the order they
+ // were queued, we don't have to wait for anything around this sendAsyncMessage call.
+ // In theory, the FrameTargetActor may emit events in its constructor.
+ // If it does, such RDP packets may be lost. But in practice, no events
+ // are emitted during its construction. Instead the frontend will start
+ // the communication first.
+ this.sendAsyncMessage("DevToolsFrameChild:connectFromContent", {
+ watcherActorID,
+ forwardingPrefix,
+ actor: targetActor.form(),
+ });
+ }
+
+ _destroyTargetActor(watcherActorID) {
+ 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();
+ this._connections.delete(watcherActorID);
+ if (this._connections.size == 0) {
+ this.didDestroy();
+ }
+ }
+
+ _createConnectionAndActor(forwardingPrefix) {
+ this.useCustomLoader = this.document.nodePrincipal.isSystemPrincipal;
+
+ // When debugging chrome pages, use a new dedicated loader, using a distinct chrome compartment.
+ if (!this.loader) {
+ this.loader = this.useCustomLoader
+ ? new Loader.DevToolsLoader({
+ invisibleToDebugger: true,
+ })
+ : Loader;
+ }
+ const { DevToolsServer } = this.loader.require(
+ "devtools/server/devtools-server"
+ );
+
+ const { FrameTargetActor } = this.loader.require(
+ "devtools/server/actors/targets/frame"
+ );
+
+ DevToolsServer.init();
+
+ // We want a special server without any root actor and only target-scoped actors.
+ // We are going to spawn a FrameTargetActor instance in the next few lines,
+ // it is going to act like a root actor without being one.
+ DevToolsServer.registerActors({ target: true });
+ DevToolsServer.on("connectionchange", this._onConnectionChange);
+
+ const connection = DevToolsServer.connectToParentWindowActor(
+ this,
+ forwardingPrefix
+ );
+
+ // Create the actual target actor.
+ const targetActor = new FrameTargetActor(connection, this.docShell, {
+ followWindowGlobalLifeCycle: true,
+ doNotFireFrameUpdates: true,
+ });
+ targetActor.manage(targetActor);
+
+ return { connection, targetActor };
+ }
+
+ /**
+ * Destroy the server once its last connection closes. Note that multiple
+ * frame scripts may be running in parallel and reuse the same server.
+ */
+ _onConnectionChange() {
+ const { DevToolsServer } = this.loader.require(
+ "devtools/server/devtools-server"
+ );
+
+ // Only destroy the server if there is no more connections to it. It may be
+ // used to debug another tab running in the same process.
+ if (DevToolsServer.hasConnection() || DevToolsServer.keepAlive) {
+ return;
+ }
+
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ DevToolsServer.off("connectionchange", this._onConnectionChange);
+ DevToolsServer.destroy();
+ }
+
+ /**
+ * 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) {
+ // All messages but "packet" one pass `browserId` and are expected
+ // to match shouldNotifyWindowGlobal result.
+ if (message.name != "DevToolsFrameParent:packet") {
+ const { browserId } = message.data;
+ // 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 &&
+ !shouldNotifyWindowGlobal(this.manager, browserId)
+ ) {
+ throw new Error(
+ "Mismatch between DevToolsFrameParent and DevToolsFrameChild " +
+ (this.manager.browsingContext.browserId == browserId
+ ? "window global shouldn't be notified (shouldNotifyWindowGlobal mismatch)"
+ : `expected browsing context with browserId ${browserId}, but got ${this.manager.browsingContext.browserId}`)
+ );
+ }
+ }
+ switch (message.name) {
+ case "DevToolsFrameParent:instantiate-already-available": {
+ const { watcherActorID, connectionPrefix, watchedData } = message.data;
+ return this._createTargetActor(
+ watcherActorID,
+ connectionPrefix,
+ watchedData
+ );
+ }
+ case "DevToolsFrameParent:destroy": {
+ const { watcherActorID } = message.data;
+ return this._destroyTargetActor(watcherActorID);
+ }
+ case "DevToolsFrameParent:addWatcherDataEntry": {
+ const { watcherActorID, browserId, type, entries } = message.data;
+ return this._addWatcherDataEntry(
+ watcherActorID,
+ browserId,
+ type,
+ entries
+ );
+ }
+ case "DevToolsFrameParent:removeWatcherDataEntry": {
+ const { watcherActorID, browserId, type, entries } = message.data;
+ return this._removeWatcherDataEntry(
+ watcherActorID,
+ browserId,
+ type,
+ entries
+ );
+ }
+ case "DevToolsFrameParent:packet":
+ return this.emit("packet-received", message);
+ default:
+ throw new Error(
+ "Unsupported message in DevToolsFrameParent: " + message.name
+ );
+ }
+ }
+
+ _getTargetActorForWatcherActorID(watcherActorID, browserId) {
+ const connectionInfo = this._connections.get(watcherActorID);
+ let targetActor = connectionInfo ? connectionInfo.actor : null;
+ // We might not get the target actor created by DevToolsFrameChild.
+ // For the Tab top-level target for content toolbox,
+ // we are still using the "message manager connector",
+ // so that they keep working across navigation.
+ // We will surely remove all of this. "Message manager connector", and
+ // this special codepath once we are ready to make the top level target to
+ // be destroyed on navigations. See bug 1602748 for more context.
+ if (!targetActor && this.manager.browsingContext.browserId == browserId) {
+ // 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+$/, "");
+ targetActor = TargetActorRegistry.getTargetActor(
+ browserId,
+ connectionPrefix
+ );
+ }
+ return targetActor;
+ }
+
+ _addWatcherDataEntry(watcherActorID, browserId, type, entries) {
+ const targetActor = this._getTargetActorForWatcherActorID(
+ watcherActorID,
+ browserId
+ );
+ if (!targetActor) {
+ throw new Error(
+ `No target actor for this Watcher Actor ID:"${watcherActorID}" / BrowserId:${browserId}`
+ );
+ }
+ return targetActor.addWatcherDataEntry(type, entries);
+ }
+
+ _removeWatcherDataEntry(watcherActorID, browserId, type, entries) {
+ const targetActor = this._getTargetActorForWatcherActorID(
+ watcherActorID,
+ browserId
+ );
+ // By the time we are calling this, the target may already have been destroyed.
+ if (targetActor) {
+ return targetActor.removeWatcherDataEntry(type, entries);
+ }
+ return null;
+ }
+
+ handleEvent({ type }) {
+ // 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();
+ }
+ }
+
+ didDestroy() {
+ for (const [, connectionInfo] of this._connections) {
+ connectionInfo.connection.close();
+ }
+ this._connections.clear();
+ if (this.useCustomLoader) {
+ this.loader.destroy();
+ }
+ }
+}
diff --git a/devtools/server/connectors/js-window-actor/DevToolsFrameParent.jsm b/devtools/server/connectors/js-window-actor/DevToolsFrameParent.jsm
new file mode 100644
index 0000000000..a89c7c4f6d
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/DevToolsFrameParent.jsm
@@ -0,0 +1,234 @@
+/* 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 EXPORTED_SYMBOLS = ["DevToolsFrameParent"];
+const { loader } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { EventEmitter } = ChromeUtils.import(
+ "resource://gre/modules/EventEmitter.jsm"
+);
+const { WatcherRegistry } = ChromeUtils.import(
+ "resource://devtools/server/actors/watcher/WatcherRegistry.jsm"
+);
+
+loader.lazyRequireGetter(
+ this,
+ "JsWindowActorTransport",
+ "devtools/shared/transport/js-window-actor-transport",
+ true
+);
+
+class DevToolsFrameParent 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 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,
+ browserId,
+ watchedData,
+ }) {
+ try {
+ await this.sendQuery(
+ "DevToolsFrameParent:instantiate-already-available",
+ {
+ watcherActorID,
+ connectionPrefix,
+ browserId,
+ watchedData,
+ }
+ );
+ } catch (e) {
+ console.warn(
+ "Failed to create DevTools Frame target for browsingContext",
+ this.browsingContext.id
+ );
+ console.warn(e);
+ }
+ }
+
+ destroyTarget({ watcherActorID, browserId }) {
+ this.sendAsyncMessage("DevToolsFrameParent:destroy", {
+ watcherActorID,
+ browserId,
+ });
+ }
+
+ /**
+ * Communicate to the content process that some data have been added.
+ */
+ async addWatcherDataEntry({ watcherActorID, browserId, type, entries }) {
+ try {
+ await this.sendQuery("DevToolsFrameParent:addWatcherDataEntry", {
+ watcherActorID,
+ browserId,
+ type,
+ entries,
+ });
+ } catch (e) {
+ console.warn(
+ "Failed to add watcher 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.
+ */
+ removeWatcherDataEntry({ watcherActorID, browserId, type, entries }) {
+ this.sendAsyncMessage("DevToolsFrameParent:removeWatcherDataEntry", {
+ watcherActorID,
+ browserId,
+ 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 JsWindowActorTransport(this, forwardingPrefix);
+ transport.hooks = {
+ onPacket: connection.send.bind(connection),
+ onClosed() {},
+ };
+ 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, prefix) {
+ if (this._connections.has(prefix)) {
+ const { connection } = this._connections.get(prefix);
+ this._cleanupConnection(connection);
+ }
+ }
+
+ async _cleanupConnection(connection) {
+ const { forwardingPrefix, transport } = this._connections.get(
+ connection.prefix
+ );
+
+ connection.off("closed", this._onConnectionClosed);
+ if (transport) {
+ // If we have a child transport, the actor has already
+ // been created. We need to stop using this transport.
+ transport.close();
+ }
+
+ connection.cancelForwarding(forwardingPrefix);
+ this._connections.delete(connection.prefix);
+ if (!this._connections.size) {
+ this._destroy();
+ }
+ }
+
+ _destroy() {
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ for (const { actor, connection, watcher } of this._connections.values()) {
+ watcher.notifyTargetDestroyed(actor);
+
+ // XXX: we should probably get rid of this
+ if (actor && connection.transport) {
+ // The FrameTargetActor within the child process doesn't necessary
+ // have time to uninitialize itself when the frame is closed/killed.
+ // So ensure telling the client that the related actor is detached.
+ connection.send({ from: actor.actor, type: "tabDetached" });
+ }
+
+ this._cleanupConnection(connection);
+ }
+ }
+
+ /**
+ * 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);
+ default:
+ throw new Error(
+ "Unsupported message in DevToolsFrameParent: " + message.name
+ );
+ }
+ }
+
+ didDestroy() {
+ this._destroy();
+ }
+}
diff --git a/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.jsm b/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.jsm
new file mode 100644
index 0000000000..becb77a3bc
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.jsm
@@ -0,0 +1,608 @@
+/* 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 EXPORTED_SYMBOLS = ["DevToolsWorkerChild"];
+
+const { EventEmitter } = ChromeUtils.import(
+ "resource://gre/modules/EventEmitter.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "wdm",
+ "@mozilla.org/dom/workers/workerdebuggermanager;1",
+ "nsIWorkerDebuggerManager"
+);
+
+XPCOMUtils.defineLazyGetter(this, "Loader", () =>
+ ChromeUtils.import("resource://devtools/shared/Loader.jsm")
+);
+
+XPCOMUtils.defineLazyGetter(this, "DevToolsUtils", () =>
+ Loader.require("devtools/shared/DevToolsUtils")
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ WatchedDataHelpers:
+ "resource://devtools/server/actors/watcher/WatchedDataHelpers.jsm",
+});
+
+// Name of the attribute into which we save data in `sharedData` object.
+const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
+
+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.
+ // - watchedData: Data (targets, resources, …) the watcher wants to be notified about.
+ // See WatcherRegistry.getWatchedData to see the full list of properties.
+ this._connections = new Map();
+
+ this._onConnectionChange = this._onConnectionChange.bind(this);
+
+ 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 watchedDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
+ if (!watchedDataByWatcherActor) {
+ 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, watchedData] of watchedDataByWatcherActor) {
+ const { targets, connectionPrefix, browserId } = watchedData;
+ if (
+ targets.includes("worker") &&
+ shouldNotifyWindowGlobal(this.manager, browserId)
+ ) {
+ this._watchWorkerTargets({
+ watcherActorID,
+ parentConnectionPrefix: connectionPrefix,
+ watchedData,
+ // When navigating, this code is triggered _before_ the workers living on the page
+ // we navigate from are terminated, which would create worker targets for them again.
+ // Since at this point the new document can't have any workers yet, we are going to
+ // ignore existing targets (i.e. the workers that belong to the previous document).
+ ignoreExistingTargets: true,
+ });
+ }
+ }
+ }
+
+ /**
+ * 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 `browserId` (except packet) and are expected
+ // to match shouldNotifyWindowGlobal result.
+ if (message.name != "DevToolsWorkerParent:packet") {
+ const { browserId } = message.data;
+ // 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 &&
+ !shouldNotifyWindowGlobal(this.manager, browserId)
+ ) {
+ throw new Error(
+ "Mismatch between DevToolsWorkerParent and DevToolsWorkerChild " +
+ (this.manager.browsingContext.browserId == browserId
+ ? "window global shouldn't be notified (shouldNotifyWindowGlobal mismatch)"
+ : `expected browsing context with ID ${browserId}, but got ${this.manager.browsingContext.browserId}`)
+ );
+ }
+ }
+
+ switch (message.name) {
+ case "DevToolsWorkerParent:instantiate-already-available": {
+ const { watcherActorID, connectionPrefix, watchedData } = message.data;
+
+ return this._watchWorkerTargets({
+ watcherActorID,
+ parentConnectionPrefix: connectionPrefix,
+ watchedData,
+ });
+ }
+ case "DevToolsWorkerParent:destroy": {
+ const { watcherActorID } = message.data;
+ return this._destroyTargetActors(watcherActorID);
+ }
+ case "DevToolsWorkerParent:addWatcherDataEntry": {
+ const { watcherActorID, type, entries } = message.data;
+ return this._addWatcherDataEntry(watcherActorID, type, entries);
+ }
+ case "DevToolsWorkerParent:removeWatcherDataEntry": {
+ const { watcherActorID, type, entries } = message.data;
+ return this._removeWatcherDataEntry(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 browserId. 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.watchedData: Data (targets, resources, …) the watcher wants
+ * to be notified about. See WatcherRegistry.getWatchedData to see the full list
+ * of properties.
+ * @param {Boolean} options.ignoreExistingTargets: Set to true to not loop on existing
+ * workers. This is useful when this function is called at the very early stage
+ * of the life of a document, since workers of the previous document are still
+ * alive, and there's no way to filter them out.
+ */
+ async _watchWorkerTargets({
+ watcherActorID,
+ parentConnectionPrefix,
+ ignoreExistingTargets,
+ watchedData,
+ }) {
+ 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),
+ };
+ 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,
+ watchedData,
+ });
+
+ if (ignoreExistingTargets !== true) {
+ await Promise.all(
+ Array.from(wdm.getWorkerDebuggerEnumerator())
+ .filter(dbg => this._shouldHandleWorker(dbg))
+ .map(dbg =>
+ this._createWorkerTargetActor({
+ dbg,
+ connection,
+ forwardingPrefix,
+ watcherActorID,
+ })
+ )
+ );
+ }
+ }
+
+ _createConnection(forwardingPrefix) {
+ const { DevToolsServer } = Loader.require(
+ "devtools/server/devtools-server"
+ );
+
+ DevToolsServer.init();
+
+ // We want a special server without any root actor and only target-scoped actors.
+ // We are going to spawn a WorkerTargetActor instance in the next few lines,
+ // it is going to act like a root actor without being one.
+ DevToolsServer.registerActors({ target: true });
+ DevToolsServer.on("connectionchange", this._onConnectionChange);
+
+ const connection = DevToolsServer.connectToParentWindowActor(
+ this,
+ forwardingPrefix
+ );
+
+ return connection;
+ }
+
+ /**
+ * Indicates whether or not we should handle the worker debugger
+ *
+ * @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 (
+ DevToolsUtils.isWorkerDebuggerAlive(dbg) &&
+ dbg.type === Ci.nsIWorkerDebugger.TYPE_DEDICATED &&
+ dbg.window?.windowGlobalChild?.innerWindowId ===
+ 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 { watchedData } = watcherConnectionData;
+ const workerThreadServerForwardingPrefix = connection.allocID(
+ "workerTarget"
+ );
+
+ // Create the actual worker target actor, in the worker thread.
+ const { connectToWorker } = Loader.require(
+ "devtools/server/connectors/worker-connector"
+ );
+
+ const onConnectToWorker = connectToWorker(
+ connection,
+ dbg,
+ workerThreadServerForwardingPrefix,
+ {
+ watchedData,
+ }
+ );
+
+ 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 (DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
+ dbg.postMessage(
+ JSON.stringify({
+ type: "disconnect",
+ forwardingPrefix: workerThreadServerForwardingPrefix,
+ })
+ );
+ }
+ } catch (e) {}
+
+ transport.close();
+ }
+
+ watcherConnectionData.connection.close();
+ }
+
+ /**
+ * Destroy the server once its last connection closes. Note that multiple
+ * worker scripts may be running in parallel and reuse the same server.
+ */
+ _onConnectionChange() {
+ const { DevToolsServer } = Loader.require(
+ "devtools/server/devtools-server"
+ );
+
+ // Only destroy the server if there is no more connections to it. It may be
+ // used to debug another tab running in the same process.
+ if (DevToolsServer.hasConnection() || DevToolsServer.keepAlive) {
+ return;
+ }
+
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ DevToolsServer.off("connectionchange", this._onConnectionChange);
+ DevToolsServer.destroy();
+ }
+
+ async sendPacket(packet, prefix) {
+ return this.sendAsyncMessage("DevToolsWorkerChild:packet", {
+ packet,
+ prefix,
+ });
+ }
+
+ async _addWatcherDataEntry(watcherActorID, type, entries) {
+ const watcherConnectionData = this._connections.get(watcherActorID);
+ if (!watcherConnectionData) {
+ return;
+ }
+
+ WatchedDataHelpers.addWatchedDataEntry(
+ watcherConnectionData.watchedData,
+ type,
+ entries
+ );
+
+ const promises = [];
+ for (const {
+ dbg,
+ workerThreadServerForwardingPrefix,
+ } of watcherConnectionData.workers) {
+ promises.push(
+ addWatcherDataEntryInWorkerTarget({
+ dbg,
+ workerThreadServerForwardingPrefix,
+ type,
+ entries,
+ })
+ );
+ }
+ await Promise.all(promises);
+ }
+
+ _removeWatcherDataEntry(watcherActorID, type, entries) {
+ const watcherConnectionData = this._connections.get(watcherActorID);
+
+ if (!watcherConnectionData) {
+ return;
+ }
+
+ WatchedDataHelpers.removeWatchedDataEntry(
+ watcherConnectionData.watchedData,
+ type,
+ entries
+ );
+
+ for (const {
+ dbg,
+ workerThreadServerForwardingPrefix,
+ } of watcherConnectionData.workers) {
+ if (DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
+ dbg.postMessage(
+ JSON.stringify({
+ type: "remove-watcher-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) {
+ 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();
+ }
+}
+
+/**
+ * Helper function to know if we should watch for workers on a given windowGlobal
+ */
+function shouldNotifyWindowGlobal(windowGlobal, watchedBrowserId) {
+ const browsingContext = windowGlobal.browsingContext;
+
+ // If we are focusing only on a sub-tree of Browsing Element, ignore elements that are
+ // not part of it.
+ if (watchedBrowserId && browsingContext.browserId != watchedBrowserId) {
+ return false;
+ }
+
+ // `isInProcess` is always false, even if the window runs in the same process.
+ // `osPid` attribute is not set on WindowGlobalChild
+ // so it is hard to guess if the given WindowGlobal runs in this process or not,
+ // which is what we want to know here. Here is a workaround way to know it :/
+ // ---
+ // Also. It might be a bit surprising to have a DevToolsWorkerChild/JSWindowActorChild
+ // to be instantiated for WindowGlobals that aren't from this process... Is that expected?
+ if (Cu.isRemoteProxy(windowGlobal.window)) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * 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 addWatcherDataEntryInWorkerTarget({
+ dbg,
+ workerThreadServerForwardingPrefix,
+ type,
+ entries,
+}) {
+ if (!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 === "watcher-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-watcher-data-entry",
+ forwardingPrefix: workerThreadServerForwardingPrefix,
+ dataEntryType: type,
+ entries,
+ })
+ );
+ });
+}
diff --git a/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.jsm b/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.jsm
new file mode 100644
index 0000000000..616bd6e24c
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.jsm
@@ -0,0 +1,288 @@
+/* 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 EXPORTED_SYMBOLS = ["DevToolsWorkerParent"];
+const { loader } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { EventEmitter } = ChromeUtils.import(
+ "resource://gre/modules/EventEmitter.jsm"
+);
+const { WatcherRegistry } = ChromeUtils.import(
+ "resource://devtools/server/actors/watcher/WatcherRegistry.jsm"
+);
+
+loader.lazyRequireGetter(
+ this,
+ "JsWindowActorTransport",
+ "devtools/shared/transport/js-window-actor-transport",
+ true
+);
+
+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 browserId
+ * are already available.
+ */
+ async instantiateWorkerTargets({
+ watcherActorID,
+ connectionPrefix,
+ browserId,
+ watchedData,
+ }) {
+ try {
+ await this.sendQuery(
+ "DevToolsWorkerParent:instantiate-already-available",
+ {
+ watcherActorID,
+ connectionPrefix,
+ browserId,
+ watchedData,
+ }
+ );
+ } catch (e) {
+ console.warn(
+ "Failed to create DevTools Worker target for browsingContext",
+ this.browsingContext.id,
+ "and watcher actor id",
+ watcherActorID
+ );
+ console.warn(e);
+ }
+ }
+
+ destroyWorkerTargets({ watcher, browserId }) {
+ return this.sendAsyncMessage("DevToolsWorkerParent:destroy", {
+ watcherActorID: watcher.actorID,
+ browserId,
+ });
+ }
+
+ /**
+ * Communicate to the content process that some data have been added.
+ */
+ async addWatcherDataEntry({ watcherActorID, type, entries }) {
+ try {
+ await this.sendQuery("DevToolsWorkerParent:addWatcherDataEntry", {
+ watcherActorID,
+ type,
+ entries,
+ });
+ } catch (e) {
+ console.warn(
+ "Failed to add watcher 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.
+ */
+ removeWatcherDataEntry({ watcherActorID, type, entries }) {
+ this.sendAsyncMessage("DevToolsWorkerParent:removeWatcherDataEntry", {
+ watcherActorID,
+ 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 JsWindowActorTransport(this, forwardingPrefix);
+ transport.hooks = {
+ onPacket: connection.send.bind(connection),
+ onClosed() {},
+ };
+ 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) {
+ if (this._connections.has(prefix)) {
+ const { watcher } = this._connections.get(prefix);
+ this._cleanupConnection(watcher.conn);
+ }
+ }
+
+ async _cleanupConnection(connection) {
+ if (!this._connections || !this._connections.has(connection.prefix)) {
+ return;
+ }
+
+ const { transport } = this._connections.get(connection.prefix);
+
+ connection.off("closed", this._onConnectionClosed);
+ if (transport) {
+ // If we have a child transport, the actor has already
+ // been created. We need to stop using this transport.
+ connection.cancelForwarding(transport._prefix);
+ transport.close();
+ }
+
+ this._connections.delete(connection.prefix);
+ if (!this._connections.size) {
+ this._destroy();
+ }
+ }
+
+ _destroy() {
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ for (const { actors, watcher } of this._connections.values()) {
+ for (const actor of actors.values()) {
+ watcher.notifyTargetDestroyed(actor);
+ }
+
+ this._cleanupConnection(watcher.conn);
+ }
+ }
+
+ /**
+ * Part of JSActor API
+ * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
+ *
+ * > The didDestroy method, if present, will be called after the (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.jsm b/devtools/server/connectors/js-window-actor/WindowGlobalLogger.jsm
new file mode 100644
index 0000000000..a81c300a1b
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/WindowGlobalLogger.jsm
@@ -0,0 +1,67 @@
+/* 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 EXPORTED_SYMBOLS = ["WindowGlobalLogger"];
+
+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;
+ }
+
+ return windowGlobalUri;
+}
+
+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: function(windowGlobal, message) {
+ const { browsingContext } = windowGlobal;
+ const { parent } = browsingContext;
+ const windowGlobalUri = getWindowGlobalUri(windowGlobal);
+
+ const details = [];
+ details.push(
+ "BrowsingContext.browserId: " + browsingContext.browserId,
+ "BrowsingContext.id: " + browsingContext.id,
+ "innerWindowId: " + windowGlobal.innerWindowId,
+ "pid: " + windowGlobal.osPid,
+ "isClosed: " + windowGlobal.isClosed,
+ "isInProcess: " + windowGlobal.isInProcess,
+ "isCurrentGlobal: " + windowGlobal.isCurrentGlobal,
+ "currentRemoteType: " + browsingContext.currentRemoteType,
+ "hasParent: " + (parent ? parent.id : "no"),
+ "uri: " + (windowGlobalUri ? windowGlobalUri : "no uri")
+ );
+
+ 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..9aeb1507cd
--- /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.jsm",
+ "DevToolsFrameParent.jsm",
+ "DevToolsWorkerChild.jsm",
+ "DevToolsWorkerParent.jsm",
+ "WindowGlobalLogger.jsm",
+)