summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/targets
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/server/actors/targets
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/actors/targets')
-rw-r--r--devtools/server/actors/targets/base-target-actor.js114
-rw-r--r--devtools/server/actors/targets/content-process.js264
-rw-r--r--devtools/server/actors/targets/index.js12
-rw-r--r--devtools/server/actors/targets/moz.build20
-rw-r--r--devtools/server/actors/targets/parent-process.js166
-rw-r--r--devtools/server/actors/targets/session-data-processors/blackboxing.js19
-rw-r--r--devtools/server/actors/targets/session-data-processors/breakpoints.js37
-rw-r--r--devtools/server/actors/targets/session-data-processors/event-breakpoints.js26
-rw-r--r--devtools/server/actors/targets/session-data-processors/index.js50
-rw-r--r--devtools/server/actors/targets/session-data-processors/moz.build16
-rw-r--r--devtools/server/actors/targets/session-data-processors/resources.js17
-rw-r--r--devtools/server/actors/targets/session-data-processors/target-configuration.js23
-rw-r--r--devtools/server/actors/targets/session-data-processors/thread-configuration.js32
-rw-r--r--devtools/server/actors/targets/session-data-processors/xhr-breakpoints.js34
-rw-r--r--devtools/server/actors/targets/target-actor-registry.sys.mjs82
-rw-r--r--devtools/server/actors/targets/webextension.js371
-rw-r--r--devtools/server/actors/targets/window-global.js1932
-rw-r--r--devtools/server/actors/targets/worker.js134
18 files changed, 3349 insertions, 0 deletions
diff --git a/devtools/server/actors/targets/base-target-actor.js b/devtools/server/actors/targets/base-target-actor.js
new file mode 100644
index 0000000000..2108e74a0f
--- /dev/null
+++ b/devtools/server/actors/targets/base-target-actor.js
@@ -0,0 +1,114 @@
+/* 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 { Actor } = require("resource://devtools/shared/protocol.js");
+
+loader.lazyRequireGetter(
+ this,
+ "SessionDataProcessors",
+ "resource://devtools/server/actors/targets/session-data-processors/index.js",
+ true
+);
+
+class BaseTargetActor extends Actor {
+ constructor(conn, targetType, spec) {
+ super(conn, spec);
+
+ /**
+ * Type of target, a string of Targets.TYPES.
+ * @return {string}
+ */
+ this.targetType = targetType;
+ }
+
+ /**
+ * Process a new data entry, which can be watched resources, breakpoints, ...
+ *
+ * @param string type
+ * The type of data to be added
+ * @param Array<Object> entries
+ * The values to be added to this type of data
+ * @param Boolean isDocumentCreation
+ * Set to true if this function is called just after a new document (and its
+ * associated target) is created.
+ */
+ async addSessionDataEntry(type, entries, isDocumentCreation = false) {
+ const processor = SessionDataProcessors[type];
+ if (processor) {
+ await processor.addSessionDataEntry(this, entries, isDocumentCreation);
+ }
+ }
+
+ /**
+ * Remove data entries that have been previously added via addSessionDataEntry
+ *
+ * See addSessionDataEntry for argument description.
+ */
+ removeSessionDataEntry(type, entries) {
+ const processor = SessionDataProcessors[type];
+ if (processor) {
+ processor.removeSessionDataEntry(this, entries);
+ }
+ }
+
+ /**
+ * Called by Resource Watchers, when new resources are available, updated or destroyed.
+ *
+ * @param String updateType
+ * Can be "available", "updated" or "destroyed"
+ * @param Array<json> resources
+ * List of all resource's form. A resource is a JSON object piped over to the client.
+ * It can contain actor IDs, actor forms, to be manually marshalled by the client.
+ */
+ notifyResources(updateType, resources) {
+ if (resources.length === 0 || this.isDestroyed()) {
+ // Don't try to emit if the resources array is empty or the actor was
+ // destroyed.
+ return;
+ }
+
+ if (this.devtoolsSpawnedBrowsingContextForWebExtension) {
+ this.overrideResourceBrowsingContextForWebExtension(resources);
+ }
+
+ this.emit(`resource-${updateType}-form`, resources);
+ }
+
+ /**
+ * For WebExtension, we have to hack all resource's browsingContextID
+ * in order to ensure emitting them with the fixed, original browsingContextID
+ * related to the fallback document created by devtools which always exists.
+ * The target's form will always be relating to that BrowsingContext IDs (browsing context ID and inner window id).
+ * Even if the target switches internally to another document via WindowGlobalTargetActor._setWindow.
+ *
+ * @param {Array<Objects>} List of resources
+ */
+ overrideResourceBrowsingContextForWebExtension(resources) {
+ const browsingContextID =
+ this.devtoolsSpawnedBrowsingContextForWebExtension.id;
+ resources.forEach(
+ resource => (resource.browsingContextID = browsingContextID)
+ );
+ }
+
+ /**
+ * Try to return any target scoped actor instance, if it exists.
+ * They are lazily instantiated and so will only be available
+ * if the client called at least one of their method.
+ *
+ * @param {String} prefix
+ * Prefix for the actor we would like to retrieve.
+ * Defined in devtools/server/actors/utils/actor-registry.js
+ */
+ getTargetScopedActor(prefix) {
+ if (this.isDestroyed()) {
+ return null;
+ }
+ const form = this.form();
+ return this.conn._getOrCreateActor(form[prefix + "Actor"]);
+ }
+}
+exports.BaseTargetActor = BaseTargetActor;
diff --git a/devtools/server/actors/targets/content-process.js b/devtools/server/actors/targets/content-process.js
new file mode 100644
index 0000000000..239eae8bc8
--- /dev/null
+++ b/devtools/server/actors/targets/content-process.js
@@ -0,0 +1,264 @@
+/* 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";
+
+/*
+ * Target actor for all resources in a content process of Firefox (chrome sandboxes, frame
+ * scripts, documents, etc.)
+ *
+ * See devtools/docs/backend/actor-hierarchy.md for more details.
+ */
+
+const { ThreadActor } = require("resource://devtools/server/actors/thread.js");
+const {
+ WebConsoleActor,
+} = require("resource://devtools/server/actors/webconsole.js");
+const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
+const { Pool } = require("resource://devtools/shared/protocol.js");
+const { assert } = require("resource://devtools/shared/DevToolsUtils.js");
+const {
+ SourcesManager,
+} = require("resource://devtools/server/actors/utils/sources-manager.js");
+const {
+ contentProcessTargetSpec,
+} = require("resource://devtools/shared/specs/targets/content-process.js");
+const Targets = require("resource://devtools/server/actors/targets/index.js");
+const Resources = require("resource://devtools/server/actors/resources/index.js");
+const {
+ BaseTargetActor,
+} = require("resource://devtools/server/actors/targets/base-target-actor.js");
+const { TargetActorRegistry } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
+ {
+ loadInDevToolsLoader: false,
+ }
+);
+
+loader.lazyRequireGetter(
+ this,
+ "WorkerDescriptorActorList",
+ "resource://devtools/server/actors/worker/worker-descriptor-actor-list.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "MemoryActor",
+ "resource://devtools/server/actors/memory.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "TracerActor",
+ "resource://devtools/server/actors/tracer.js",
+ true
+);
+
+class ContentProcessTargetActor extends BaseTargetActor {
+ constructor(conn, { isXpcShellTarget = false, sessionContext } = {}) {
+ super(conn, Targets.TYPES.PROCESS, contentProcessTargetSpec);
+
+ this.threadActor = null;
+ this.isXpcShellTarget = isXpcShellTarget;
+ this.sessionContext = sessionContext;
+
+ // Use a see-everything debugger
+ this.makeDebugger = makeDebugger.bind(null, {
+ findDebuggees: dbg => dbg.findAllGlobals(),
+ shouldAddNewGlobalAsDebuggee: global => true,
+ });
+
+ const sandboxPrototype = {
+ get tabs() {
+ return Array.from(
+ Services.ww.getWindowEnumerator(),
+ win => win.docShell.messageManager
+ );
+ },
+ };
+
+ // Scope into which the webconsole executes:
+ // A sandbox with chrome privileges with a `tabs` getter.
+ const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+ );
+ const sandbox = Cu.Sandbox(systemPrincipal, {
+ sandboxPrototype,
+ wantGlobalProperties: ["ChromeUtils"],
+ });
+ this._consoleScope = sandbox;
+
+ this._workerList = null;
+ this._workerDescriptorActorPool = null;
+ this._onWorkerListChanged = this._onWorkerListChanged.bind(this);
+
+ // Try to destroy the Content Process Target when the content process shuts down.
+ // The parent process can't communicate during shutdown as the communication channel
+ // is already down (message manager or JS Window Actor API).
+ // So that we have to observe to some event fired from this process.
+ // While such cleanup doesn't sound ultimately necessary (the process will be completely destroyed)
+ // mochitests are asserting that there is no leaks during process shutdown.
+ // Do not override destroy as Protocol.js may override it when calling destroy,
+ // and we won't be able to call removeObserver correctly.
+ this.destroyObserver = this.destroy.bind(this);
+ Services.obs.addObserver(this.destroyObserver, "xpcom-shutdown");
+ if (this.isXpcShellTarget) {
+ TargetActorRegistry.registerXpcShellTargetActor(this);
+ }
+ }
+
+ get isRootActor() {
+ return true;
+ }
+
+ get url() {
+ return undefined;
+ }
+
+ get window() {
+ return this._consoleScope;
+ }
+
+ get sourcesManager() {
+ if (!this._sourcesManager) {
+ assert(
+ this.threadActor,
+ "threadActor should exist when creating SourcesManager."
+ );
+ this._sourcesManager = new SourcesManager(this.threadActor);
+ }
+ return this._sourcesManager;
+ }
+
+ /*
+ * Return a Debugger instance or create one if there is none yet
+ */
+ get dbg() {
+ if (!this._dbg) {
+ this._dbg = this.makeDebugger();
+ }
+ return this._dbg;
+ }
+
+ form() {
+ if (!this._consoleActor) {
+ this._consoleActor = new WebConsoleActor(this.conn, this);
+ this.manage(this._consoleActor);
+ }
+
+ if (!this.threadActor) {
+ this.threadActor = new ThreadActor(this, null);
+ this.manage(this.threadActor);
+ }
+ if (!this.memoryActor) {
+ this.memoryActor = new MemoryActor(this.conn, this);
+ this.manage(this.memoryActor);
+ }
+ if (!this.tracerActor) {
+ this.tracerActor = new TracerActor(this.conn, this);
+ this.manage(this.tracerActor);
+ }
+
+ return {
+ actor: this.actorID,
+ isXpcShellTarget: this.isXpcShellTarget,
+ processID: Services.appinfo.processID,
+ remoteType: Services.appinfo.remoteType,
+
+ consoleActor: this._consoleActor.actorID,
+ memoryActor: this.memoryActor.actorID,
+ threadActor: this.threadActor.actorID,
+ tracerActor: this.tracerActor.actorID,
+
+ traits: {
+ networkMonitor: false,
+ // See trait description in browsing-context.js
+ supportsTopLevelTargetFlag: false,
+ },
+ };
+ }
+
+ ensureWorkerList() {
+ if (!this._workerList) {
+ this._workerList = new WorkerDescriptorActorList(this.conn, {});
+ }
+ return this._workerList;
+ }
+
+ listWorkers() {
+ return this.ensureWorkerList()
+ .getList()
+ .then(actors => {
+ const pool = new Pool(this.conn, "workers");
+ for (const actor of actors) {
+ pool.manage(actor);
+ }
+
+ // Do not destroy the pool before transfering ownership to the newly created
+ // pool, so that we do not accidentally destroy actors that are still in use.
+ if (this._workerDescriptorActorPool) {
+ this._workerDescriptorActorPool.destroy();
+ }
+
+ this._workerDescriptorActorPool = pool;
+ this._workerList.onListChanged = this._onWorkerListChanged;
+
+ return { workers: actors };
+ });
+ }
+
+ _onWorkerListChanged() {
+ this.conn.send({ from: this.actorID, type: "workerListChanged" });
+ this._workerList.onListChanged = null;
+ }
+
+ pauseMatchingServiceWorkers(request) {
+ this.ensureWorkerList().workerPauser.setPauseServiceWorkers(request.origin);
+ }
+
+ destroy() {
+ // Avoid reentrancy. We will destroy the Transport when emitting "destroyed",
+ // which will force destroying all actors.
+ if (this.destroying) {
+ return;
+ }
+ this.destroying = true;
+
+ // Unregistering watchers first is important
+ // otherwise you might have leaks reported when running browser_browser_toolbox_netmonitor.js in debug builds
+ Resources.unwatchAllResources(this);
+
+ this.emit("destroyed");
+
+ super.destroy();
+
+ if (this.threadActor) {
+ this.threadActor = null;
+ }
+
+ // Tell the live lists we aren't watching any more.
+ if (this._workerList) {
+ this._workerList.destroy();
+ this._workerList = null;
+ }
+
+ if (this._sourcesManager) {
+ this._sourcesManager.destroy();
+ this._sourcesManager = null;
+ }
+
+ if (this._dbg) {
+ this._dbg.disable();
+ this._dbg = null;
+ }
+
+ Services.obs.removeObserver(this.destroyObserver, "xpcom-shutdown");
+
+ if (this.isXpcShellTarget) {
+ TargetActorRegistry.unregisterXpcShellTargetActor(this);
+ }
+ }
+}
+
+exports.ContentProcessTargetActor = ContentProcessTargetActor;
diff --git a/devtools/server/actors/targets/index.js b/devtools/server/actors/targets/index.js
new file mode 100644
index 0000000000..aced448459
--- /dev/null
+++ b/devtools/server/actors/targets/index.js
@@ -0,0 +1,12 @@
+/* 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 TYPES = {
+ FRAME: "frame",
+ PROCESS: "process",
+ WORKER: "worker",
+};
+exports.TYPES = TYPES;
diff --git a/devtools/server/actors/targets/moz.build b/devtools/server/actors/targets/moz.build
new file mode 100644
index 0000000000..f4d44ae669
--- /dev/null
+++ b/devtools/server/actors/targets/moz.build
@@ -0,0 +1,20 @@
+# -*- 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 += [
+ "session-data-processors",
+]
+
+DevToolsModules(
+ "base-target-actor.js",
+ "content-process.js",
+ "index.js",
+ "parent-process.js",
+ "target-actor-registry.sys.mjs",
+ "webextension.js",
+ "window-global.js",
+ "worker.js",
+)
diff --git a/devtools/server/actors/targets/parent-process.js b/devtools/server/actors/targets/parent-process.js
new file mode 100644
index 0000000000..2170bd0b1e
--- /dev/null
+++ b/devtools/server/actors/targets/parent-process.js
@@ -0,0 +1,166 @@
+/* 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";
+
+/*
+ * Target actor for the entire parent process.
+ *
+ * This actor extends WindowGlobalTargetActor.
+ * This actor is extended by WebExtensionTargetActor.
+ *
+ * See devtools/docs/backend/actor-hierarchy.md for more details.
+ */
+
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ getChildDocShells,
+ WindowGlobalTargetActor,
+} = require("resource://devtools/server/actors/targets/window-global.js");
+const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
+
+const {
+ parentProcessTargetSpec,
+} = require("resource://devtools/shared/specs/targets/parent-process.js");
+
+class ParentProcessTargetActor extends WindowGlobalTargetActor {
+ /**
+ * Creates a target actor for debugging all the chrome content in the parent process.
+ * Most of the implementation is inherited from WindowGlobalTargetActor.
+ * ParentProcessTargetActor is a child of RootActor, it can be instantiated via
+ * RootActor.getProcess request. ParentProcessTargetActor exposes all target-scoped actors
+ * via its form() request, like WindowGlobalTargetActor.
+ *
+ * @param conn DevToolsServerConnection
+ * The connection to the client.
+ * @param {Object} options
+ * - isTopLevelTarget: {Boolean} flag to indicate if this is the top
+ * level target of the DevTools session
+ * - sessionContext Object
+ * The Session Context to help know what is debugged.
+ * See devtools/server/actors/watcher/session-context.js
+ * - customSpec Object
+ * WebExtensionTargetActor inherits from ParentProcessTargetActor
+ * and has to use its own protocol.js specification object.
+ */
+ constructor(
+ conn,
+ { isTopLevelTarget, sessionContext, customSpec = parentProcessTargetSpec }
+ ) {
+ super(conn, {
+ isTopLevelTarget,
+ sessionContext,
+ customSpec,
+ });
+
+ // This creates a Debugger instance for chrome debugging all globals.
+ this.makeDebugger = makeDebugger.bind(null, {
+ findDebuggees: dbg => dbg.findAllGlobals(),
+ shouldAddNewGlobalAsDebuggee: () => true,
+ });
+
+ // Ensure catching the creation of any new content docshell
+ this.watchNewDocShells = true;
+
+ this.isRootActor = true;
+
+ // Listen for any new/destroyed chrome docshell
+ Services.obs.addObserver(this, "chrome-webnavigation-create");
+ Services.obs.addObserver(this, "chrome-webnavigation-destroy");
+
+ // If we are the parent process target actor and not a subclass
+ // (i.e. if we aren't the webext target actor)
+ // set the parent process docshell:
+ if (customSpec == parentProcessTargetSpec) {
+ this.setDocShell(this._getInitialDocShell());
+ }
+ }
+
+ // Overload setDocShell in order to observe all the docshells.
+ // WindowGlobalTargetActor only observes the top level one,
+ // but we also need to observe all of them for WebExtensionTargetActor subclass.
+ setDocShell(initialDocShell) {
+ super.setDocShell(initialDocShell);
+
+ // Iterate over all top-level windows.
+ for (const { docShell } of Services.ww.getWindowEnumerator()) {
+ if (docShell == this.docShell) {
+ continue;
+ }
+ this._progressListener.watch(docShell);
+ }
+ }
+
+ _getInitialDocShell() {
+ // Defines the default docshell selected for the target actor
+ let window = Services.wm.getMostRecentWindow(
+ DevToolsServer.chromeWindowType
+ );
+
+ // Default to any available top level window if there is no expected window
+ // eg when running ./mach run --chrome chrome://browser/content/aboutTabCrashed.xhtml --jsdebugger
+ if (!window) {
+ window = Services.wm.getMostRecentWindow(null);
+ }
+
+ // We really want _some_ window at least, so fallback to the hidden window if
+ // there's nothing else (such as during early startup).
+ if (!window) {
+ window = Services.appShell.hiddenDOMWindow;
+ }
+ return window.docShell;
+ }
+
+ /**
+ * Getter for the list of all docshells in this targetActor
+ * @return {Array}
+ */
+ get docShells() {
+ // Iterate over all top-level windows and all their docshells.
+ let docShells = [];
+ for (const { docShell } of Services.ww.getWindowEnumerator()) {
+ docShells = docShells.concat(getChildDocShells(docShell));
+ }
+
+ return docShells;
+ }
+
+ observe(subject, topic, data) {
+ super.observe(subject, topic, data);
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ subject.QueryInterface(Ci.nsIDocShell);
+
+ if (topic == "chrome-webnavigation-create") {
+ this._onDocShellCreated(subject);
+ } else if (topic == "chrome-webnavigation-destroy") {
+ this._onDocShellDestroy(subject);
+ }
+ }
+
+ _detach() {
+ if (this.isDestroyed()) {
+ return false;
+ }
+
+ Services.obs.removeObserver(this, "chrome-webnavigation-create");
+ Services.obs.removeObserver(this, "chrome-webnavigation-destroy");
+
+ // Iterate over all top-level windows.
+ for (const { docShell } of Services.ww.getWindowEnumerator()) {
+ if (docShell == this.docShell) {
+ continue;
+ }
+ this._progressListener.unwatch(docShell);
+ }
+
+ return super._detach();
+ }
+}
+
+exports.ParentProcessTargetActor = ParentProcessTargetActor;
diff --git a/devtools/server/actors/targets/session-data-processors/blackboxing.js b/devtools/server/actors/targets/session-data-processors/blackboxing.js
new file mode 100644
index 0000000000..a81333f108
--- /dev/null
+++ b/devtools/server/actors/targets/session-data-processors/blackboxing.js
@@ -0,0 +1,19 @@
+/* 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";
+
+module.exports = {
+ async addSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ for (const { url, range } of entries) {
+ targetActor.sourcesManager.blackBox(url, range);
+ }
+ },
+
+ removeSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ for (const { url, range } of entries) {
+ targetActor.sourcesManager.unblackBox(url, range);
+ }
+ },
+};
diff --git a/devtools/server/actors/targets/session-data-processors/breakpoints.js b/devtools/server/actors/targets/session-data-processors/breakpoints.js
new file mode 100644
index 0000000000..a3c778be59
--- /dev/null
+++ b/devtools/server/actors/targets/session-data-processors/breakpoints.js
@@ -0,0 +1,37 @@
+/* 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 {
+ STATES: THREAD_STATES,
+} = require("resource://devtools/server/actors/thread.js");
+
+module.exports = {
+ async addSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ const isTargetCreation =
+ targetActor.threadActor.state == THREAD_STATES.DETACHED;
+ if (isTargetCreation && !targetActor.targetType.endsWith("worker")) {
+ // If addSessionDataEntry is called during target creation, attach the
+ // thread actor automatically and pass the initial breakpoints.
+ // However, do not attach the thread actor for Workers. They use a codepath
+ // which releases the worker on `attach`. For them, the client will call `attach`. (bug 1691986)
+ await targetActor.threadActor.attach({ breakpoints: entries });
+ } else {
+ // If addSessionDataEntry is called for an existing target, set the new
+ // breakpoints on the already running thread actor.
+ await Promise.all(
+ entries.map(({ location, options }) =>
+ targetActor.threadActor.setBreakpoint(location, options)
+ )
+ );
+ }
+ },
+
+ removeSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ for (const { location } of entries) {
+ targetActor.threadActor.removeBreakpoint(location);
+ }
+ },
+};
diff --git a/devtools/server/actors/targets/session-data-processors/event-breakpoints.js b/devtools/server/actors/targets/session-data-processors/event-breakpoints.js
new file mode 100644
index 0000000000..5cd2004281
--- /dev/null
+++ b/devtools/server/actors/targets/session-data-processors/event-breakpoints.js
@@ -0,0 +1,26 @@
+/* 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 {
+ STATES: THREAD_STATES,
+} = require("resource://devtools/server/actors/thread.js");
+
+module.exports = {
+ async addSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ // Same as comments for XHR breakpoints. See lines 117-118
+ if (
+ targetActor.threadActor.state == THREAD_STATES.DETACHED &&
+ !targetActor.targetType.endsWith("worker")
+ ) {
+ targetActor.threadActor.attach();
+ }
+ targetActor.threadActor.addEventBreakpoints(entries);
+ },
+
+ removeSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ targetActor.threadActor.removeEventBreakpoints(entries);
+ },
+};
diff --git a/devtools/server/actors/targets/session-data-processors/index.js b/devtools/server/actors/targets/session-data-processors/index.js
new file mode 100644
index 0000000000..19b7d69302
--- /dev/null
+++ b/devtools/server/actors/targets/session-data-processors/index.js
@@ -0,0 +1,50 @@
+/* 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 {
+ SessionDataHelpers,
+} = require("resource://devtools/server/actors/watcher/SessionDataHelpers.jsm");
+const { SUPPORTED_DATA } = SessionDataHelpers;
+
+const SessionDataProcessors = {};
+
+loader.lazyRequireGetter(
+ SessionDataProcessors,
+ SUPPORTED_DATA.BLACKBOXING,
+ "resource://devtools/server/actors/targets/session-data-processors/blackboxing.js"
+);
+loader.lazyRequireGetter(
+ SessionDataProcessors,
+ SUPPORTED_DATA.BREAKPOINTS,
+ "resource://devtools/server/actors/targets/session-data-processors/breakpoints.js"
+);
+loader.lazyRequireGetter(
+ SessionDataProcessors,
+ SUPPORTED_DATA.EVENT_BREAKPOINTS,
+ "resource://devtools/server/actors/targets/session-data-processors/event-breakpoints.js"
+);
+loader.lazyRequireGetter(
+ SessionDataProcessors,
+ SUPPORTED_DATA.RESOURCES,
+ "resource://devtools/server/actors/targets/session-data-processors/resources.js"
+);
+loader.lazyRequireGetter(
+ SessionDataProcessors,
+ SUPPORTED_DATA.TARGET_CONFIGURATION,
+ "resource://devtools/server/actors/targets/session-data-processors/target-configuration.js"
+);
+loader.lazyRequireGetter(
+ SessionDataProcessors,
+ SUPPORTED_DATA.THREAD_CONFIGURATION,
+ "resource://devtools/server/actors/targets/session-data-processors/thread-configuration.js"
+);
+loader.lazyRequireGetter(
+ SessionDataProcessors,
+ SUPPORTED_DATA.XHR_BREAKPOINTS,
+ "resource://devtools/server/actors/targets/session-data-processors/xhr-breakpoints.js"
+);
+
+exports.SessionDataProcessors = SessionDataProcessors;
diff --git a/devtools/server/actors/targets/session-data-processors/moz.build b/devtools/server/actors/targets/session-data-processors/moz.build
new file mode 100644
index 0000000000..ea924d7d79
--- /dev/null
+++ b/devtools/server/actors/targets/session-data-processors/moz.build
@@ -0,0 +1,16 @@
+# -*- 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(
+ "blackboxing.js",
+ "breakpoints.js",
+ "event-breakpoints.js",
+ "index.js",
+ "resources.js",
+ "target-configuration.js",
+ "thread-configuration.js",
+ "xhr-breakpoints.js",
+)
diff --git a/devtools/server/actors/targets/session-data-processors/resources.js b/devtools/server/actors/targets/session-data-processors/resources.js
new file mode 100644
index 0000000000..c784e128d3
--- /dev/null
+++ b/devtools/server/actors/targets/session-data-processors/resources.js
@@ -0,0 +1,17 @@
+/* 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 Resources = require("resource://devtools/server/actors/resources/index.js");
+
+module.exports = {
+ async addSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ await Resources.watchResources(targetActor, entries);
+ },
+
+ removeSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ Resources.unwatchResources(targetActor, entries);
+ },
+};
diff --git a/devtools/server/actors/targets/session-data-processors/target-configuration.js b/devtools/server/actors/targets/session-data-processors/target-configuration.js
new file mode 100644
index 0000000000..e3fa7e0317
--- /dev/null
+++ b/devtools/server/actors/targets/session-data-processors/target-configuration.js
@@ -0,0 +1,23 @@
+/* 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";
+
+module.exports = {
+ async addSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ // Only WindowGlobalTargetActor implements updateTargetConfiguration,
+ // skip targetActor data entry update for other targets.
+ if (typeof targetActor.updateTargetConfiguration == "function") {
+ const options = {};
+ for (const { key, value } of entries) {
+ options[key] = value;
+ }
+ targetActor.updateTargetConfiguration(options, isDocumentCreation);
+ }
+ },
+
+ removeSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ // configuration data entries are always added/updated, never removed.
+ },
+};
diff --git a/devtools/server/actors/targets/session-data-processors/thread-configuration.js b/devtools/server/actors/targets/session-data-processors/thread-configuration.js
new file mode 100644
index 0000000000..e81ef819f0
--- /dev/null
+++ b/devtools/server/actors/targets/session-data-processors/thread-configuration.js
@@ -0,0 +1,32 @@
+/* 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 {
+ STATES: THREAD_STATES,
+} = require("resource://devtools/server/actors/thread.js");
+
+module.exports = {
+ async addSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ const threadOptions = {};
+
+ for (const { key, value } of entries) {
+ threadOptions[key] = value;
+ }
+
+ if (
+ !targetActor.targetType.endsWith("worker") &&
+ targetActor.threadActor.state == THREAD_STATES.DETACHED
+ ) {
+ await targetActor.threadActor.attach(threadOptions);
+ } else {
+ await targetActor.threadActor.reconfigure(threadOptions);
+ }
+ },
+
+ removeSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ // configuration data entries are always added/updated, never removed.
+ },
+};
diff --git a/devtools/server/actors/targets/session-data-processors/xhr-breakpoints.js b/devtools/server/actors/targets/session-data-processors/xhr-breakpoints.js
new file mode 100644
index 0000000000..d9a9bdc750
--- /dev/null
+++ b/devtools/server/actors/targets/session-data-processors/xhr-breakpoints.js
@@ -0,0 +1,34 @@
+/* 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 {
+ STATES: THREAD_STATES,
+} = require("resource://devtools/server/actors/thread.js");
+
+module.exports = {
+ async addSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ // The thread actor has to be initialized in order to correctly
+ // retrieve the stack trace when hitting an XHR
+ if (
+ targetActor.threadActor.state == THREAD_STATES.DETACHED &&
+ !targetActor.targetType.endsWith("worker")
+ ) {
+ await targetActor.threadActor.attach();
+ }
+
+ await Promise.all(
+ entries.map(({ path, method }) =>
+ targetActor.threadActor.setXHRBreakpoint(path, method)
+ )
+ );
+ },
+
+ removeSessionDataEntry(targetActor, entries, isDocumentCreation) {
+ for (const { path, method } of entries) {
+ targetActor.threadActor.removeXHRBreakpoint(path, method);
+ }
+ },
+};
diff --git a/devtools/server/actors/targets/target-actor-registry.sys.mjs b/devtools/server/actors/targets/target-actor-registry.sys.mjs
new file mode 100644
index 0000000000..4cb6d13868
--- /dev/null
+++ b/devtools/server/actors/targets/target-actor-registry.sys.mjs
@@ -0,0 +1,82 @@
+/* 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/. */
+
+// Keep track of all WindowGlobal target actors.
+// This is especially used to track the actors using Message manager connector,
+// or the ones running in the parent process.
+// Top level actors, like tab's top level target or parent process target
+// are still using message manager in order to avoid being destroyed on navigation.
+// And because of this, these actors aren't using JS Window Actor.
+const windowGlobalTargetActors = new Set();
+let xpcShellTargetActor = null;
+
+export var TargetActorRegistry = {
+ registerTargetActor(targetActor) {
+ windowGlobalTargetActors.add(targetActor);
+ },
+
+ unregisterTargetActor(targetActor) {
+ windowGlobalTargetActors.delete(targetActor);
+ },
+
+ registerXpcShellTargetActor(targetActor) {
+ xpcShellTargetActor = targetActor;
+ },
+
+ unregisterXpcShellTargetActor(targetActor) {
+ xpcShellTargetActor = null;
+ },
+
+ get xpcShellTargetActor() {
+ return xpcShellTargetActor;
+ },
+
+ /**
+ * Return the target actors matching the passed browser element id.
+ * In some scenarios, the registry can have multiple target actors for a given
+ * browserId (e.g. the regular DevTools content toolbox + DevTools WebExtensions targets).
+ *
+ * @param {Object} sessionContext: The Session Context to help know what is debugged.
+ * See devtools/server/actors/watcher/session-context.js
+ * @param {String} connectionPrefix: DevToolsServerConnection's prefix, in order to select only actor
+ * related to the same connection. i.e. the same client.
+ * @returns {Array<TargetActor>}
+ */
+ getTargetActors(sessionContext, connectionPrefix) {
+ const actors = [];
+ for (const actor of windowGlobalTargetActors) {
+ const isMatchingPrefix = actor.actorID.startsWith(connectionPrefix);
+ const isMatchingContext =
+ sessionContext.type == "all" ||
+ (sessionContext.type == "browser-element" &&
+ (actor.browserId == sessionContext.browserId ||
+ actor.openerBrowserId == sessionContext.browserId)) ||
+ (sessionContext.type == "webextension" &&
+ actor.addonId == sessionContext.addonId);
+ if (isMatchingPrefix && isMatchingContext) {
+ actors.push(actor);
+ }
+ }
+ return actors;
+ },
+
+ /**
+ * Helper for tests to help track the number of targets created for a given tab.
+ * (Used by browser_ext_devtools_inspectedWindow.js)
+ *
+ * @param {Number} browserId: ID for the tab
+ *
+ * @returns {Number} Number of targets for this tab.
+ */
+
+ getTargetActorsCountForBrowserElement(browserId) {
+ let count = 0;
+ for (const actor of windowGlobalTargetActors) {
+ if (actor.browserId == browserId) {
+ count++;
+ }
+ }
+ return count;
+ },
+};
diff --git a/devtools/server/actors/targets/webextension.js b/devtools/server/actors/targets/webextension.js
new file mode 100644
index 0000000000..e94580378a
--- /dev/null
+++ b/devtools/server/actors/targets/webextension.js
@@ -0,0 +1,371 @@
+/* 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";
+
+/*
+ * Target actor for a WebExtension add-on.
+ *
+ * This actor extends ParentProcessTargetActor.
+ *
+ * See devtools/docs/backend/actor-hierarchy.md for more details.
+ */
+
+const {
+ ParentProcessTargetActor,
+} = require("resource://devtools/server/actors/targets/parent-process.js");
+const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
+const {
+ webExtensionTargetSpec,
+} = require("resource://devtools/shared/specs/targets/webextension.js");
+
+const {
+ getChildDocShells,
+} = require("resource://devtools/server/actors/targets/window-global.js");
+
+loader.lazyRequireGetter(
+ this,
+ "unwrapDebuggerObjectGlobal",
+ "resource://devtools/server/actors/thread.js",
+ true
+);
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ getAddonIdForWindowGlobal:
+ "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
+});
+
+const FALLBACK_DOC_URL =
+ "chrome://devtools/content/shared/webextension-fallback.html";
+
+class WebExtensionTargetActor extends ParentProcessTargetActor {
+ /**
+ * Creates a target actor for debugging all the contexts associated to a target
+ * WebExtensions add-on running in a child extension process. Most of the implementation
+ * is inherited from ParentProcessTargetActor (which inherits most of its implementation
+ * from WindowGlobalTargetActor).
+ *
+ * WebExtensionTargetActor is created by a WebExtensionActor counterpart, when its
+ * parent actor's `connect` method has been called (on the listAddons RDP package),
+ * it runs in the same process that the extension is running into (which can be the main
+ * process if the extension is running in non-oop mode, or the child extension process
+ * if the extension is running in oop-mode).
+ *
+ * A WebExtensionTargetActor contains all target-scoped actors, like a regular
+ * ParentProcessTargetActor or WindowGlobalTargetActor.
+ *
+ * History lecture:
+ * - The add-on actors used to not inherit WindowGlobalTargetActor because of the
+ * different way the add-on APIs where exposed to the add-on itself, and for this reason
+ * the Addon Debugger has only a sub-set of the feature available in the Tab or in the
+ * Browser Toolbox.
+ * - In a WebExtensions add-on all the provided contexts (background, popups etc.),
+ * besides the Content Scripts which run in the content process, hooked to an existent
+ * tab, by creating a new WebExtensionActor which inherits from
+ * ParentProcessTargetActor, we can provide a full features Addon Toolbox (which is
+ * basically like a BrowserToolbox which filters the visible sources and frames to the
+ * one that are related to the target add-on).
+ * - When the WebExtensions OOP mode has been introduced, this actor has been refactored
+ * and moved from the main process to the new child extension process.
+ *
+ * @param {DevToolsServerConnection} conn
+ * The connection to the client.
+ * @param {nsIMessageSender} chromeGlobal.
+ * The chromeGlobal where this actor has been injected by the
+ * frame-connector.js connectToFrame method.
+ * @param {Object} options
+ * - addonId: {String} the addonId of the target WebExtension.
+ * - addonBrowsingContextGroupId: {String} the BrowsingContextGroupId used by this addon.
+ * - chromeGlobal: {nsIMessageSender} The chromeGlobal where this actor
+ * has been injected by the frame-connector.js connectToFrame method.
+ * - isTopLevelTarget: {Boolean} flag to indicate if this is the top
+ * level target of the DevTools session
+ * - prefix: {String} the custom RDP prefix to use.
+ * - sessionContext Object
+ * The Session Context to help know what is debugged.
+ * See devtools/server/actors/watcher/session-context.js
+ */
+ constructor(
+ conn,
+ {
+ addonId,
+ addonBrowsingContextGroupId,
+ chromeGlobal,
+ isTopLevelTarget,
+ prefix,
+ sessionContext,
+ }
+ ) {
+ super(conn, {
+ isTopLevelTarget,
+ sessionContext,
+ customSpec: webExtensionTargetSpec,
+ });
+
+ this.addonId = addonId;
+ this.addonBrowsingContextGroupId = addonBrowsingContextGroupId;
+ this._chromeGlobal = chromeGlobal;
+ this._prefix = prefix;
+
+ // Expose the BrowsingContext of the fallback document,
+ // which is the one this target actor will always refer to via its form()
+ // and all resources should be related to this one as we currently spawn
+ // only just this one target actor to debug all webextension documents.
+ this.devtoolsSpawnedBrowsingContextForWebExtension =
+ chromeGlobal.browsingContext;
+
+ // Redefine the messageManager getter to return the chromeGlobal
+ // as the messageManager for this actor (which is the browser XUL
+ // element used by the parent actor running in the main process to
+ // connect to the extension process).
+ Object.defineProperty(this, "messageManager", {
+ enumerable: true,
+ configurable: true,
+ get: () => {
+ return this._chromeGlobal;
+ },
+ });
+
+ this._onParentExit = this._onParentExit.bind(this);
+
+ this._chromeGlobal.addMessageListener(
+ "debug:webext_parent_exit",
+ this._onParentExit
+ );
+
+ // Set the consoleAPIListener filtering options
+ // (retrieved and used in the related webconsole child actor).
+ this.consoleAPIListenerOptions = {
+ addonId: this.addonId,
+ };
+
+ // This creates a Debugger instance for debugging all the add-on globals.
+ this.makeDebugger = makeDebugger.bind(null, {
+ findDebuggees: dbg => {
+ return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
+ },
+ shouldAddNewGlobalAsDebuggee:
+ this._shouldAddNewGlobalAsDebuggee.bind(this),
+ });
+
+ // NOTE: This is needed to catch in the webextension webconsole all the
+ // errors raised by the WebExtension internals that are not currently
+ // associated with any window.
+ this.isRootActor = true;
+
+ // Try to discovery an existent extension page to attach (which will provide the initial
+ // URL shown in the window tittle when the addon debugger is opened).
+ const extensionWindow = this._searchForExtensionWindow();
+ this.setDocShell(extensionWindow.docShell);
+ }
+
+ // Override the ParentProcessTargetActor's override in order to only iterate
+ // over the docshells specific to this add-on
+ get docShells() {
+ // Iterate over all top-level windows and all their docshells.
+ let docShells = [];
+ for (const window of Services.ww.getWindowEnumerator(null)) {
+ docShells = docShells.concat(getChildDocShells(window.docShell));
+ }
+ // Then filter out the ones specific to the add-on
+ return docShells.filter(docShell => {
+ return this.isExtensionWindowDescendent(docShell.domWindow);
+ });
+ }
+
+ /**
+ * Called when the actor is removed from the connection.
+ */
+ destroy() {
+ if (this._chromeGlobal) {
+ const chromeGlobal = this._chromeGlobal;
+ this._chromeGlobal = null;
+
+ chromeGlobal.removeMessageListener(
+ "debug:webext_parent_exit",
+ this._onParentExit
+ );
+
+ chromeGlobal.sendAsyncMessage("debug:webext_child_exit", {
+ actor: this.actorID,
+ });
+ }
+
+ if (this.fallbackWindow) {
+ this.fallbackWindow = null;
+ }
+
+ this.addon = null;
+ this.addonId = null;
+
+ return super.destroy();
+ }
+
+ // Private helpers.
+
+ _searchFallbackWindow() {
+ if (this.fallbackWindow) {
+ // Skip if there is already an existent fallback window.
+ return this.fallbackWindow;
+ }
+
+ // Set and initialize the fallbackWindow (which initially is a empty
+ // about:blank browser), this window is related to a XUL browser element
+ // specifically created for the devtools server and it is never used
+ // or navigated anywhere else.
+ this.fallbackWindow = this._chromeGlobal.content;
+
+ // Add the addonId in the URL to retrieve this information in other devtools
+ // helpers. The addonId is usually populated in the principal, but this will
+ // not be the case for the fallback window because it is loaded from chrome://
+ // instead of moz-extension://${addonId}
+ this.fallbackWindow.document.location.href = `${FALLBACK_DOC_URL}#${this.addonId}`;
+
+ return this.fallbackWindow;
+ }
+
+ // Discovery an extension page to use as a default target window.
+ // NOTE: This currently fail to discovery an extension page running in a
+ // windowless browser when running in non-oop mode, and the background page
+ // is set later using _onNewExtensionWindow.
+ _searchForExtensionWindow() {
+ // Looks if there is any top level add-on document:
+ // (we do not want to pass any nested add-on iframe)
+ const docShell = this.docShells.find(d =>
+ this.isTopLevelExtensionWindow(d.domWindow)
+ );
+ if (docShell) {
+ return docShell.domWindow;
+ }
+
+ return this._searchFallbackWindow();
+ }
+
+ // Customized ParentProcessTargetActor/WindowGlobalTargetActor hooks.
+
+ _onDocShellCreated(docShell) {
+ // Compare against the BrowsingContext's group ID as the document's principal addonId
+ // won't be set yet for freshly created docshells. It will be later set, when loading the addon URL.
+ // But right now, it is still on the initial about:blank document and the principal isn't related to the add-on.
+ if (docShell.browsingContext.group.id != this.addonBrowsingContextGroupId) {
+ return;
+ }
+ super._onDocShellCreated(docShell);
+ }
+
+ _onDocShellDestroy(docShell) {
+ if (docShell.browsingContext.group.id != this.addonBrowsingContextGroupId) {
+ return;
+ }
+ // Stop watching this docshell (the unwatch() method will check if we
+ // started watching it before).
+ this._unwatchDocShell(docShell);
+
+ // Let the _onDocShellDestroy notify that the docShell has been destroyed.
+ const webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ this._notifyDocShellDestroy(webProgress);
+
+ // If the destroyed docShell:
+ // * was the current docShell,
+ // * the actor is not destroyed,
+ // * isn't the background page, as it means the addon is being shutdown or reloaded
+ // and the target would be replaced by a new one to come, or everything is closing.
+ // => switch to the fallback window
+ if (
+ !this.isDestroyed() &&
+ docShell == this.docShell &&
+ !docShell.domWindow.location.href.includes(
+ "_generated_background_page.html"
+ )
+ ) {
+ this._changeTopLevelDocument(this._searchForExtensionWindow());
+ }
+ }
+
+ _onNewExtensionWindow(window) {
+ if (!this.window || this.window === this.fallbackWindow) {
+ this._changeTopLevelDocument(window);
+ // For new extension windows, the BrowsingContext group id might have
+ // changed, for instance when reloading the addon.
+ this.addonBrowsingContextGroupId =
+ window.docShell.browsingContext.group.id;
+ }
+ }
+
+ isTopLevelExtensionWindow(window) {
+ const { docShell } = window;
+ const isTopLevel = docShell.sameTypeRootTreeItem == docShell;
+ // Note: We are not using getAddonIdForWindowGlobal here because the
+ // fallback window should not be considered as a top level extension window.
+ return isTopLevel && window.document.nodePrincipal.addonId == this.addonId;
+ }
+
+ isExtensionWindowDescendent(window) {
+ // Check if the source is coming from a descendant docShell of an extension window.
+ // We may have an iframe that loads http content which won't use the add-on principal.
+ const rootWin = window.docShell.sameTypeRootTreeItem.domWindow;
+ const addonId = lazy.getAddonIdForWindowGlobal(rootWin.windowGlobalChild);
+ return addonId == this.addonId;
+ }
+
+ /**
+ * Return true if the given global is associated with this addon and should be
+ * added as a debuggee, false otherwise.
+ */
+ _shouldAddNewGlobalAsDebuggee(newGlobal) {
+ const global = unwrapDebuggerObjectGlobal(newGlobal);
+
+ if (global instanceof Ci.nsIDOMWindow) {
+ try {
+ global.document;
+ } catch (e) {
+ // The global might be a sandbox with a window object in its proto chain. If the
+ // window navigated away since the sandbox was created, it can throw a security
+ // exception during this property check as the sandbox no longer has access to
+ // its own proto.
+ return false;
+ }
+ // When `global` is a sandbox it may be a nsIDOMWindow object,
+ // but won't be the real Window object. Retrieve it via document's ownerGlobal.
+ const window = global.document.ownerGlobal;
+ if (!window) {
+ return false;
+ }
+
+ // Change top level document as a simulated frame switching.
+ if (this.isTopLevelExtensionWindow(window)) {
+ this._onNewExtensionWindow(window);
+ }
+
+ return this.isExtensionWindowDescendent(window);
+ }
+
+ try {
+ // This will fail for non-Sandbox objects, hence the try-catch block.
+ const metadata = Cu.getSandboxMetadata(global);
+ if (metadata) {
+ return metadata.addonID === this.addonId;
+ }
+ } catch (e) {
+ // Unable to retrieve the sandbox metadata.
+ }
+
+ return false;
+ }
+
+ // Handlers for the messages received from the parent actor.
+
+ _onParentExit(msg) {
+ if (msg.json.actor !== this.actorID) {
+ return;
+ }
+
+ this.destroy();
+ }
+}
+
+exports.WebExtensionTargetActor = WebExtensionTargetActor;
diff --git a/devtools/server/actors/targets/window-global.js b/devtools/server/actors/targets/window-global.js
new file mode 100644
index 0000000000..0a7df9c272
--- /dev/null
+++ b/devtools/server/actors/targets/window-global.js
@@ -0,0 +1,1932 @@
+/* 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";
+
+// protocol.js uses objects as exceptions in order to define
+// error packets.
+/* eslint-disable no-throw-literal */
+
+/*
+ * WindowGlobalTargetActor is an abstract class used by target actors that hold
+ * documents, such as frames, chrome windows, etc.
+ *
+ * This class is extended by ParentProcessTargetActor, itself being extented by WebExtensionTargetActor.
+ *
+ * See devtools/docs/backend/actor-hierarchy.md for more details.
+ *
+ * For performance matters, this file should only be loaded in the targeted context's
+ * process. For example, it shouldn't be evaluated in the parent process until we try to
+ * debug a document living in the parent process.
+ */
+
+var {
+ ActorRegistry,
+} = require("resource://devtools/server/actors/utils/actor-registry.js");
+var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+var { assert } = DevToolsUtils;
+var {
+ SourcesManager,
+} = require("resource://devtools/server/actors/utils/sources-manager.js");
+var makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
+const Targets = require("resource://devtools/server/actors/targets/index.js");
+const { TargetActorRegistry } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
+ {
+ loadInDevToolsLoader: false,
+ }
+);
+const { PrivateBrowsingUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
+);
+
+const EXTENSION_CONTENT_SYS_MJS =
+ "resource://gre/modules/ExtensionContent.sys.mjs";
+
+const { Pool } = require("resource://devtools/shared/protocol.js");
+const {
+ LazyPool,
+ createExtraActors,
+} = require("resource://devtools/shared/protocol/lazy-pool.js");
+const {
+ windowGlobalTargetSpec,
+} = require("resource://devtools/shared/specs/targets/window-global.js");
+const Resources = require("resource://devtools/server/actors/resources/index.js");
+const {
+ BaseTargetActor,
+} = require("resource://devtools/server/actors/targets/base-target-actor.js");
+
+loader.lazyRequireGetter(
+ this,
+ ["ThreadActor", "unwrapDebuggerObjectGlobal"],
+ "resource://devtools/server/actors/thread.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "WorkerDescriptorActorList",
+ "resource://devtools/server/actors/worker/worker-descriptor-actor-list.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "StyleSheetsManager",
+ "resource://devtools/server/actors/utils/stylesheets-manager.js",
+ true
+);
+const lazy = {};
+loader.lazyGetter(lazy, "ExtensionContent", () => {
+ return ChromeUtils.importESModule(EXTENSION_CONTENT_SYS_MJS, {
+ // ExtensionContent.sys.mjs is a singleton and must be loaded through the
+ // main loader. Note that the user of lazy.ExtensionContent elsewhere in
+ // this file (at webextensionsContentScriptGlobals) looks up the module
+ // via Cu.isESModuleLoaded, which also uses the main loader as desired.
+ loadInDevToolsLoader: false,
+ }).ExtensionContent;
+});
+
+loader.lazyRequireGetter(
+ this,
+ "TouchSimulator",
+ "resource://devtools/server/actors/emulation/touch-simulator.js",
+ true
+);
+
+function getWindowID(window) {
+ return window.windowGlobalChild.innerWindowId;
+}
+
+function getDocShellChromeEventHandler(docShell) {
+ let handler = docShell.chromeEventHandler;
+ if (!handler) {
+ try {
+ // Toplevel xul window's docshell doesn't have chromeEventHandler
+ // attribute. The chrome event handler is just the global window object.
+ handler = docShell.domWindow;
+ } catch (e) {
+ // ignore
+ }
+ }
+ return handler;
+}
+
+/**
+ * Helper to retrieve all children docshells of a given docshell.
+ *
+ * Given that docshell interfaces can only be used within the same process,
+ * this only returns docshells for children documents that runs in the same process
+ * as the given docshell.
+ */
+function getChildDocShells(parentDocShell) {
+ return parentDocShell.browsingContext
+ .getAllBrowsingContextsInSubtree()
+ .filter(browsingContext => {
+ // Filter out browsingContext which don't expose any docshell (e.g. remote frame)
+ return browsingContext.docShell;
+ })
+ .map(browsingContext => {
+ // Map BrowsingContext to DocShell
+ return browsingContext.docShell;
+ });
+}
+
+exports.getChildDocShells = getChildDocShells;
+
+/**
+ * Browser-specific actors.
+ */
+
+function getInnerId(window) {
+ return window.windowGlobalChild.innerWindowId;
+}
+
+class WindowGlobalTargetActor extends BaseTargetActor {
+ /**
+ * WindowGlobalTargetActor is the target actor to debug (HTML) documents.
+ *
+ * WindowGlobal's are the Gecko representation for a given document's window object.
+ * It relates to a given nsGlobalWindowInner instance.
+ *
+ * The main goal of this class is to expose the target-scoped actors being registered
+ * via `ActorRegistry.registerModule` and manage their lifetimes. In addition, this
+ * class also tracks the lifetime of the targeted window global.
+ *
+ * ### Main requests:
+ *
+ * `detach`:
+ * Stop document watching and cleanup everything that the target and its children actors created.
+ * It ultimately lead to destroy the target actor.
+ * `switchToFrame`:
+ * Change the targeted document of the whole actor, and its child target-scoped actors
+ * to an iframe or back to its original document.
+ *
+ * Most properties (like `chromeEventHandler` or `docShells`) are meant to be
+ * used by the various child target actors.
+ *
+ * ### RDP events:
+ *
+ * - `tabNavigated`:
+ * Sent when the window global is about to navigate or has just navigated
+ * to a different document.
+ * This event contains the following attributes:
+ * * url (string)
+ * The new URI being loaded.
+ * * state (string)
+ * `start` if we just start requesting the new URL
+ * `stop` if the new URL is done loading
+ * * isFrameSwitching (boolean)
+ * Indicates the event is dispatched when switching the actor context to a
+ * different frame. When we switch to an iframe, there is no document
+ * load. The targeted document is most likely going to be already done
+ * loading.
+ * * title (string)
+ * The document title being loaded. (sent only on state=stop)
+ *
+ * - `frameUpdate`:
+ * Sent when there was a change in the child frames contained in the document
+ * or when the actor's context was switched to another frame.
+ * This event can have four different forms depending on the type of change:
+ * * One or many frames are updated:
+ * { frames: [{ id, url, title, parentID }, ...] }
+ * * One frame got destroyed:
+ * { frames: [{ id, destroy: true }]}
+ * * All frames got destroyed:
+ * { destroyAll: true }
+ * * We switched the context of the actor to a specific frame:
+ * { selected: #id }
+ *
+ * ### Internal, non-rdp events:
+ *
+ * Various events are also dispatched on the actor itself without being sent to
+ * the client. They all relate to the documents tracked by this target actor
+ * (its main targeted document, but also any of its iframes):
+ * - will-navigate
+ * This event fires once navigation starts. All pending user prompts are
+ * dealt with, but it is fired before the first request starts.
+ * - navigate
+ * This event is fired once the document's readyState is "complete".
+ * - window-ready
+ * This event is fired in various distinct scenarios:
+ * * When a new Window object is crafted, equivalent of `DOMWindowCreated`.
+ * It is dispatched before any page script is executed.
+ * * We will have already received a window-ready event for this window
+ * when it was created, but we received a window-destroyed event when
+ * it was frozen into the bfcache, and now the user navigated back to
+ * this page, so it's now live again and we should resume handling it.
+ * * For each existing document, when an `attach` request is received.
+ * At this point scripts in the page will be already loaded.
+ * * When `swapFrameLoaders` is used, such as with moving window globals
+ * between windows or toggling Responsive Design Mode.
+ * - window-destroyed
+ * This event is fired in two cases:
+ * * When the window object is destroyed, i.e. when the related document
+ * is garbage collected. This can happen when the window global is
+ * closed or the iframe is removed from the DOM.
+ * It is equivalent of `inner-window-destroyed` event.
+ * * When the page goes into the bfcache and gets frozen.
+ * The equivalent of `pagehide`.
+ * - changed-toplevel-document
+ * This event fires when we switch the actor's targeted document
+ * to one of its iframes, or back to its original top document.
+ * It is dispatched between window-destroyed and window-ready.
+ *
+ * Note that *all* these events are dispatched in the following order
+ * when we switch the context of the actor to a given iframe:
+ * - will-navigate
+ * - window-destroyed
+ * - changed-toplevel-document
+ * - window-ready
+ * - navigate
+ *
+ * This class is subclassed by ParentProcessTargetActor and others.
+ * Subclasses are expected to implement a getter for the docShell property.
+ *
+ * @param conn DevToolsServerConnection
+ * The conection to the client.
+ * @param options Object
+ * Object with following attributes:
+ * - docShell nsIDocShell
+ * The |docShell| for the debugged frame.
+ * - followWindowGlobalLifeCycle Boolean
+ * If true, the target actor will only inspect the current WindowGlobal (and its children windows).
+ * But won't inspect next document loaded in the same BrowsingContext.
+ * The actor will behave more like a WindowGlobalTarget rather than a BrowsingContextTarget.
+ * This is always true for Tab debugging, but not yet for parent process/web extension.
+ * - isTopLevelTarget Boolean
+ * Should be set to true for all top-level targets. A top level target
+ * is the topmost target of a DevTools "session". For instance for a local
+ * tab toolbox, the WindowGlobalTargetActor for the content page is the top level target.
+ * For the Multiprocess Browser Toolbox, the parent process target is the top level
+ * target.
+ * At the moment this only impacts the WindowGlobalTarget `reconfigure`
+ * implementation. But for server-side target switching this flag will be exposed
+ * to the client and should be available for all target actor classes. It will be
+ * used to detect target switching. (Bug 1644397)
+ * - ignoreSubFrames Boolean
+ * If true, the actor will only focus on the passed docShell and not on the whole
+ * docShell tree. This should be enabled when we have targets for all documents.
+ * - sessionContext Object
+ * The Session Context to help know what is debugged.
+ * See devtools/server/actors/watcher/session-context.js
+ */
+ constructor(
+ conn,
+ {
+ docShell,
+ followWindowGlobalLifeCycle,
+ isTopLevelTarget,
+ ignoreSubFrames,
+ sessionContext,
+ customSpec = windowGlobalTargetSpec,
+ }
+ ) {
+ super(conn, Targets.TYPES.FRAME, customSpec);
+
+ this.followWindowGlobalLifeCycle = followWindowGlobalLifeCycle;
+ this.isTopLevelTarget = !!isTopLevelTarget;
+ this.ignoreSubFrames = ignoreSubFrames;
+ this.sessionContext = sessionContext;
+
+ // A map of actor names to actor instances provided by extensions.
+ this._extraActors = {};
+ this._sourcesManager = null;
+
+ this._shouldAddNewGlobalAsDebuggee =
+ this._shouldAddNewGlobalAsDebuggee.bind(this);
+
+ this.makeDebugger = makeDebugger.bind(null, {
+ findDebuggees: () => {
+ const result = [];
+ const inspectUAWidgets = Services.prefs.getBoolPref(
+ "devtools.inspector.showAllAnonymousContent",
+ false
+ );
+ for (const win of this.windows) {
+ result.push(win);
+ // Only expose User Agent internal (like <video controls>) when the
+ // related pref is set.
+ if (inspectUAWidgets) {
+ const principal = win.document.nodePrincipal;
+ // We don't use UA widgets for the system principal.
+ if (!principal.isSystemPrincipal) {
+ result.push(Cu.getUAWidgetScope(principal));
+ }
+ }
+ }
+ return result.concat(this.webextensionsContentScriptGlobals);
+ },
+ shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee,
+ });
+
+ // Flag eventually overloaded by sub classes in order to watch new docshells
+ // Used by the ParentProcessTargetActor to list all frames in the Browser Toolbox
+ this.watchNewDocShells = false;
+
+ this._workerDescriptorActorList = null;
+ this._workerDescriptorActorPool = null;
+ this._onWorkerDescriptorActorListChanged =
+ this._onWorkerDescriptorActorListChanged.bind(this);
+
+ this._onConsoleApiProfilerEvent =
+ this._onConsoleApiProfilerEvent.bind(this);
+ Services.obs.addObserver(
+ this._onConsoleApiProfilerEvent,
+ "console-api-profiler"
+ );
+
+ // Start observing navigations as well as sub documents.
+ // (This is probably meant to disappear once EFT is the only supported codepath)
+ this._progressListener = new DebuggerProgressListener(this);
+
+ TargetActorRegistry.registerTargetActor(this);
+
+ if (docShell) {
+ this.setDocShell(docShell);
+ }
+ }
+
+ /**
+ * Define the initial docshell.
+ *
+ * This is called from the constructor for WindowGlobalTargetActor,
+ * or from sub class constructors: WebExtensionTargetActor and ParentProcessTargetActor.
+ *
+ * This is to circumvent the fact that sub classes need to call inner method
+ * to compute the initial docshell and we can't call inner methods before calling
+ * the base class constructor...
+ */
+ setDocShell(docShell) {
+ Object.defineProperty(this, "docShell", {
+ value: docShell,
+ configurable: true,
+ writable: true,
+ });
+
+ // Save references to the original document we attached to
+ this._originalWindow = this.window;
+
+ // Update isPrivate as window is based on docShell
+ this.isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(this.window);
+
+ // Instantiate the Thread Actor immediately.
+ // This is the only one actor instantiated right away by the target actor.
+ // All the others are instantiated lazily on first request made the client,
+ // via LazyPool API.
+ this._createThreadActor();
+
+ // Ensure notifying about the target actor first
+ // before notifying about new docshells.
+ // Otherwise we would miss these RDP event as the client hasn't
+ // yet received the target actor's form.
+ // (This is also probably meant to disappear once EFT is the only supported codepath)
+ this._docShellsObserved = false;
+ DevToolsUtils.executeSoon(() => this._watchDocshells());
+ }
+
+ get docShell() {
+ throw new Error(
+ "A docShell should be provided as constructor argument of WindowGlobalTargetActor, or redefined by the subclass"
+ );
+ }
+
+ // Optional console API listener options (e.g. used by the WebExtensionActor to
+ // filter console messages by addonID), set to an empty (no options) object by default.
+ consoleAPIListenerOptions = {};
+
+ /*
+ * Return a Debugger instance or create one if there is none yet
+ */
+ get dbg() {
+ if (!this._dbg) {
+ this._dbg = this.makeDebugger();
+ }
+ return this._dbg;
+ }
+
+ /**
+ * Try to locate the console actor if it exists.
+ */
+ get _consoleActor() {
+ if (this.isDestroyed()) {
+ return null;
+ }
+ const form = this.form();
+ return this.conn._getOrCreateActor(form.consoleActor);
+ }
+
+ get _memoryActor() {
+ if (this.isDestroyed()) {
+ return null;
+ }
+ const form = this.form();
+ return this.conn._getOrCreateActor(form.memoryActor);
+ }
+
+ _targetScopedActorPool = null;
+
+ /**
+ * An object on which listen for DOMWindowCreated and pageshow events.
+ */
+ get chromeEventHandler() {
+ return getDocShellChromeEventHandler(this.docShell);
+ }
+
+ /**
+ * Getter for the nsIMessageManager associated to the window global.
+ */
+ get messageManager() {
+ try {
+ return this.docShell.messageManager;
+ } catch (e) {
+ // In some cases we can't get a docshell. We just have no message manager
+ // then,
+ return null;
+ }
+ }
+
+ /**
+ * Getter for the list of all `docShell`s in the window global.
+ * @return {Array}
+ */
+ get docShells() {
+ if (this.ignoreSubFrames) {
+ return [this.docShell];
+ }
+
+ return getChildDocShells(this.docShell);
+ }
+
+ /**
+ * Getter for the window global's current DOM window.
+ */
+ get window() {
+ return this.docShell && !this.docShell.isBeingDestroyed()
+ ? this.docShell.domWindow
+ : null;
+ }
+
+ get outerWindowID() {
+ if (this.docShell) {
+ return this.docShell.outerWindowID;
+ }
+ return null;
+ }
+
+ get browsingContext() {
+ return this.docShell?.browsingContext;
+ }
+
+ get browsingContextID() {
+ return this.browsingContext?.id;
+ }
+
+ get browserId() {
+ return this.browsingContext?.browserId;
+ }
+
+ get openerBrowserId() {
+ return this.browsingContext?.opener?.browserId;
+ }
+
+ /**
+ * Getter for the WebExtensions ContentScript globals related to the
+ * window global's current DOM window.
+ */
+ get webextensionsContentScriptGlobals() {
+ // Only retrieve the content scripts globals if the ExtensionContent JSM module
+ // has been already loaded (which is true if the WebExtensions internals have already
+ // been loaded in the same content process).
+ if (Cu.isESModuleLoaded(EXTENSION_CONTENT_SYS_MJS)) {
+ return lazy.ExtensionContent.getContentScriptGlobals(this.window);
+ }
+
+ return [];
+ }
+
+ /**
+ * Getter for the list of all content DOM windows in the window global.
+ * @return {Array}
+ */
+ get windows() {
+ return this.docShells.map(docShell => {
+ return docShell.domWindow;
+ });
+ }
+
+ /**
+ * Getter for the original docShell this actor got attached to in the first
+ * place.
+ * Note that your actor should normally *not* rely on this top level docShell
+ * if you want it to show information relative to the iframe that's currently
+ * being inspected in the toolbox.
+ */
+ get originalDocShell() {
+ if (!this._originalWindow) {
+ return this.docShell;
+ }
+
+ return this._originalWindow.docShell;
+ }
+
+ /**
+ * Getter for the original window this actor got attached to in the first
+ * place.
+ * Note that your actor should normally *not* rely on this top level window if
+ * you want it to show information relative to the iframe that's currently
+ * being inspected in the toolbox.
+ */
+ get originalWindow() {
+ return this._originalWindow || this.window;
+ }
+
+ /**
+ * Getter for the nsIWebProgress for watching this window.
+ */
+ get webProgress() {
+ return this.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ }
+
+ /**
+ * Getter for the nsIWebNavigation for the target.
+ */
+ get webNavigation() {
+ return this.docShell.QueryInterface(Ci.nsIWebNavigation);
+ }
+
+ /**
+ * Getter for the window global's document.
+ */
+ get contentDocument() {
+ return this.webNavigation.document;
+ }
+
+ /**
+ * Getter for the window global's title.
+ */
+ get title() {
+ return this.contentDocument.title;
+ }
+
+ /**
+ * Getter for the window global's URL.
+ */
+ get url() {
+ if (this.webNavigation.currentURI) {
+ return this.webNavigation.currentURI.spec;
+ }
+ // Abrupt closing of the browser window may leave callbacks without a
+ // currentURI.
+ return null;
+ }
+
+ get sourcesManager() {
+ if (!this._sourcesManager) {
+ this._sourcesManager = new SourcesManager(this.threadActor);
+ }
+ return this._sourcesManager;
+ }
+
+ getStyleSheetsManager() {
+ if (!this._styleSheetsManager) {
+ this._styleSheetsManager = new StyleSheetsManager(this);
+ }
+ return this._styleSheetsManager;
+ }
+
+ _createExtraActors() {
+ // Always use the same Pool, so existing actor instances
+ // (created in createExtraActors) are not lost.
+ if (!this._targetScopedActorPool) {
+ this._targetScopedActorPool = new LazyPool(this.conn);
+ }
+
+ // Walk over target-scoped actor factories and make sure they are all
+ // instantiated and added into the Pool.
+ return createExtraActors(
+ ActorRegistry.targetScopedActorFactories,
+ this._targetScopedActorPool,
+ this
+ );
+ }
+
+ form() {
+ assert(
+ !this.isDestroyed(),
+ "form() shouldn't be called on destroyed browser actor."
+ );
+ assert(this.actorID, "Actor should have an actorID.");
+
+ // Note that we don't want the iframe dropdown to change our BrowsingContext.id/innerWindowId
+ // We only want to refer to the topmost original window we attached to
+ // as that's the one top document this target actor really represent.
+ // The iframe dropdown is just a hack that temporarily focus the scope
+ // of the target actor to a children iframe document.
+ //
+ // Also, for WebExtension, we want the target to represent the <browser> element
+ // created by DevTools, which always exists and help better connect resources to the target
+ // in the frontend. Otherwise all other <browser> element of webext may be reloaded or go away
+ // and then we would have troubles matching targets for resources.
+ const originalBrowsingContext = this
+ .devtoolsSpawnedBrowsingContextForWebExtension
+ ? this.devtoolsSpawnedBrowsingContextForWebExtension
+ : this.originalDocShell.browsingContext;
+ const browsingContextID = originalBrowsingContext.id;
+ const innerWindowId =
+ originalBrowsingContext.currentWindowContext.innerWindowId;
+ const parentInnerWindowId =
+ originalBrowsingContext.parent?.currentWindowContext.innerWindowId;
+ // Doesn't only check `!!opener` as some iframe might have an opener
+ // if their location was loaded via `window.open(url, "iframe-name")`.
+ // So also ensure that the document is opened in a distinct tab.
+ const isPopup =
+ !!originalBrowsingContext.opener &&
+ originalBrowsingContext.browserId !=
+ originalBrowsingContext.opener.browserId;
+
+ const response = {
+ actor: this.actorID,
+ browsingContextID,
+ processID: Services.appinfo.processID,
+ // True for targets created by JSWindowActors, see constructor JSDoc.
+ followWindowGlobalLifeCycle: this.followWindowGlobalLifeCycle,
+ innerWindowId,
+ parentInnerWindowId,
+ topInnerWindowId: this.browsingContext.topWindowContext.innerWindowId,
+ isTopLevelTarget: this.isTopLevelTarget,
+ ignoreSubFrames: this.ignoreSubFrames,
+ isPopup,
+ isPrivate: this.isPrivate,
+ traits: {
+ // @backward-compat { version 64 } Exposes a new trait to help identify
+ // BrowsingContextActor's inherited actors from the client side.
+ isBrowsingContext: true,
+ // Browsing context targets can compute the isTopLevelTarget flag on the
+ // server. But other target actors don't support this yet. See Bug 1709314.
+ supportsTopLevelTargetFlag: true,
+ // Supports frame listing via `listFrames` request and `frameUpdate` events
+ // as well as frame switching via `switchToFrame` request
+ frames: true,
+ // Supports the logInPage request.
+ logInPage: true,
+ // Supports watchpoints in the server. We need to keep this trait because target
+ // actors that don't extend WindowGlobalTargetActor (Worker, ContentProcess, …)
+ // might not support watchpoints.
+ watchpoints: true,
+ // Supports back and forward navigation
+ navigation: true,
+ },
+ };
+
+ // We may try to access window while the document is closing, then accessing window
+ // throws.
+ if (!this.docShell.isBeingDestroyed()) {
+ response.title = this.title;
+ response.url = this.url;
+ response.outerWindowID = this.outerWindowID;
+ }
+
+ const actors = this._createExtraActors();
+ Object.assign(response, actors);
+
+ // The thread actor is the only actor manually created by the target actor.
+ // It is not registered in targetScopedActorFactories and therefore needs
+ // to be added here manually.
+ if (this.threadActor) {
+ Object.assign(response, {
+ threadActor: this.threadActor.actorID,
+ });
+ }
+
+ return response;
+ }
+
+ /**
+ * Called when the actor is removed from the connection.
+ *
+ * @params {Object} options
+ * @params {Boolean} options.isTargetSwitching: Set to true when this is called during
+ * a target switch.
+ * @params {Boolean} options.isModeSwitching: Set to true true when this is called as the
+ * result of a change to the devtools.browsertoolbox.scope pref.
+ */
+ destroy({ isTargetSwitching = false, isModeSwitching = false } = {}) {
+ // Avoid reentrancy. We will destroy the Transport when emitting "destroyed",
+ // which will force destroying all actors.
+ if (this.destroying) {
+ return;
+ }
+ this.destroying = true;
+
+ // Tell the thread actor that the window global is closed, so that it may terminate
+ // instead of resuming the debuggee script.
+ // TODO: Bug 997119: Remove this coupling with thread actor
+ if (this.threadActor) {
+ this.threadActor._parentClosed = true;
+ }
+
+ if (this._touchSimulator) {
+ this._touchSimulator.stop();
+ this._touchSimulator = null;
+ }
+
+ // Check for `docShell` availability, as it can be already gone during
+ // Firefox shutdown.
+ if (this.docShell) {
+ this._unwatchDocShell(this.docShell);
+
+ // If this target is being destroyed as part of a target switch or a mode switch,
+ // we don't need to restore the configuration (this might cause the content page to
+ // be focused again, causing issues in tests and disturbing the user when switching modes).
+ if (!isTargetSwitching && !isModeSwitching) {
+ this._restoreTargetConfiguration();
+ }
+ }
+ this._unwatchDocshells();
+
+ this._destroyThreadActor();
+
+ if (this._styleSheetsManager) {
+ this._styleSheetsManager.destroy();
+ this._styleSheetsManager = null;
+ }
+
+ // Shut down actors that belong to this target's pool.
+ if (this._targetScopedActorPool) {
+ this._targetScopedActorPool.destroy();
+ this._targetScopedActorPool = null;
+ }
+
+ // Make sure that no more workerListChanged notifications are sent.
+ if (this._workerDescriptorActorList !== null) {
+ this._workerDescriptorActorList.destroy();
+ this._workerDescriptorActorList = null;
+ }
+
+ if (this._workerDescriptorActorPool !== null) {
+ this._workerDescriptorActorPool.destroy();
+ this._workerDescriptorActorPool = null;
+ }
+
+ if (this._dbg) {
+ this._dbg.disable();
+ this._dbg = null;
+ }
+
+ // Emit a last event before calling Actor.destroy
+ // which will destroy the EventEmitter API
+ this.emit("destroyed", { isTargetSwitching, isModeSwitching });
+
+ // Destroy BaseTargetActor before nullifying docShell in case any child actor queries the window/docShell.
+ super.destroy();
+
+ this.docShell = null;
+ this._extraActors = null;
+
+ Services.obs.removeObserver(
+ this._onConsoleApiProfilerEvent,
+ "console-api-profiler"
+ );
+
+ TargetActorRegistry.unregisterTargetActor(this);
+ Resources.unwatchAllResources(this);
+ }
+
+ /**
+ * Return true if the given global is associated with this window global and should
+ * be added as a debuggee, false otherwise.
+ */
+ _shouldAddNewGlobalAsDebuggee(wrappedGlobal) {
+ // Otherwise, check if it is a WebExtension content script sandbox
+ const global = unwrapDebuggerObjectGlobal(wrappedGlobal);
+ if (!global) {
+ return false;
+ }
+
+ // Check if the global is a sdk page-mod sandbox.
+ let metadata = {};
+ let id = "";
+ try {
+ id = getInnerId(this.window);
+ metadata = Cu.getSandboxMetadata(global);
+ } catch (e) {
+ // ignore
+ }
+ if (metadata?.["inner-window-id"] && metadata["inner-window-id"] == id) {
+ return true;
+ }
+
+ return false;
+ }
+
+ _watchDocshells() {
+ // If for some unexpected reason, the actor is immediately destroyed,
+ // avoid registering leaking observer listener.
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ // In child processes, we watch all docshells living in the process.
+ Services.obs.addObserver(this, "webnavigation-create");
+ Services.obs.addObserver(this, "webnavigation-destroy");
+ this._docShellsObserved = true;
+
+ // We watch for all child docshells under the current document,
+ this._progressListener.watch(this.docShell);
+
+ // And list all already existing ones.
+ this._updateChildDocShells();
+ }
+
+ _unwatchDocshells() {
+ if (this._progressListener) {
+ this._progressListener.destroy();
+ this._progressListener = null;
+ this._originalWindow = null;
+ }
+
+ // Removes the observers being set in _watchDocshells, but only
+ // if _watchDocshells has been called. The target actor may be immediately destroyed
+ // and doesn't have time to register them.
+ // (Calling removeObserver without having called addObserver throws)
+ if (this._docShellsObserved) {
+ Services.obs.removeObserver(this, "webnavigation-create");
+ Services.obs.removeObserver(this, "webnavigation-destroy");
+ this._docShellsObserved = false;
+ }
+ }
+
+ _unwatchDocShell(docShell) {
+ if (this._progressListener) {
+ this._progressListener.unwatch(docShell);
+ }
+ }
+
+ switchToFrame(request) {
+ const windowId = request.windowId;
+ let win;
+
+ try {
+ win = Services.wm.getOuterWindowWithId(windowId);
+ } catch (e) {
+ // ignore
+ }
+ if (!win) {
+ throw {
+ error: "noWindow",
+ message: "The related docshell is destroyed or not found",
+ };
+ } else if (win == this.window) {
+ return {};
+ }
+
+ // Reply first before changing the document
+ DevToolsUtils.executeSoon(() => this._changeTopLevelDocument(win));
+
+ return {};
+ }
+
+ listFrames(request) {
+ const windows = this._docShellsToWindows(this.docShells);
+ return { frames: windows };
+ }
+
+ ensureWorkerDescriptorActorList() {
+ if (this._workerDescriptorActorList === null) {
+ this._workerDescriptorActorList = new WorkerDescriptorActorList(
+ this.conn,
+ {
+ type: Ci.nsIWorkerDebugger.TYPE_DEDICATED,
+ window: this.window,
+ }
+ );
+ }
+ return this._workerDescriptorActorList;
+ }
+
+ pauseWorkersUntilAttach(shouldPause) {
+ this.ensureWorkerDescriptorActorList().workerPauser.setPauseMatching(
+ shouldPause
+ );
+ }
+
+ listWorkers(request) {
+ return this.ensureWorkerDescriptorActorList()
+ .getList()
+ .then(actors => {
+ const pool = new Pool(this.conn, "worker-targets");
+ for (const actor of actors) {
+ pool.manage(actor);
+ }
+
+ // Do not destroy the pool before transfering ownership to the newly created
+ // pool, so that we do not accidently destroy actors that are still in use.
+ if (this._workerDescriptorActorPool) {
+ this._workerDescriptorActorPool.destroy();
+ }
+
+ this._workerDescriptorActorPool = pool;
+ this._workerDescriptorActorList.onListChanged =
+ this._onWorkerDescriptorActorListChanged;
+
+ return {
+ workers: actors,
+ };
+ });
+ }
+
+ logInPage(request) {
+ const { text, category, flags } = request;
+ const scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
+ const scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
+ scriptError.initWithWindowID(
+ text,
+ null,
+ null,
+ 0,
+ 0,
+ flags,
+ category,
+ getInnerId(this.window)
+ );
+ Services.console.logMessage(scriptError);
+ return {};
+ }
+
+ _onWorkerDescriptorActorListChanged() {
+ this._workerDescriptorActorList.onListChanged = null;
+ this.emit("workerListChanged");
+ }
+
+ _onConsoleApiProfilerEvent(subject, topic, data) {
+ // TODO: We will receive console-api-profiler events for any browser running
+ // in the same process as this target. We should filter irrelevant events,
+ // but console-api-profiler currently doesn't emit any information to identify
+ // the origin of the event. See Bug 1731033.
+
+ // The new performance panel is not compatible with console.profile().
+ const warningFlag = 1;
+ this.logInPage({
+ text:
+ "console.profile is not compatible with the new Performance recorder. " +
+ "See https://bugzilla.mozilla.org/show_bug.cgi?id=1730896",
+ category: "console.profile unavailable",
+ flags: warningFlag,
+ });
+ }
+
+ observe(subject, topic, data) {
+ // Ignore any event that comes before/after the actor is attached.
+ // That typically happens during Firefox shutdown.
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ subject.QueryInterface(Ci.nsIDocShell);
+
+ if (topic == "webnavigation-create") {
+ this._onDocShellCreated(subject);
+ } else if (topic == "webnavigation-destroy") {
+ this._onDocShellDestroy(subject);
+ }
+ }
+
+ _onDocShellCreated(docShell) {
+ // (chrome-)webnavigation-create is fired very early during docshell
+ // construction. In new root docshells within child processes, involving
+ // BrowserChild, this event is from within this call:
+ // https://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l912
+ // whereas the chromeEventHandler (and most likely other stuff) is set
+ // later:
+ // https://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l944
+ // So wait a tick before watching it:
+ DevToolsUtils.executeSoon(() => {
+ // Bug 1142752: sometimes, the docshell appears to be immediately
+ // destroyed, bailout early to prevent random exceptions.
+ if (docShell.isBeingDestroyed()) {
+ return;
+ }
+
+ // In child processes, we have new root docshells,
+ // let's watch them and all their child docshells.
+ if (this._isRootDocShell(docShell) && this.watchNewDocShells) {
+ this._progressListener.watch(docShell);
+ }
+ this._notifyDocShellsUpdate([docShell]);
+ });
+ }
+
+ _onDocShellDestroy(docShell) {
+ // Stop watching this docshell (the unwatch() method will check if we
+ // started watching it before).
+ this._unwatchDocShell(docShell);
+
+ const webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ this._notifyDocShellDestroy(webProgress);
+
+ if (webProgress.DOMWindow == this._originalWindow) {
+ // If the original top level document we connected to is removed,
+ // we try to switch to any other top level document
+ const rootDocShells = this.docShells.filter(d => {
+ // Ignore docshells without a working DOM Window.
+ // When we close firefox we have a chrome://extensions/content/dummy.xhtml
+ // which is in process of being destroyed and we might try to fallback to it.
+ // Unfortunately docshell.isBeingDestroyed() doesn't return true...
+ return d != this.docShell && this._isRootDocShell(d) && d.DOMWindow;
+ });
+ if (rootDocShells.length) {
+ const newRoot = rootDocShells[0];
+ this._originalWindow = newRoot.DOMWindow;
+ this._changeTopLevelDocument(this._originalWindow);
+ } else {
+ // If for some reason (typically during Firefox shutdown), the original
+ // document is destroyed, and there is no other top level docshell,
+ // we detach the actor to unregister all listeners and prevent any
+ // exception.
+ this.destroy();
+ }
+ return;
+ }
+
+ // If the currently targeted window global is destroyed, and we aren't on
+ // the top-level document, we have to switch to the top-level one.
+ if (
+ webProgress.DOMWindow == this.window &&
+ this.window != this._originalWindow
+ ) {
+ this._changeTopLevelDocument(this._originalWindow);
+ }
+ }
+
+ _isRootDocShell(docShell) {
+ // Should report as root docshell:
+ // - New top level window's docshells, when using ParentProcessTargetActor against a
+ // process. It allows tracking iframes of the newly opened windows
+ // like Browser console or new browser windows.
+ // - MozActivities or window.open frames on B2G, where a new root docshell
+ // is spawn in the child process of the app.
+ return !docShell.parent;
+ }
+
+ _docShellToWindow(docShell) {
+ const webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ const window = webProgress.DOMWindow;
+ const id = docShell.outerWindowID;
+ let parentID = undefined;
+ // Ignore the parent of the original document on non-e10s firefox,
+ // as we get the xul window as parent and don't care about it.
+ // Furthermore, ignore setting parentID when parent window is same as
+ // current window in order to deal with front end. e.g. toolbox will be fall
+ // into infinite loop due to recursive search with by using parent id.
+ if (
+ window.parent &&
+ window.parent != window &&
+ window != this._originalWindow
+ ) {
+ parentID = window.parent.docShell.outerWindowID;
+ }
+
+ return {
+ id,
+ parentID,
+ isTopLevel: window == this.originalWindow && this.isTopLevelTarget,
+ url: window.location.href,
+ title: window.document.title,
+ };
+ }
+
+ // Convert docShell list to windows objects list being sent to the client
+ _docShellsToWindows(docshells) {
+ return docshells
+ .filter(docShell => {
+ // Ensure docShell.document is available.
+ docShell.QueryInterface(Ci.nsIWebNavigation);
+
+ // don't include transient about:blank documents
+ if (docShell.document.isInitialDocument) {
+ return false;
+ }
+
+ return true;
+ })
+ .map(docShell => this._docShellToWindow(docShell));
+ }
+
+ _notifyDocShellsUpdate(docshells) {
+ // Only top level target uses frameUpdate in order to update the iframe dropdown.
+ // This may eventually be replaced by Target listening and target switching.
+ if (!this.isTopLevelTarget) {
+ return;
+ }
+
+ const windows = this._docShellsToWindows(docshells);
+
+ // Do not send the `frameUpdate` event if the windows array is empty.
+ if (!windows.length) {
+ return;
+ }
+
+ this.emit("frameUpdate", {
+ frames: windows,
+ });
+ }
+
+ _updateChildDocShells() {
+ this._notifyDocShellsUpdate(this.docShells);
+ }
+
+ _notifyDocShellDestroy(webProgress) {
+ // Only top level target uses frameUpdate in order to update the iframe dropdown.
+ // This may eventually be replaced by Target listening and target switching.
+ if (!this.isTopLevelTarget) {
+ return;
+ }
+
+ webProgress = webProgress.QueryInterface(Ci.nsIWebProgress);
+ const id = webProgress.DOMWindow.docShell.outerWindowID;
+ this.emit("frameUpdate", {
+ frames: [
+ {
+ id,
+ destroy: true,
+ },
+ ],
+ });
+ }
+
+ /**
+ * Creates and manages the thread actor as part of the Browsing Context Target pool.
+ * This sets up the content window for being debugged
+ */
+ _createThreadActor() {
+ this.threadActor = new ThreadActor(this, this.window);
+ this.manage(this.threadActor);
+ }
+
+ /**
+ * Exits the current thread actor and removes it from the Browsing Context Target pool.
+ * The content window is no longer being debugged after this call.
+ */
+ _destroyThreadActor() {
+ if (this.threadActor) {
+ this.threadActor.destroy();
+ this.threadActor = null;
+ }
+
+ if (this._sourcesManager) {
+ this._sourcesManager.destroy();
+ this._sourcesManager = null;
+ }
+ }
+
+ // Protocol Request Handlers
+
+ detach(request) {
+ // Destroy the actor in the next event loop in order
+ // to ensure responding to the `detach` request.
+ DevToolsUtils.executeSoon(() => {
+ this.destroy();
+ });
+
+ return {};
+ }
+
+ /**
+ * Bring the window global's window to front.
+ */
+ focus() {
+ if (this.window) {
+ this.window.focus();
+ }
+ return {};
+ }
+
+ goForward() {
+ // Wait a tick so that the response packet can be dispatched before the
+ // subsequent navigation event packet.
+ Services.tm.dispatchToMainThread(
+ DevToolsUtils.makeInfallible(() => {
+ // This won't work while the browser is shutting down and we don't really
+ // care.
+ if (Services.startup.shuttingDown) {
+ return;
+ }
+
+ this.webNavigation.goForward();
+ }, "WindowGlobalTargetActor.prototype.goForward's delayed body")
+ );
+
+ return {};
+ }
+
+ goBack() {
+ // Wait a tick so that the response packet can be dispatched before the
+ // subsequent navigation event packet.
+ Services.tm.dispatchToMainThread(
+ DevToolsUtils.makeInfallible(() => {
+ // This won't work while the browser is shutting down and we don't really
+ // care.
+ if (Services.startup.shuttingDown) {
+ return;
+ }
+
+ this.webNavigation.goBack();
+ }, "WindowGlobalTargetActor.prototype.goBack's delayed body")
+ );
+
+ return {};
+ }
+
+ /**
+ * Reload the page in this window global.
+ *
+ * @backward-compat { legacy }
+ * reload is preserved for third party tools. See Bug 1717837.
+ * DevTools should use Descriptor::reloadDescriptor instead.
+ */
+ reload(request) {
+ const force = request?.options?.force;
+ // Wait a tick so that the response packet can be dispatched before the
+ // subsequent navigation event packet.
+ Services.tm.dispatchToMainThread(
+ DevToolsUtils.makeInfallible(() => {
+ // This won't work while the browser is shutting down and we don't really
+ // care.
+ if (Services.startup.shuttingDown) {
+ return;
+ }
+
+ this.webNavigation.reload(
+ force
+ ? Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE
+ : Ci.nsIWebNavigation.LOAD_FLAGS_NONE
+ );
+ }, "WindowGlobalTargetActor.prototype.reload's delayed body")
+ );
+ return {};
+ }
+
+ /**
+ * Navigate this window global to a new location
+ */
+ navigateTo(request) {
+ // Wait a tick so that the response packet can be dispatched before the
+ // subsequent navigation event packet.
+ Services.tm.dispatchToMainThread(
+ DevToolsUtils.makeInfallible(() => {
+ this.window.location = request.url;
+ }, "WindowGlobalTargetActor.prototype.navigateTo's delayed body:" + request.url)
+ );
+ return {};
+ }
+
+ /**
+ * For browsing-context targets which can't use the watcher configuration
+ * actor (eg webextension targets), the client directly calls `reconfigure`.
+ * Once all targets support the watcher, this method can be removed.
+ */
+ reconfigure(request) {
+ const options = request.options || {};
+ return this.updateTargetConfiguration(options);
+ }
+
+ /**
+ * Apply target-specific options.
+ *
+ * This will be called by the watcher when the DevTools target-configuration
+ * is updated, or when a target is created via JSWindowActors.
+ */
+ updateTargetConfiguration(options = {}, calledFromDocumentCreation = false) {
+ if (!this.docShell) {
+ // The window global is already closed.
+ return;
+ }
+
+ let reload = false;
+ if (typeof options.touchEventsOverride !== "undefined") {
+ const enableTouchSimulator = options.touchEventsOverride === "enabled";
+
+ this.docShell.metaViewportOverride = enableTouchSimulator
+ ? Ci.nsIDocShell.META_VIEWPORT_OVERRIDE_ENABLED
+ : Ci.nsIDocShell.META_VIEWPORT_OVERRIDE_NONE;
+
+ // We want to reload the document if it's an "existing" top level target on which
+ // the touch simulator will be toggled and the user has turned the
+ // "reload on touch simulation" setting on.
+ if (
+ enableTouchSimulator !== this.touchSimulator.enabled &&
+ options.reloadOnTouchSimulationToggle === true &&
+ this.isTopLevelTarget &&
+ !calledFromDocumentCreation
+ ) {
+ reload = true;
+ }
+
+ if (enableTouchSimulator) {
+ this.touchSimulator.start();
+ } else {
+ this.touchSimulator.stop();
+ }
+ }
+
+ if (typeof options.customFormatters !== "undefined") {
+ this.customFormatters = options.customFormatters;
+ }
+
+ if (typeof options.useSimpleHighlightersForReducedMotion == "boolean") {
+ this._useSimpleHighlightersForReducedMotion =
+ options.useSimpleHighlightersForReducedMotion;
+ this.emit("use-simple-highlighters-updated");
+ }
+
+ if (!this.isTopLevelTarget) {
+ // Following DevTools target options should only apply to the top target and be
+ // propagated through the window global tree via the platform.
+ return;
+ }
+ if (typeof options.restoreFocus == "boolean") {
+ this._restoreFocus = options.restoreFocus;
+ }
+ if (typeof options.recordAllocations == "object") {
+ const actor = this._memoryActor;
+ if (options.recordAllocations == null) {
+ actor.stopRecordingAllocations();
+ } else {
+ actor.attach();
+ actor.startRecordingAllocations(options.recordAllocations);
+ }
+ }
+
+ if (reload) {
+ this.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
+ }
+ }
+
+ get touchSimulator() {
+ if (!this._touchSimulator) {
+ this._touchSimulator = new TouchSimulator(this.chromeEventHandler);
+ }
+
+ return this._touchSimulator;
+ }
+
+ /**
+ * Opposite of the updateTargetConfiguration method, that resets document
+ * state when closing the toolbox.
+ */
+ _restoreTargetConfiguration() {
+ if (this._restoreFocus && this.browsingContext?.isActive) {
+ this.window.focus();
+ }
+ }
+
+ _changeTopLevelDocument(window) {
+ // In case of WebExtension, still using one WindowGlobalTarget instance for many document,
+ // when reloading the add-on we might not destroy the previous target and wait for the next
+ // one to come and destroy it.
+ if (this.window) {
+ // Fake a will-navigate on the previous document
+ // to let a chance to unregister it
+ this._willNavigate({
+ window: this.window,
+ newURI: window.location.href,
+ request: null,
+ isFrameSwitching: true,
+ navigationStart: Date.now(),
+ });
+
+ this._windowDestroyed(this.window, {
+ isFrozen: true,
+ isFrameSwitching: true,
+ });
+ }
+
+ // Immediately change the window as this window, if in process of unload
+ // may already be non working on the next cycle and start throwing
+ this._setWindow(window);
+
+ DevToolsUtils.executeSoon(() => {
+ // No need to do anything more if the actor is destroyed.
+ // e.g. the client has been closed and the actors destroyed in the meantime.
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ // Then fake window-ready and navigate on the given document
+ this._windowReady(window, { isFrameSwitching: true });
+ DevToolsUtils.executeSoon(() => {
+ this._navigate(window, true);
+ });
+ });
+ }
+
+ _setWindow(window) {
+ // Here is the very important call where we switch the currently targeted
+ // window global (it will indirectly update this.window and many other
+ // attributes defined from docShell).
+ this.docShell = window.docShell;
+ this.emit("changed-toplevel-document");
+ this.emit("frameUpdate", {
+ selected: this.outerWindowID,
+ });
+ }
+
+ /**
+ * Handle location changes, by clearing the previous debuggees and enabling
+ * debugging, which may have been disabled temporarily by the
+ * DebuggerProgressListener.
+ */
+ _windowReady(window, { isFrameSwitching, isBFCache } = {}) {
+ if (this.ignoreSubFrames) {
+ return;
+ }
+ const isTopLevel = window == this.window;
+
+ // We just reset iframe list on WillNavigate, so we now list all existing
+ // frames when we load a new document in the original window
+ if (window == this._originalWindow && !isFrameSwitching) {
+ this._updateChildDocShells();
+ }
+
+ // If this follows WindowGlobal lifecycle, a new Target actor will be spawn for the top level
+ // target document. Only notify about in-process iframes.
+ // Note that OOP iframes won't emit window-ready and will also have their dedicated target.
+ // Also, we allow window-ready to be fired for iframe switching of top level documents,
+ // otherwise the iframe dropdown no longer works with server side targets.
+ if (this.followWindowGlobalLifeCycle && isTopLevel && !isFrameSwitching) {
+ return;
+ }
+
+ this.emit("window-ready", {
+ window,
+ isTopLevel,
+ isBFCache,
+ id: getWindowID(window),
+ isFrameSwitching,
+ });
+ }
+
+ _windowDestroyed(
+ window,
+ { id = null, isFrozen = false, isFrameSwitching = false }
+ ) {
+ if (this.ignoreSubFrames) {
+ return;
+ }
+ const isTopLevel = window == this.window;
+
+ // If this follows WindowGlobal lifecycle, this target will be destroyed, alongside its top level document.
+ // Only notify about in-process iframes.
+ // Note that OOP iframes won't emit window-ready and will also have their dedicated target.
+ // Also, we allow window-destroyed to be fired for iframe switching of top level documents,
+ // otherwise the iframe dropdown no longer works with server side targets.
+ if (this.followWindowGlobalLifeCycle && isTopLevel && !isFrameSwitching) {
+ return;
+ }
+
+ this.emit("window-destroyed", {
+ window,
+ isTopLevel,
+ id: id || getWindowID(window),
+ isFrozen,
+ });
+ }
+
+ /**
+ * Start notifying server and client about a new document being loaded in the
+ * currently targeted window global.
+ */
+ _willNavigate({
+ window,
+ newURI,
+ request,
+ isFrameSwitching = false,
+ navigationStart,
+ }) {
+ if (this.ignoreSubFrames) {
+ return;
+ }
+ let isTopLevel = window == this.window;
+
+ let reset = false;
+ if (window == this._originalWindow && !isFrameSwitching) {
+ // If the top level document changes and we are targeting an iframe, we
+ // need to reset to the upcoming new top level document. But for this
+ // will-navigate event, we will dispatch on the old window. (The inspector
+ // codebase expect to receive will-navigate for the currently displayed
+ // document in order to cleanup the markup view)
+ if (this.window != this._originalWindow) {
+ reset = true;
+ window = this.window;
+ isTopLevel = true;
+ }
+ }
+
+ // will-navigate event needs to be dispatched synchronously, by calling the
+ // listeners in the order or registration. This event fires once navigation
+ // starts, (all pending user prompts are dealt with), but before the first
+ // request starts.
+ this.emit("will-navigate", {
+ window,
+ isTopLevel,
+ newURI,
+ request,
+ navigationStart,
+ isFrameSwitching,
+ });
+
+ // We don't do anything for inner frames here.
+ // (we will only update thread actor on window-ready)
+ if (!isTopLevel) {
+ return;
+ }
+
+ // When the actor acts as a WindowGlobalTarget, will-navigate won't fired.
+ // Instead we will receive a new top level target with isTargetSwitching=true.
+ if (!this.followWindowGlobalLifeCycle) {
+ this.emit("tabNavigated", {
+ url: newURI,
+ state: "start",
+ isFrameSwitching,
+ });
+ }
+
+ if (reset) {
+ this._setWindow(this._originalWindow);
+ }
+ }
+
+ /**
+ * Notify server and client about a new document done loading in the current
+ * targeted window global.
+ */
+ _navigate(window, isFrameSwitching = false) {
+ if (this.ignoreSubFrames) {
+ return;
+ }
+ const isTopLevel = window == this.window;
+
+ // navigate event needs to be dispatched synchronously,
+ // by calling the listeners in the order or registration.
+ // This event is fired once the document is loaded,
+ // after the load event, it's document ready-state is 'complete'.
+ this.emit("navigate", {
+ window,
+ isTopLevel,
+ });
+
+ // We don't do anything for inner frames here.
+ // (we will only update thread actor on window-ready)
+ if (!isTopLevel) {
+ return;
+ }
+
+ // We may still significate when the document is done loading, via navigate.
+ // But as we no longer fire the "will-navigate", may be it is better to find
+ // other ways to get to our means.
+ // Listening to "navigate" is misleading as the document may already be loaded
+ // if we just opened the DevTools. So it is better to use "watch" pattern
+ // and instead have the actor either emit immediately resources as they are
+ // already available, or later on as the load progresses.
+ if (this.followWindowGlobalLifeCycle) {
+ return;
+ }
+
+ this.emit("tabNavigated", {
+ url: this.url,
+ title: this.title,
+ state: "stop",
+ isFrameSwitching,
+ });
+ }
+
+ removeActorByName(name) {
+ if (name in this._extraActors) {
+ const actor = this._extraActors[name];
+ if (this._targetScopedActorPool.has(actor)) {
+ this._targetScopedActorPool.removeActor(actor);
+ }
+ delete this._extraActors[name];
+ }
+ }
+}
+
+exports.WindowGlobalTargetActor = WindowGlobalTargetActor;
+
+class DebuggerProgressListener {
+ /**
+ * The DebuggerProgressListener class is an nsIWebProgressListener which
+ * handles onStateChange events for the targeted window global. If the user
+ * tries to navigate away from a paused page, the listener makes sure that the
+ * debuggee is resumed before the navigation begins.
+ *
+ * @param WindowGlobalTargetActor targetActor
+ * The window global target actor associated with this listener.
+ */
+ constructor(targetActor) {
+ this._targetActor = targetActor;
+ this._onWindowCreated = this.onWindowCreated.bind(this);
+ this._onWindowHidden = this.onWindowHidden.bind(this);
+
+ // Watch for windows destroyed (global observer that will need filtering)
+ Services.obs.addObserver(this, "inner-window-destroyed");
+
+ // XXX: for now we maintain the list of windows we know about in this instance
+ // so that we can discriminate windows we care about when observing
+ // inner-window-destroyed events. Bug 1016952 would remove the need for this.
+ this._knownWindowIDs = new Map();
+
+ this._watchedDocShells = new WeakSet();
+ }
+
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]);
+
+ destroy() {
+ Services.obs.removeObserver(this, "inner-window-destroyed");
+ this._knownWindowIDs.clear();
+ this._knownWindowIDs = null;
+ }
+
+ watch(docShell) {
+ // Add the docshell to the watched set. We're actually adding the window,
+ // because docShell objects are not wrappercached and would be rejected
+ // by the WeakSet.
+ const docShellWindow = docShell.domWindow;
+ this._watchedDocShells.add(docShellWindow);
+
+ const webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(
+ this,
+ Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
+ );
+
+ const handler = getDocShellChromeEventHandler(docShell);
+ handler.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
+ handler.addEventListener("pageshow", this._onWindowCreated, true);
+ handler.addEventListener("pagehide", this._onWindowHidden, true);
+
+ // Dispatch the _windowReady event on the targetActor for pre-existing windows
+ const windows = this._targetActor.ignoreSubFrames
+ ? [docShellWindow]
+ : this._getWindowsInDocShell(docShell);
+ for (const win of windows) {
+ this._targetActor._windowReady(win);
+ this._knownWindowIDs.set(getWindowID(win), win);
+ }
+
+ // The `watchedByDevTools` enables gecko behavior tied to this flag, such as:
+ // - reporting the contents of HTML loaded in the docshells,
+ // - or capturing stacks for the network monitor.
+ //
+ // This flag is also set in frame-helper but in the case of the browser toolbox, we
+ // don't have the watcher enabled by default yet, and as a result we need to set it
+ // here for the parent process window global.
+ // This should be removed as part of Bug 1709529.
+ if (this._targetActor.typeName === "parentProcessTarget") {
+ docShell.browsingContext.watchedByDevTools = true;
+ }
+ // Immediately enable CSS error reports on new top level docshells, if this was already enabled.
+ // This is specific to MBT and WebExtension targets (so the isRootActor check).
+ if (
+ this._targetActor.isRootActor &&
+ this._targetActor.docShell.cssErrorReportingEnabled
+ ) {
+ docShell.cssErrorReportingEnabled = true;
+ }
+ }
+
+ unwatch(docShell) {
+ const docShellWindow = docShell.domWindow;
+ if (!this._watchedDocShells.has(docShellWindow)) {
+ return;
+ }
+ this._watchedDocShells.delete(docShellWindow);
+
+ const webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ // During process shutdown, the docshell may already be cleaned up and throw
+ try {
+ webProgress.removeProgressListener(this);
+ } catch (e) {
+ // ignore
+ }
+
+ const handler = getDocShellChromeEventHandler(docShell);
+ handler.removeEventListener(
+ "DOMWindowCreated",
+ this._onWindowCreated,
+ true
+ );
+ handler.removeEventListener("pageshow", this._onWindowCreated, true);
+ handler.removeEventListener("pagehide", this._onWindowHidden, true);
+
+ const windows = this._targetActor.ignoreSubFrames
+ ? [docShellWindow]
+ : this._getWindowsInDocShell(docShell);
+ for (const win of windows) {
+ this._knownWindowIDs.delete(getWindowID(win));
+ }
+
+ // We only reset it for parent process target actor as the flag should be set in parent
+ // process, and thus is set elsewhere for other type of BrowsingContextActor.
+ if (this._targetActor.typeName === "parentProcessTarget") {
+ docShell.browsingContext.watchedByDevTools = false;
+ }
+ }
+
+ _getWindowsInDocShell(docShell) {
+ return getChildDocShells(docShell).map(d => {
+ return d.domWindow;
+ });
+ }
+
+ onWindowCreated = DevToolsUtils.makeInfallible(function (evt) {
+ if (this._targetActor.isDestroyed()) {
+ return;
+ }
+
+ // If we're in a frame swap (which occurs when toggling RDM, for example), then we can
+ // ignore this event, as the window never really went anywhere for our purposes.
+ if (evt.inFrameSwap) {
+ return;
+ }
+
+ const window = evt.target.defaultView;
+ if (!window) {
+ // Some old UIs might emit unrelated events called pageshow/pagehide on
+ // elements which are not documents. Bail in this case. See Bug 1669666.
+ return;
+ }
+
+ const innerID = getWindowID(window);
+
+ // This handler is called for two events: "DOMWindowCreated" and "pageshow".
+ // Bail out if we already processed this window.
+ if (this._knownWindowIDs.has(innerID)) {
+ return;
+ }
+ this._knownWindowIDs.set(innerID, window);
+
+ // For a regular page navigation, "DOMWindowCreated" is fired before
+ // "pageshow". If the current event is "pageshow" but we have not processed
+ // the window yet, it means this is a BF cache navigation. In theory,
+ // `event.persisted` should be set for BF cache navigation events, but it is
+ // not always available, so we fallback on checking if "pageshow" is the
+ // first event received for a given window (see Bug 1378133).
+ const isBFCache = evt.type == "pageshow";
+
+ this._targetActor._windowReady(window, { isBFCache });
+ }, "DebuggerProgressListener.prototype.onWindowCreated");
+
+ onWindowHidden = DevToolsUtils.makeInfallible(function (evt) {
+ if (this._targetActor.isDestroyed()) {
+ return;
+ }
+
+ // If we're in a frame swap (which occurs when toggling RDM, for example), then we can
+ // ignore this event, as the window isn't really going anywhere for our purposes.
+ if (evt.inFrameSwap) {
+ return;
+ }
+
+ // Only act as if the window has been destroyed if the 'pagehide' event
+ // was sent for a persisted window (persisted is set when the page is put
+ // and frozen in the bfcache). If the page isn't persisted, the observer's
+ // inner-window-destroyed event will handle it.
+ if (!evt.persisted) {
+ return;
+ }
+
+ const window = evt.target.defaultView;
+ if (!window) {
+ // Some old UIs might emit unrelated events called pageshow/pagehide on
+ // elements which are not documents. Bail in this case. See Bug 1669666.
+ return;
+ }
+
+ this._targetActor._windowDestroyed(window, { isFrozen: true });
+ this._knownWindowIDs.delete(getWindowID(window));
+ }, "DebuggerProgressListener.prototype.onWindowHidden");
+
+ observe = DevToolsUtils.makeInfallible(function (subject, topic) {
+ if (this._targetActor.isDestroyed()) {
+ return;
+ }
+
+ // Because this observer will be called for all inner-window-destroyed in
+ // the application, we need to filter out events for windows we are not
+ // watching
+ const innerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ const window = this._knownWindowIDs.get(innerID);
+ if (window) {
+ this._knownWindowIDs.delete(innerID);
+ this._targetActor._windowDestroyed(window, { id: innerID });
+ }
+
+ // Bug 1598364: when debugging browser.xhtml from the Browser Toolbox
+ // the DOMWindowCreated/pageshow/pagehide event listeners have to be
+ // re-registered against the next document when we reload browser.html
+ // (or navigate to another doc).
+ // That's because we registered the listener on docShell.domWindow as
+ // top level windows don't have a chromeEventHandler.
+ if (
+ this._watchedDocShells.has(window) &&
+ !window.docShell.chromeEventHandler
+ ) {
+ // First cleanup all the existing listeners
+ this.unwatch(window.docShell);
+ // Re-register new ones. The docShell is already referencing the new document.
+ this.watch(window.docShell);
+ }
+ }, "DebuggerProgressListener.prototype.observe");
+
+ onStateChange = DevToolsUtils.makeInfallible(function (
+ progress,
+ request,
+ flag,
+ status
+ ) {
+ if (this._targetActor.isDestroyed()) {
+ return;
+ }
+ progress.QueryInterface(Ci.nsIDocShell);
+ if (progress.isBeingDestroyed()) {
+ return;
+ }
+
+ const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
+ const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
+ const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+ const isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
+
+ // Ideally, we would fetch navigationStart from window.performance.timing.navigationStart
+ // but as WindowGlobal isn't instantiated yet we don't have access to it.
+ // This is ultimately handed over to DocumentEventListener, which uses this.
+ // See its comment about WILL_NAVIGATE_TIME_SHIFT for more details about the related workaround.
+ const navigationStart = Date.now();
+
+ // Catch any iframe location change
+ if (isDocument && isStop) {
+ // Watch document stop to ensure having the new iframe url.
+ this._targetActor._notifyDocShellsUpdate([progress]);
+ }
+
+ const window = progress.DOMWindow;
+ if (isDocument && isStart) {
+ // One of the earliest events that tells us a new URI
+ // is being loaded in this window.
+ const newURI = request instanceof Ci.nsIChannel ? request.URI.spec : null;
+ this._targetActor._willNavigate({
+ window,
+ newURI,
+ request,
+ isFrameSwitching: false,
+ navigationStart,
+ });
+ }
+ if (isWindow && isStop) {
+ // Don't dispatch "navigate" event just yet when there is a redirect to
+ // about:neterror page.
+ // Navigating to about:neterror will make `status` be something else than NS_OK.
+ // But for some error like NS_BINDING_ABORTED we don't want to emit any `navigate`
+ // event as the page load has been cancelled and the related page document is going
+ // to be a dead wrapper.
+ if (
+ request.status != Cr.NS_OK &&
+ request.status != Cr.NS_BINDING_ABORTED
+ ) {
+ // Instead, listen for DOMContentLoaded as about:neterror is loaded
+ // with LOAD_BACKGROUND flags and never dispatches load event.
+ // That may be the same reason why there is no onStateChange event
+ // for about:neterror loads.
+ const handler = getDocShellChromeEventHandler(progress);
+ const onLoad = evt => {
+ // Ignore events from iframes
+ if (evt.target === window.document) {
+ handler.removeEventListener("DOMContentLoaded", onLoad, true);
+ this._targetActor._navigate(window);
+ }
+ };
+ handler.addEventListener("DOMContentLoaded", onLoad, true);
+ } else {
+ // Somewhat equivalent of load event.
+ // (window.document.readyState == complete)
+ this._targetActor._navigate(window);
+ }
+ }
+ },
+ "DebuggerProgressListener.prototype.onStateChange");
+}
diff --git a/devtools/server/actors/targets/worker.js b/devtools/server/actors/targets/worker.js
new file mode 100644
index 0000000000..fb523b43c5
--- /dev/null
+++ b/devtools/server/actors/targets/worker.js
@@ -0,0 +1,134 @@
+/* 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 {
+ workerTargetSpec,
+} = require("resource://devtools/shared/specs/targets/worker.js");
+
+const {
+ WebConsoleActor,
+} = require("resource://devtools/server/actors/webconsole.js");
+const { ThreadActor } = require("resource://devtools/server/actors/thread.js");
+const { TracerActor } = require("resource://devtools/server/actors/tracer.js");
+
+const Targets = require("resource://devtools/server/actors/targets/index.js");
+
+const makeDebuggerUtil = require("resource://devtools/server/actors/utils/make-debugger.js");
+const {
+ SourcesManager,
+} = require("resource://devtools/server/actors/utils/sources-manager.js");
+
+const {
+ BaseTargetActor,
+} = require("resource://devtools/server/actors/targets/base-target-actor.js");
+
+class WorkerTargetActor extends BaseTargetActor {
+ /**
+ * Target actor for a worker in the content process.
+ *
+ * @param {DevToolsServerConnection} conn: The connection to the client.
+ * @param {WorkerGlobalScope} workerGlobal: The worker global.
+ * @param {Object} workerDebuggerData: The worker debugger information
+ * @param {String} workerDebuggerData.id: The worker debugger id
+ * @param {String} workerDebuggerData.url: The worker debugger url
+ * @param {String} workerDebuggerData.type: The worker debugger type
+ * @param {Boolean} workerDebuggerData.workerConsoleApiMessagesDispatchedToMainThread:
+ * Value of the dom.worker.console.dispatch_events_to_main_thread pref
+ * @param {Object} sessionContext: The Session Context to help know what is debugged.
+ * See devtools/server/actors/watcher/session-context.js
+ */
+ constructor(conn, workerGlobal, workerDebuggerData, sessionContext) {
+ super(conn, Targets.TYPES.WORKER, workerTargetSpec);
+
+ // workerGlobal is needed by the console actor for evaluations.
+ this.workerGlobal = workerGlobal;
+ this.sessionContext = sessionContext;
+
+ this._workerDebuggerData = workerDebuggerData;
+ this._sourcesManager = null;
+ this.workerConsoleApiMessagesDispatchedToMainThread =
+ workerDebuggerData.workerConsoleApiMessagesDispatchedToMainThread;
+
+ this.makeDebugger = makeDebuggerUtil.bind(null, {
+ findDebuggees: () => {
+ return [workerGlobal];
+ },
+ shouldAddNewGlobalAsDebuggee: () => true,
+ });
+
+ // needed by the console actor
+ this.threadActor = new ThreadActor(this, this.workerGlobal);
+
+ // needed by the thread actor to communicate with the console when evaluating logpoints.
+ this._consoleActor = new WebConsoleActor(this.conn, this);
+
+ this.tracerActor = new TracerActor(this.conn, this);
+
+ this.manage(this.threadActor);
+ this.manage(this._consoleActor);
+ this.manage(this.tracerActor);
+ }
+
+ // Expose the worker URL to the thread actor.
+ // so that it can easily know what is the base URL of all worker scripts.
+ get workerUrl() {
+ return this._workerDebuggerData.url;
+ }
+
+ form() {
+ return {
+ actor: this.actorID,
+
+ consoleActor: this._consoleActor?.actorID,
+ threadActor: this.threadActor?.actorID,
+ tracerActor: this.tracerActor?.actorID,
+
+ id: this._workerDebuggerData.id,
+ type: this._workerDebuggerData.type,
+ url: this._workerDebuggerData.url,
+ traits: {
+ // See trait description in browsing-context.js
+ supportsTopLevelTargetFlag: false,
+ },
+ };
+ }
+
+ get dbg() {
+ if (!this._dbg) {
+ this._dbg = this.makeDebugger();
+ }
+ return this._dbg;
+ }
+
+ get sourcesManager() {
+ if (this._sourcesManager === null) {
+ this._sourcesManager = new SourcesManager(this.threadActor);
+ }
+
+ return this._sourcesManager;
+ }
+
+ // This is called from the ThreadActor#onAttach method
+ onThreadAttached() {
+ // This isn't an RDP event and is only listened to from startup/worker.js.
+ this.emit("worker-thread-attached");
+ }
+
+ destroy() {
+ super.destroy();
+
+ if (this._sourcesManager) {
+ this._sourcesManager.destroy();
+ this._sourcesManager = null;
+ }
+
+ this.workerGlobal = null;
+ this._dbg = null;
+ this._consoleActor = null;
+ this.threadActor = null;
+ }
+}
+exports.WorkerTargetActor = WorkerTargetActor;