summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/targets/window-global.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/targets/window-global.js')
-rw-r--r--devtools/server/actors/targets/window-global.js1935
1 files changed, 1935 insertions, 0 deletions
diff --git a/devtools/server/actors/targets/window-global.js b/devtools/server/actors/targets/window-global.js
new file mode 100644
index 0000000000..5d2bb10164
--- /dev/null
+++ b/devtools/server/actors/targets/window-global.js
@@ -0,0 +1,1935 @@
+/* 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;
+ }
+
+ // Also update configurations which applies to all target types
+ super.updateTargetConfiguration(options, calledFromDocumentCreation);
+
+ 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");
+}