From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../server/actors/targets/base-target-actor.js | 114 ++ devtools/server/actors/targets/content-process.js | 264 +++ devtools/server/actors/targets/index.js | 12 + devtools/server/actors/targets/moz.build | 20 + devtools/server/actors/targets/parent-process.js | 166 ++ .../targets/session-data-processors/blackboxing.js | 19 + .../targets/session-data-processors/breakpoints.js | 37 + .../session-data-processors/event-breakpoints.js | 26 + .../targets/session-data-processors/index.js | 50 + .../targets/session-data-processors/moz.build | 16 + .../targets/session-data-processors/resources.js | 17 + .../target-configuration.js | 23 + .../thread-configuration.js | 32 + .../session-data-processors/xhr-breakpoints.js | 34 + .../actors/targets/target-actor-registry.sys.mjs | 82 + devtools/server/actors/targets/webextension.js | 371 ++++ devtools/server/actors/targets/window-global.js | 1932 ++++++++++++++++++++ devtools/server/actors/targets/worker.js | 134 ++ 18 files changed, 3349 insertions(+) create mode 100644 devtools/server/actors/targets/base-target-actor.js create mode 100644 devtools/server/actors/targets/content-process.js create mode 100644 devtools/server/actors/targets/index.js create mode 100644 devtools/server/actors/targets/moz.build create mode 100644 devtools/server/actors/targets/parent-process.js create mode 100644 devtools/server/actors/targets/session-data-processors/blackboxing.js create mode 100644 devtools/server/actors/targets/session-data-processors/breakpoints.js create mode 100644 devtools/server/actors/targets/session-data-processors/event-breakpoints.js create mode 100644 devtools/server/actors/targets/session-data-processors/index.js create mode 100644 devtools/server/actors/targets/session-data-processors/moz.build create mode 100644 devtools/server/actors/targets/session-data-processors/resources.js create mode 100644 devtools/server/actors/targets/session-data-processors/target-configuration.js create mode 100644 devtools/server/actors/targets/session-data-processors/thread-configuration.js create mode 100644 devtools/server/actors/targets/session-data-processors/xhr-breakpoints.js create mode 100644 devtools/server/actors/targets/target-actor-registry.sys.mjs create mode 100644 devtools/server/actors/targets/webextension.js create mode 100644 devtools/server/actors/targets/window-global.js create mode 100644 devtools/server/actors/targets/worker.js (limited to 'devtools/server/actors/targets') 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 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 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} 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} + */ + 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