tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

window-global.js (69266B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 // protocol.js uses objects as exceptions in order to define
      8 // error packets.
      9 /* eslint-disable no-throw-literal */
     10 
     11 /*
     12 * WindowGlobalTargetActor is an abstract class used by target actors that hold
     13 * documents, such as frames, chrome windows, etc.
     14 *
     15 * This class is extended by ParentProcessTargetActor.
     16 *
     17 * See devtools/docs/contributor/backend/actor-hierarchy.md for more details about all the targets.
     18 *
     19 * For performance matters, this file should only be loaded in the targeted context's
     20 * process. For example, it shouldn't be evaluated in the parent process until we try to
     21 * debug a document living in the parent process.
     22 */
     23 
     24 var {
     25  ActorRegistry,
     26 } = require("resource://devtools/server/actors/utils/actor-registry.js");
     27 var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
     28 var { assert } = DevToolsUtils;
     29 var {
     30  SourcesManager,
     31 } = require("resource://devtools/server/actors/utils/sources-manager.js");
     32 var makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
     33 const Targets = require("resource://devtools/server/actors/targets/index.js");
     34 
     35 const lazy = {};
     36 // Modules loaded in the same module loader as this module.
     37 // (may spawn distinct module instances with other devtools and firefox frontend)
     38 ChromeUtils.defineESModuleGetters(
     39  lazy,
     40  {
     41    PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     42    WEBEXTENSION_FALLBACK_DOC_URL:
     43      "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
     44    getAddonIdForWindowGlobal:
     45      "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
     46  },
     47  { global: "contextual" }
     48 );
     49 
     50 // Modules loaded from the shared module loader.
     51 // Won't be as debuggable from the Browser Toolbox and may trigger breakpoints.
     52 ChromeUtils.defineESModuleGetters(
     53  lazy,
     54  {
     55    TargetActorRegistry:
     56      "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
     57  },
     58  { global: "shared" }
     59 );
     60 
     61 const { Pool } = require("resource://devtools/shared/protocol.js");
     62 const {
     63  LazyPool,
     64  createExtraActors,
     65 } = require("resource://devtools/shared/protocol/lazy-pool.js");
     66 const {
     67  windowGlobalTargetSpec,
     68 } = require("resource://devtools/shared/specs/targets/window-global.js");
     69 const Resources = require("resource://devtools/server/actors/resources/index.js");
     70 const {
     71  BaseTargetActor,
     72 } = require("resource://devtools/server/actors/targets/base-target-actor.js");
     73 
     74 loader.lazyRequireGetter(
     75  this,
     76  ["ThreadActor"],
     77  "resource://devtools/server/actors/thread.js",
     78  true
     79 );
     80 loader.lazyRequireGetter(
     81  this,
     82  "WorkerDescriptorActorList",
     83  "resource://devtools/server/actors/worker/worker-descriptor-actor-list.js",
     84  true
     85 );
     86 loader.lazyRequireGetter(
     87  this,
     88  "StyleSheetsManager",
     89  "resource://devtools/server/actors/utils/stylesheets-manager.js",
     90  true
     91 );
     92 loader.lazyRequireGetter(
     93  this,
     94  "TouchSimulator",
     95  "resource://devtools/server/actors/emulation/touch-simulator.js",
     96  true
     97 );
     98 
     99 function getWindowID(window) {
    100  return window.windowGlobalChild.innerWindowId;
    101 }
    102 
    103 function getDocShellChromeEventHandler(docShell) {
    104  let handler = docShell.chromeEventHandler;
    105  if (!handler) {
    106    try {
    107      // Toplevel xul window's docshell doesn't have chromeEventHandler
    108      // attribute. The chrome event handler is just the global window object.
    109      handler = docShell.domWindow;
    110    } catch (e) {
    111      // ignore
    112    }
    113  }
    114  return handler;
    115 }
    116 
    117 /**
    118 * Helper to retrieve all children docshells of a given docshell.
    119 *
    120 * Given that docshell interfaces can only be used within the same process,
    121 * this only returns docshells for children documents that runs in the same process
    122 * as the given docshell.
    123 */
    124 function getChildDocShells(parentDocShell) {
    125  return parentDocShell.browsingContext
    126    .getAllBrowsingContextsInSubtree()
    127    .filter(browsingContext => {
    128      // Filter out browsingContext which don't expose any docshell (e.g. remote frame)
    129      return browsingContext.docShell;
    130    })
    131    .map(browsingContext => {
    132      // Map BrowsingContext to DocShell
    133      return browsingContext.docShell;
    134    });
    135 }
    136 
    137 exports.getChildDocShells = getChildDocShells;
    138 
    139 /**
    140 * Helper to retrieve all the windows (parent, children, or browsing context own window)
    141 * that exists in the same process than the given browsing context.
    142 *
    143 * @param {BrowsingContext} browsingContext
    144 * @returns {Array<Window>}
    145 */
    146 function getAllSameProcessGlobalsFromBrowsingContext(browsingContext) {
    147  const windows = [];
    148  const topBrowsingContext = browsingContext.top;
    149  for (const bc of topBrowsingContext.getAllBrowsingContextsInSubtree()) {
    150    // Filter out browsingContext which don't expose any docshell (e.g. remote frame)
    151    if (!bc.docShell) {
    152      continue;
    153    }
    154    try {
    155      windows.push(bc.docShell.domWindow);
    156    } catch (e) {
    157      // docShell.domWindow may throw when the docshell is being destroyed.
    158      // Ignore them. We can't use docShell.isBeingDestroyed as it
    159      // is flagging too early. e.g it's already true when hitting a breakpoint
    160      // in the unload event.
    161    }
    162  }
    163 
    164  return windows;
    165 }
    166 
    167 /**
    168 * Browser-specific actors.
    169 */
    170 
    171 function getInnerId(window) {
    172  return window.windowGlobalChild.innerWindowId;
    173 }
    174 
    175 class WindowGlobalTargetActor extends BaseTargetActor {
    176  /**
    177   * WindowGlobalTargetActor is the target actor to debug (HTML) documents.
    178   *
    179   * WindowGlobal's are the Gecko representation for a given document's window object.
    180   * It relates to a given nsGlobalWindowInner instance.
    181   *
    182   * The main goal of this class is to expose the target-scoped actors being registered
    183   * via `ActorRegistry.registerModule` and manage their lifetimes. In addition, this
    184   * class also tracks the lifetime of the targeted window global.
    185   *
    186   * ### Main requests:
    187   *
    188   * `detach`:
    189   *  Stop document watching and cleanup everything that the target and its children actors created.
    190   *  It ultimately lead to destroy the target actor.
    191   * `switchToFrame`:
    192   *  Change the targeted document of the whole actor, and its child target-scoped actors
    193   *  to an iframe or back to its original document.
    194   *
    195   * Most properties (like `chromeEventHandler` or `docShells`) are meant to be
    196   * used by the various child target actors.
    197   *
    198   * ### RDP events:
    199   *
    200   *  - `tabNavigated`:
    201   *    Sent when the window global is about to navigate or has just navigated
    202   *    to a different document.
    203   *    This event contains the following attributes:
    204   *     * url (string)
    205   *       The new URI being loaded.
    206   *     * state (string)
    207   *       `start` if we just start requesting the new URL
    208   *       `stop`  if the new URL is done loading
    209   *     * isFrameSwitching (boolean)
    210   *       Indicates the event is dispatched when switching the actor context to a
    211   *       different frame. When we switch to an iframe, there is no document
    212   *       load. The targeted document is most likely going to be already done
    213   *       loading.
    214   *     * title (string)
    215   *       The document title being loaded. (sent only on state=stop)
    216   *
    217   *  - `frameUpdate`:
    218   *    Sent when there was a change in the child frames contained in the document
    219   *    or when the actor's context was switched to another frame.
    220   *    This event can have four different forms depending on the type of change:
    221   *    * One or many frames are updated:
    222   *      { frames: [{ id, url, title, parentID }, ...] }
    223   *    * One frame got destroyed:
    224   *      { frames: [{ id, destroy: true }]}
    225   *    * All frames got destroyed:
    226   *      { destroyAll: true }
    227   *    * We switched the context of the actor to a specific frame:
    228   *      { selected: #id }
    229   *
    230   * ### Internal, non-rdp events:
    231   *
    232   * Various events are also dispatched on the actor itself without being sent to
    233   * the client. They all relate to the documents tracked by this target actor
    234   * (its main targeted document, but also any of its iframes):
    235   *  - will-navigate
    236   *    This event fires once navigation starts. All pending user prompts are
    237   *    dealt with, but it is fired before the first request starts.
    238   *  - navigate
    239   *    This event is fired once the document's readyState is "complete".
    240   *  - window-ready
    241   *    This event is fired in various distinct scenarios:
    242   *     * When a new Window object is crafted, equivalent of `DOMWindowCreated`.
    243   *       It is dispatched before any page script is executed.
    244   *     * We will have already received a window-ready event for this window
    245   *       when it was created, but we received a window-destroyed event when
    246   *       it was frozen into the bfcache, and now the user navigated back to
    247   *       this page, so it's now live again and we should resume handling it.
    248   *     * For each existing document, when an `attach` request is received.
    249   *       At this point scripts in the page will be already loaded.
    250   *     * When `swapFrameLoaders` is used, such as with moving window globals
    251   *       between windows or toggling Responsive Design Mode.
    252   *  - window-destroyed
    253   *    This event is fired in two cases:
    254   *     * When the window object is destroyed, i.e. when the related document
    255   *       is garbage collected. This can happen when the window global is
    256   *       closed or the iframe is removed from the DOM.
    257   *       It is equivalent of `inner-window-destroyed` event.
    258   *     * When the page goes into the bfcache and gets frozen.
    259   *       The equivalent of `pagehide`.
    260   *  - changed-toplevel-document
    261   *    This event fires when we switch the actor's targeted document
    262   *    to one of its iframes, or back to its original top document.
    263   *    It is dispatched between window-destroyed and window-ready.
    264   *
    265   * Note that *all* these events are dispatched in the following order
    266   * when we switch the context of the actor to a given iframe:
    267   *  - will-navigate
    268   *  - window-destroyed
    269   *  - changed-toplevel-document
    270   *  - window-ready
    271   *  - navigate
    272   *
    273   * This class is subclassed by ParentProcessTargetActor and others.
    274   * Subclasses are expected to implement a getter for the docShell property.
    275   *
    276   * @param conn DevToolsServerConnection
    277   *        The conection to the client.
    278   * @param options Object
    279   *        Object with following attributes:
    280   *        - docShell nsIDocShell
    281   *          The |docShell| for the debugged frame.
    282   *        - followWindowGlobalLifeCycle Boolean
    283   *          If true, the target actor will only inspect the current WindowGlobal (and its children windows).
    284   *          But won't inspect next document loaded in the same BrowsingContext.
    285   *          The actor will behave more like a WindowGlobalTarget rather than a BrowsingContextTarget.
    286   *          This is always true for Tab and web extension debugging, but not yet for parent process target
    287   *          used by the browser toolbox.
    288   *        - isTopLevelTarget Boolean
    289   *          Should be set to true for all top-level targets. A top level target
    290   *          is the topmost target of a DevTools "session". For instance for a local
    291   *          tab toolbox, the WindowGlobalTargetActor for the content page is the top level target.
    292   *          For the Multiprocess Browser Toolbox, the parent process target is the top level
    293   *          target.
    294   *          At the moment this only impacts the WindowGlobalTarget `reconfigure`
    295   *          implementation. But for server-side target switching this flag will be exposed
    296   *          to the client and should be available for all target actor classes. It will be
    297   *          used to detect target switching. (Bug 1644397)
    298   *        - ignoreSubFrames Boolean
    299   *          If true, the actor will only focus on the passed docShell and not on the whole
    300   *          docShell tree. This should be enabled when we have targets for all documents.
    301   *        - sessionContext Object
    302   *          The Session Context to help know what is debugged.
    303   *          See devtools/server/actors/watcher/session-context.js
    304   */
    305  constructor(
    306    conn,
    307    {
    308      docShell,
    309      followWindowGlobalLifeCycle,
    310      isTopLevelTarget,
    311      ignoreSubFrames,
    312      sessionContext,
    313      customSpec = windowGlobalTargetSpec,
    314    }
    315  ) {
    316    super(conn, Targets.TYPES.FRAME, customSpec);
    317 
    318    this.followWindowGlobalLifeCycle = followWindowGlobalLifeCycle;
    319    this.isTopLevelTarget = !!isTopLevelTarget;
    320    this.ignoreSubFrames = ignoreSubFrames;
    321    this.sessionContext = sessionContext;
    322 
    323    // A map of actor names to actor instances provided by extensions.
    324    this._extraActors = {};
    325    this._sourcesManager = null;
    326 
    327    this._shouldAddNewGlobalAsDebuggee =
    328      this._shouldAddNewGlobalAsDebuggee.bind(this);
    329 
    330    this.makeDebugger = makeDebugger.bind(null, {
    331      findDebuggees: (dbg, includeAllSameProcessGlobals) => {
    332        const result = [];
    333        const inspectUAWidgets = Services.prefs.getBoolPref(
    334          "devtools.inspector.showAllAnonymousContent",
    335          false
    336        );
    337        const windows = includeAllSameProcessGlobals
    338          ? getAllSameProcessGlobalsFromBrowsingContext(this.browsingContext)
    339          : this.windows;
    340        for (const win of windows) {
    341          result.push(win);
    342          // Only expose User Agent internal (like <video controls>) when the
    343          // related pref is set.
    344          if (inspectUAWidgets) {
    345            const principal = win.document.nodePrincipal;
    346            // We don't use UA widgets for the system principal.
    347            if (!principal.isSystemPrincipal) {
    348              result.push(Cu.getUAWidgetScope(principal));
    349            }
    350          }
    351        }
    352        return result;
    353      },
    354      shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee,
    355    });
    356 
    357    // Flag eventually overloaded by sub classes in order to watch new docshells
    358    // Used by the ParentProcessTargetActor to list all frames in the Browser Toolbox
    359    this.watchNewDocShells = false;
    360 
    361    this._workerDescriptorActorList = null;
    362    this._workerDescriptorActorPool = null;
    363    this._onWorkerDescriptorActorListChanged =
    364      this._onWorkerDescriptorActorListChanged.bind(this);
    365 
    366    this._onConsoleApiProfilerEvent =
    367      this._onConsoleApiProfilerEvent.bind(this);
    368    Services.obs.addObserver(
    369      this._onConsoleApiProfilerEvent,
    370      "console-api-profiler"
    371    );
    372 
    373    // Start observing navigations as well as sub documents.
    374    // (This is probably meant to disappear once EFT is the only supported codepath)
    375    this._progressListener = new DebuggerProgressListener(this);
    376 
    377    lazy.TargetActorRegistry.registerTargetActor(this);
    378 
    379    if (docShell) {
    380      this.setDocShell(docShell);
    381    }
    382  }
    383 
    384  // Attribute only set when debugging web extensions, in order to distinguish:
    385  // - "fallback-document" created by DevTools for the top level target
    386  // - "background" for the background page
    387  // - "popup" for any popup document
    388  #isFallbackExtensionDocument = false;
    389 
    390  /**
    391   * Define the initial docshell.
    392   *
    393   * This is called from the constructor for WindowGlobalTargetActor,
    394   * or from sub class constructor of ParentProcessTargetActor.
    395   *
    396   * This is to circumvent the fact that sub classes need to call inner method
    397   * to compute the initial docshell and we can't call inner methods before calling
    398   * the base class constructor...
    399   */
    400  setDocShell(docShell) {
    401    Object.defineProperty(this, "docShell", {
    402      value: docShell,
    403      configurable: true,
    404      writable: true,
    405    });
    406 
    407    // When this target tracks only one WindowGlobal, set a fixed innerWindowId and window,
    408    // so that it can easily be read safely while the related WindowGlobal is being destroyed.
    409    if (this.followWindowGlobalLifeCycle) {
    410      Object.defineProperty(this, "innerWindowId", {
    411        value: this.innerWindowId,
    412        configurable: false,
    413        writable: false,
    414      });
    415      Object.defineProperty(this, "window", {
    416        value: this.window,
    417        configurable: false,
    418        writable: false,
    419      });
    420      Object.defineProperty(this, "chromeEventHandler", {
    421        value: this.chromeEventHandler,
    422        configurable: false,
    423        writable: false,
    424      });
    425    }
    426 
    427    // Save references to the original document we attached to
    428    this._originalWindow = this.window;
    429 
    430    // Update isPrivate as window is based on docShell
    431    this.isPrivate = lazy.PrivateBrowsingUtils.isContentWindowPrivate(
    432      this.window
    433    );
    434 
    435    // Instantiate the Thread Actor immediately.
    436    // This is the only one actor instantiated right away by the target actor.
    437    // All the others are instantiated lazily on first request made the client,
    438    // via LazyPool API.
    439    this._createThreadActor();
    440 
    441    // Ensure notifying about the target actor first
    442    // before notifying about new docshells.
    443    // Otherwise we would miss these RDP event as the client hasn't
    444    // yet received the target actor's form.
    445    // (This is also probably meant to disappear once EFT is the only supported codepath)
    446    this._docShellsObserved = false;
    447    DevToolsUtils.executeSoon(() => this._watchDocshells());
    448 
    449    // The `watchedByDevTools` enables gecko behavior tied to this flag, such as:
    450    //  - reporting the contents of HTML loaded in the docshells,
    451    //  - or capturing stacks for the network monitor.
    452    //
    453    // This flag can only be set on top level BrowsingContexts.
    454    if (!this.browsingContext.parent) {
    455      this.browsingContext.watchedByDevTools = true;
    456    }
    457 
    458    if (this.sessionContext.type == "webextension") {
    459      if (
    460        this.window.location.href.startsWith(lazy.WEBEXTENSION_FALLBACK_DOC_URL)
    461      ) {
    462        this.#isFallbackExtensionDocument = true;
    463      }
    464    }
    465  }
    466 
    467  get docShell() {
    468    throw new Error(
    469      "A docShell should be provided as constructor argument of WindowGlobalTargetActor, or redefined by the subclass"
    470    );
    471  }
    472 
    473  /*
    474   * Return a Debugger instance or create one if there is none yet
    475   */
    476  get dbg() {
    477    if (!this._dbg) {
    478      this._dbg = this.makeDebugger();
    479    }
    480    return this._dbg;
    481  }
    482 
    483  /**
    484   * Try to locate the console actor if it exists.
    485   */
    486  get _consoleActor() {
    487    if (this.isDestroyed()) {
    488      return null;
    489    }
    490    const form = this.form();
    491    return this.conn._getOrCreateActor(form.consoleActor);
    492  }
    493 
    494  get _memoryActor() {
    495    if (this.isDestroyed()) {
    496      return null;
    497    }
    498    const form = this.form();
    499    return this.conn._getOrCreateActor(form.memoryActor);
    500  }
    501 
    502  _targetScopedActorPool = null;
    503 
    504  /**
    505   * A EventTarget object on which to listen for 'DOMWindowCreated' and 'pageshow' events.
    506   */
    507  get chromeEventHandler() {
    508    return getDocShellChromeEventHandler(this.docShell);
    509  }
    510 
    511  /**
    512   * Getter for the list of all `docShell`s in the window global.
    513   *
    514   * @return {Array}
    515   */
    516  get docShells() {
    517    if (this.ignoreSubFrames) {
    518      return [this.docShell];
    519    }
    520 
    521    return getChildDocShells(this.docShell);
    522  }
    523 
    524  /**
    525   * Getter for the window global's current DOM window.
    526   */
    527  get window() {
    528    try {
    529      return this.docShell?.domWindow;
    530    } catch (e) {
    531      // When querying `domWindow` on document's unload `docShell.isBeingDestroyed()` will return true,
    532      // whereas `domWindow` is still functional... and useful to return!
    533      return null;
    534    }
    535  }
    536 
    537  get targetGlobal() {
    538    return this.window;
    539  }
    540 
    541  get outerWindowID() {
    542    if (this.docShell) {
    543      return this.docShell.outerWindowID;
    544    }
    545    return null;
    546  }
    547 
    548  get browsingContext() {
    549    return this.docShell?.browsingContext;
    550  }
    551 
    552  get browsingContextID() {
    553    return this.browsingContext?.id;
    554  }
    555 
    556  get innerWindowId() {
    557    return this.window?.windowGlobalChild.innerWindowId;
    558  }
    559 
    560  get browserId() {
    561    return this.browsingContext?.browserId;
    562  }
    563 
    564  get openerBrowserId() {
    565    return this.browsingContext?.opener?.browserId;
    566  }
    567 
    568  /**
    569   * Getter for the list of all content DOM windows in the window global.
    570   *
    571   * @return {Array}
    572   */
    573  get windows() {
    574    const windows = [];
    575    for (const docShell of this.docShells) {
    576      try {
    577        windows.push(docShell.domWindow);
    578      } catch (e) {
    579        // docShell.domWindow may throw when the docshell is being destroyed.
    580        // Ignore them. We can't use docShell.isBeingDestroyed as it
    581        // is flagging too early. e.g it's already true when hitting a breakpoint
    582        // in the unload event.
    583      }
    584    }
    585    return windows;
    586  }
    587 
    588  /**
    589   * Getter for the original docShell this actor got attached to in the first
    590   * place.
    591   * Note that your actor should normally *not* rely on this top level docShell
    592   * if you want it to show information relative to the iframe that's currently
    593   * being inspected in the toolbox.
    594   */
    595  get originalDocShell() {
    596    if (!this._originalWindow || Cu.isDeadWrapper(this._originalWindow)) {
    597      return this.docShell;
    598    }
    599 
    600    return this._originalWindow.docShell;
    601  }
    602 
    603  /**
    604   * Getter for the original window this actor got attached to in the first
    605   * place.
    606   * Note that your actor should normally *not* rely on this top level window if
    607   * you want it to show information relative to the iframe that's currently
    608   * being inspected in the toolbox.
    609   */
    610  get originalWindow() {
    611    return this._originalWindow || this.window;
    612  }
    613 
    614  /**
    615   * Getter for the nsIWebProgress for watching this window.
    616   */
    617  get webProgress() {
    618    return this.docShell
    619      .QueryInterface(Ci.nsIInterfaceRequestor)
    620      .getInterface(Ci.nsIWebProgress);
    621  }
    622 
    623  /**
    624   * Getter for the nsIWebNavigation for the target.
    625   */
    626  get webNavigation() {
    627    return this.docShell.QueryInterface(Ci.nsIWebNavigation);
    628  }
    629 
    630  /**
    631   * Getter for the window global's document.
    632   */
    633  get contentDocument() {
    634    return this.webNavigation.document;
    635  }
    636 
    637  /**
    638   * Getter for the window global's title.
    639   */
    640  get title() {
    641    return this.contentDocument.title;
    642  }
    643 
    644  /**
    645   * Getter for the window global's URL.
    646   */
    647  get url() {
    648    if (this.webNavigation.currentURI) {
    649      return this.webNavigation.currentURI.spec;
    650    }
    651    // Abrupt closing of the browser window may leave callbacks without a
    652    // currentURI.
    653    return null;
    654  }
    655 
    656  get sourcesManager() {
    657    if (!this._sourcesManager) {
    658      this._sourcesManager = new SourcesManager(this.threadActor);
    659    }
    660    return this._sourcesManager;
    661  }
    662 
    663  getStyleSheetsManager() {
    664    if (!this._styleSheetsManager) {
    665      this._styleSheetsManager = new StyleSheetsManager(this);
    666    }
    667    return this._styleSheetsManager;
    668  }
    669 
    670  _createExtraActors() {
    671    // Always use the same Pool, so existing actor instances
    672    // (created in createExtraActors) are not lost.
    673    if (!this._targetScopedActorPool) {
    674      this._targetScopedActorPool = new LazyPool(this.conn);
    675    }
    676 
    677    // Walk over target-scoped actor factories and make sure they are all
    678    // instantiated and added into the Pool.
    679    return createExtraActors(
    680      ActorRegistry.targetScopedActorFactories,
    681      this._targetScopedActorPool,
    682      this
    683    );
    684  }
    685 
    686  form() {
    687    assert(
    688      !this.isDestroyed(),
    689      "form() shouldn't be called on destroyed browser actor."
    690    );
    691    assert(this.actorID, "Actor should have an actorID.");
    692 
    693    // If the actor or the document is already being destroyed, return a very minimal form,
    694    // enough to identify the target actor from the client side and attributes used by the
    695    // server code to process the target destruction.
    696    // Otherwise, many of the form attributes can't be retrieved and would throw exceptions.
    697    // Also, `_createExtraActors` may start re-creating actor as they were already cleared by destroy()...
    698    if (this.destroying || !this.originalDocShell) {
    699      return {
    700        actor: this.actorID,
    701        innerWindowId: this.innerWindowId,
    702        isTopLevelTarget: this.isTopLevelTarget,
    703      };
    704    }
    705 
    706    // Note that we don't want the iframe dropdown to change our BrowsingContext.id/innerWindowId
    707    // We only want to refer to the topmost original window we attached to
    708    // as that's the one top document this target actor really represent.
    709    // The iframe dropdown is just a hack that temporarily focus the scope
    710    // of the target actor to a children iframe document.
    711    const originalBrowsingContext = this.originalDocShell.browsingContext;
    712 
    713    // When toggling the toolbox on/off many times in a row,
    714    // we may try to destroy the actor while the related document is already destroyed.
    715    // In such scenario, return the minimum viable form
    716    if (!originalBrowsingContext.currentWindowContext) {
    717      return { actor: this.actorID };
    718    }
    719 
    720    const browsingContextID = originalBrowsingContext.id;
    721    const innerWindowId =
    722      originalBrowsingContext.currentWindowContext.innerWindowId;
    723    const parentInnerWindowId =
    724      originalBrowsingContext.parent?.currentWindowContext.innerWindowId;
    725    // Doesn't only check `!!opener` as some iframe might have an opener
    726    // if their location was loaded via `window.open(url, "iframe-name")`.
    727    // So also ensure that the document is opened in a distinct tab.
    728    const isPopup =
    729      !!originalBrowsingContext.opener &&
    730      originalBrowsingContext.browserId !=
    731        originalBrowsingContext.opener.browserId;
    732 
    733    const response = {
    734      actor: this.actorID,
    735      targetType: this.targetType,
    736 
    737      browsingContextID,
    738      processID: Services.appinfo.processID,
    739      // True for targets created by JSWindowActors, see constructor JSDoc.
    740      followWindowGlobalLifeCycle: this.followWindowGlobalLifeCycle,
    741      innerWindowId,
    742      parentInnerWindowId,
    743      topInnerWindowId: this.browsingContext.topWindowContext.innerWindowId,
    744      isTopLevelTarget: this.isTopLevelTarget,
    745      ignoreSubFrames: this.ignoreSubFrames,
    746      isPopup,
    747      isPrivate: this.isPrivate,
    748      title: this.title,
    749      url: this.url,
    750      outerWindowID: this.outerWindowID,
    751 
    752      // Specific to Web Extension documents
    753      isFallbackExtensionDocument: this.#isFallbackExtensionDocument,
    754      addonId: lazy.getAddonIdForWindowGlobal(this.window.windowGlobalChild),
    755 
    756      traits: {
    757        // @backward-compat { version 64 } Exposes a new trait to help identify
    758        // BrowsingContextActor's inherited actors from the client side.
    759        isBrowsingContext: true,
    760        // Browsing context targets can compute the isTopLevelTarget flag on the
    761        // server. But other target actors don't support this yet. See Bug 1709314.
    762        supportsTopLevelTargetFlag: true,
    763        // Supports frame listing via `listFrames` request and `frameUpdate` events
    764        // as well as frame switching via `switchToFrame` request
    765        frames: true,
    766        // Supports the logInPage request.
    767        logInPage: true,
    768        // Supports watchpoints in the server. We need to keep this trait because target
    769        // actors that don't extend WindowGlobalTargetActor (Worker, ContentProcess, …)
    770        // might not support watchpoints.
    771        watchpoints: true,
    772        // Supports back and forward navigation
    773        navigation: true,
    774      },
    775    };
    776 
    777    const actors = this._createExtraActors();
    778    Object.assign(response, actors);
    779 
    780    // The thread actor is the only actor manually created by the target actor.
    781    // It is not registered in targetScopedActorFactories and therefore needs
    782    // to be added here manually.
    783    if (this.threadActor) {
    784      Object.assign(response, {
    785        threadActor: this.threadActor.actorID,
    786      });
    787    }
    788 
    789    return response;
    790  }
    791 
    792  /**
    793   * Called when the actor is removed from the connection.
    794   *
    795   * @param {object} options
    796   * @param {boolean} options.isTargetSwitching: Set to true when this is called during
    797   *         a target switch.
    798   * @param {boolean} options.isModeSwitching: Set to true true when this is called as the
    799   *         result of a change to the devtools.browsertoolbox.scope pref.
    800   */
    801  destroy({ isTargetSwitching = false, isModeSwitching = false } = {}) {
    802    // Avoid reentrancy. We will destroy the Transport when emitting "destroyed",
    803    // which will force destroying all actors.
    804    if (this.destroying) {
    805      return;
    806    }
    807    this.destroying = true;
    808 
    809    // Force flushing pending resources if the actor isn't already destroyed.
    810    // This helps notify the client about pending resources on navigation.
    811    if (!this.isDestroyed()) {
    812      this.emitResources();
    813    }
    814 
    815    // Tell the thread actor that the window global is closed, so that it may terminate
    816    // instead of resuming the debuggee script.
    817    // TODO: Bug 997119: Remove this coupling with thread actor
    818    if (this.threadActor) {
    819      this.threadActor._parentClosed = true;
    820    }
    821 
    822    if (this._touchSimulator) {
    823      this._touchSimulator.stop();
    824      this._touchSimulator = null;
    825    }
    826 
    827    // The watchedByDevTools flag is only set on top level BrowsingContext
    828    // (as it then cascades to all its children),
    829    // and when destroying the target, we should tell the platform we no longer
    830    // observe this BrowsingContext and set this attribute to false.
    831    // Ignore this cleanup if the related BrowsingContext is being destroyed.
    832    if (
    833      this.browsingContext?.watchedByDevTools &&
    834      !this.browsingContext.parent &&
    835      !this.browsingContext.isDiscarded
    836    ) {
    837      this.browsingContext.watchedByDevTools = false;
    838    }
    839 
    840    // Check for `docShell` availability, as it can be already gone during
    841    // Firefox shutdown.
    842    if (this.docShell) {
    843      this._unwatchDocShell(this.docShell);
    844 
    845      // If this target is being destroyed as part of a target switch or a mode switch,
    846      // we don't need to restore the configuration (this might cause the content page to
    847      // be focused again, causing issues in tests and disturbing the user when switching modes).
    848      if (!isTargetSwitching && !isModeSwitching) {
    849        this._restoreTargetConfiguration();
    850      }
    851    }
    852    this._unwatchDocshells();
    853 
    854    this._destroyThreadActor();
    855 
    856    if (this._styleSheetsManager) {
    857      this._styleSheetsManager.destroy();
    858      this._styleSheetsManager = null;
    859    }
    860 
    861    // Shut down actors that belong to this target's pool.
    862    if (this._targetScopedActorPool) {
    863      this._targetScopedActorPool.destroy();
    864      this._targetScopedActorPool = null;
    865    }
    866 
    867    // Make sure that no more workerListChanged notifications are sent.
    868    if (this._workerDescriptorActorList !== null) {
    869      this._workerDescriptorActorList.destroy();
    870      this._workerDescriptorActorList = null;
    871    }
    872 
    873    if (this._workerDescriptorActorPool !== null) {
    874      this._workerDescriptorActorPool.destroy();
    875      this._workerDescriptorActorPool = null;
    876    }
    877 
    878    if (this._dbg) {
    879      this._dbg.disable();
    880      this._dbg = null;
    881    }
    882 
    883    // Emit a last event before calling Actor.destroy
    884    // which will destroy the EventEmitter API
    885    this.emit("destroyed", { isTargetSwitching, isModeSwitching });
    886 
    887    // Destroy BaseTargetActor before nullifying docShell in case any child actor queries the window/docShell.
    888    super.destroy();
    889 
    890    this.docShell = null;
    891    this._extraActors = null;
    892 
    893    Services.obs.removeObserver(
    894      this._onConsoleApiProfilerEvent,
    895      "console-api-profiler"
    896    );
    897 
    898    lazy.TargetActorRegistry.unregisterTargetActor(this);
    899    Resources.unwatchAllResources(this);
    900  }
    901 
    902  /**
    903   * This is only used by WebExtensionTargetActor, which overrides this method.
    904   */
    905  _shouldAddNewGlobalAsDebuggee() {
    906    return false;
    907  }
    908 
    909  _watchDocshells() {
    910    // If for some unexpected reason, the actor is immediately destroyed,
    911    // avoid registering leaking observer listener.
    912    if (this.isDestroyed()) {
    913      return;
    914    }
    915 
    916    // This method is called asynchronously and the document may have been destroyed in the meantime.
    917    // In such case, automatically destroy the target actor.
    918    if (this.docShell.isBeingDestroyed()) {
    919      this.destroy();
    920      return;
    921    }
    922 
    923    // In child processes, we watch all docshells living in the process.
    924    // Avoid watching for all docshell unless we are in non-EFT codepath,
    925    // which only happens for the ParentProcess's WindowGlobalTarget
    926    // used by the browser toolbox to reach all documents/docshells in the parent process.
    927    if (!this.ignoreSubFrames) {
    928      Services.obs.addObserver(this, "webnavigation-create");
    929      Services.obs.addObserver(this, "webnavigation-destroy");
    930      this._docShellsObserved = true;
    931    }
    932 
    933    // We watch for all child docshells under the current document,
    934    this._progressListener.watch(this.docShell);
    935 
    936    // And list all already existing ones.
    937    if (!this.ignoreSubFrames) {
    938      this._updateChildDocShells();
    939    }
    940  }
    941 
    942  _unwatchDocshells() {
    943    if (this._progressListener) {
    944      this._progressListener.destroy();
    945      this._progressListener = null;
    946      this._originalWindow = null;
    947    }
    948 
    949    // Removes the observers being set in _watchDocshells, but only
    950    // if _watchDocshells has been called. The target actor may be immediately destroyed
    951    // and doesn't have time to register them.
    952    // (Calling removeObserver without having called addObserver throws)
    953    if (this._docShellsObserved) {
    954      Services.obs.removeObserver(this, "webnavigation-create");
    955      Services.obs.removeObserver(this, "webnavigation-destroy");
    956      this._docShellsObserved = false;
    957    }
    958  }
    959 
    960  _unwatchDocShell(docShell) {
    961    if (this._progressListener) {
    962      this._progressListener.unwatch(docShell);
    963    }
    964  }
    965 
    966  switchToFrame(request) {
    967    const windowId = request.windowId;
    968    let win;
    969 
    970    try {
    971      win = Services.wm.getOuterWindowWithId(windowId);
    972    } catch (e) {
    973      // ignore
    974    }
    975    if (!win) {
    976      throw {
    977        error: "noWindow",
    978        message: "The related docshell is destroyed or not found",
    979      };
    980    } else if (win == this.window) {
    981      return {};
    982    }
    983 
    984    // Reply first before changing the document
    985    DevToolsUtils.executeSoon(() => this._changeTopLevelDocument(win));
    986 
    987    return {};
    988  }
    989 
    990  listFrames() {
    991    const windows = this._docShellsToWindows(this.docShells);
    992    return { frames: windows };
    993  }
    994 
    995  ensureWorkerDescriptorActorList() {
    996    if (this._workerDescriptorActorList === null) {
    997      this._workerDescriptorActorList = new WorkerDescriptorActorList(
    998        this.conn,
    999        {
   1000          type: Ci.nsIWorkerDebugger.TYPE_DEDICATED,
   1001          window: this.window,
   1002        }
   1003      );
   1004    }
   1005    return this._workerDescriptorActorList;
   1006  }
   1007 
   1008  pauseWorkersUntilAttach(shouldPause) {
   1009    this.ensureWorkerDescriptorActorList().workerPauser.setPauseMatching(
   1010      shouldPause
   1011    );
   1012  }
   1013 
   1014  listWorkers() {
   1015    return this.ensureWorkerDescriptorActorList()
   1016      .getList()
   1017      .then(actors => {
   1018        const pool = new Pool(this.conn, "worker-targets");
   1019        for (const actor of actors) {
   1020          pool.manage(actor);
   1021        }
   1022 
   1023        // Do not destroy the pool before transfering ownership to the newly created
   1024        // pool, so that we do not accidently destroy actors that are still in use.
   1025        if (this._workerDescriptorActorPool) {
   1026          this._workerDescriptorActorPool.destroy();
   1027        }
   1028 
   1029        this._workerDescriptorActorPool = pool;
   1030        this._workerDescriptorActorList.onListChanged =
   1031          this._onWorkerDescriptorActorListChanged;
   1032 
   1033        return {
   1034          workers: actors,
   1035        };
   1036      });
   1037  }
   1038 
   1039  logInPage(request) {
   1040    const { text, category, flags } = request;
   1041    const scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
   1042    const scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
   1043    scriptError.initWithWindowID(
   1044      text,
   1045      null,
   1046      0,
   1047      0,
   1048      flags,
   1049      category,
   1050      getInnerId(this.window)
   1051    );
   1052    Services.console.logMessage(scriptError);
   1053    return {};
   1054  }
   1055 
   1056  _onWorkerDescriptorActorListChanged() {
   1057    this._workerDescriptorActorList.onListChanged = null;
   1058    this.emit("workerListChanged");
   1059  }
   1060 
   1061  _onConsoleApiProfilerEvent() {
   1062    // TODO: We will receive console-api-profiler events for any browser running
   1063    // in the same process as this target. We should filter irrelevant events,
   1064    // but console-api-profiler currently doesn't emit any information to identify
   1065    // the origin of the event. See Bug 1731033.
   1066 
   1067    // The new performance panel is not compatible with console.profile().
   1068    const warningFlag = 1;
   1069    this.logInPage({
   1070      text:
   1071        "console.profile is not compatible with the new Performance recorder. " +
   1072        "See https://bugzilla.mozilla.org/show_bug.cgi?id=1730896",
   1073      category: "console.profile unavailable",
   1074      flags: warningFlag,
   1075    });
   1076  }
   1077 
   1078  observe(subject, topic) {
   1079    // Ignore any event that comes before/after the actor is attached.
   1080    // That typically happens during Firefox shutdown.
   1081    if (this.isDestroyed()) {
   1082      return;
   1083    }
   1084 
   1085    subject.QueryInterface(Ci.nsIDocShell);
   1086 
   1087    if (topic == "webnavigation-create") {
   1088      this._onDocShellCreated(subject);
   1089    } else if (topic == "webnavigation-destroy") {
   1090      this._onDocShellDestroy(subject);
   1091    }
   1092  }
   1093 
   1094  _onDocShellCreated(docShell) {
   1095    // (chrome-)webnavigation-create is fired very early during docshell
   1096    // construction. In new root docshells within child processes, involving
   1097    // BrowserChild, this event is from within this call:
   1098    //   https://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l912
   1099    // whereas the chromeEventHandler (and most likely other stuff) is set
   1100    // later:
   1101    //   https://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l944
   1102    // So wait a tick before watching it:
   1103    DevToolsUtils.executeSoon(() => {
   1104      // Bug 1142752: sometimes, the docshell appears to be immediately
   1105      // destroyed, bailout early to prevent random exceptions.
   1106      if (docShell.isBeingDestroyed()) {
   1107        return;
   1108      }
   1109 
   1110      // In child processes, we have new root docshells,
   1111      // let's watch them and all their child docshells.
   1112      if (this._isRootDocShell(docShell) && this.watchNewDocShells) {
   1113        this._progressListener.watch(docShell);
   1114      }
   1115      this._notifyDocShellsUpdate([docShell]);
   1116    });
   1117  }
   1118 
   1119  _onDocShellDestroy(docShell) {
   1120    // Stop watching this docshell (the unwatch() method will check if we
   1121    // started watching it before).
   1122    this._unwatchDocShell(docShell);
   1123 
   1124    const webProgress = docShell
   1125      .QueryInterface(Ci.nsIInterfaceRequestor)
   1126      .getInterface(Ci.nsIWebProgress);
   1127    this._notifyDocShellDestroy(webProgress);
   1128 
   1129    if (webProgress.DOMWindow == this._originalWindow) {
   1130      // If the original top level document we connected to is removed,
   1131      // we try to switch to any other top level document
   1132      const rootDocShells = this.docShells.filter(d => {
   1133        // Ignore docshells without a working DOM Window.
   1134        // When we close firefox we have a chrome://extensions/content/dummy.xhtml
   1135        // which is in process of being destroyed and we might try to fallback to it.
   1136        // Unfortunately docshell.isBeingDestroyed() doesn't return true...
   1137        return d != this.docShell && this._isRootDocShell(d) && d.DOMWindow;
   1138      });
   1139      if (rootDocShells.length) {
   1140        const newRoot = rootDocShells[0];
   1141        this._originalWindow = newRoot.DOMWindow;
   1142        this._changeTopLevelDocument(this._originalWindow);
   1143      } else {
   1144        // If for some reason (typically during Firefox shutdown), the original
   1145        // document is destroyed, and there is no other top level docshell,
   1146        // we detach the actor to unregister all listeners and prevent any
   1147        // exception.
   1148        this.destroy();
   1149      }
   1150      return;
   1151    }
   1152 
   1153    // If the currently targeted window global is destroyed, and we aren't on
   1154    // the top-level document, we have to switch to the top-level one.
   1155    if (
   1156      webProgress.DOMWindow == this.window &&
   1157      this.window != this._originalWindow
   1158    ) {
   1159      this._changeTopLevelDocument(this._originalWindow);
   1160    }
   1161  }
   1162 
   1163  _isRootDocShell(docShell) {
   1164    // Should report as root docshell:
   1165    //  - New top level window's docshells, when using ParentProcessTargetActor against a
   1166    // process. It allows tracking iframes of the newly opened windows
   1167    // like Browser console or new browser windows.
   1168    //  - MozActivities or window.open frames on B2G, where a new root docshell
   1169    // is spawn in the child process of the app.
   1170    return !docShell.parent;
   1171  }
   1172 
   1173  _docShellToWindow(docShell) {
   1174    const webProgress = docShell
   1175      .QueryInterface(Ci.nsIInterfaceRequestor)
   1176      .getInterface(Ci.nsIWebProgress);
   1177    const window = webProgress.DOMWindow;
   1178    const id = docShell.outerWindowID;
   1179    let parentID = undefined;
   1180    // Ignore the parent of the original document on non-e10s firefox,
   1181    // as we get the xul window as parent and don't care about it.
   1182    // Furthermore, ignore setting parentID when parent window is same as
   1183    // current window in order to deal with front end. e.g. toolbox will be fall
   1184    // into infinite loop due to recursive search with by using parent id.
   1185    if (
   1186      window.parent &&
   1187      window.parent != window &&
   1188      window != this._originalWindow
   1189    ) {
   1190      parentID = window.parent.docShell.outerWindowID;
   1191    }
   1192 
   1193    return {
   1194      id,
   1195      parentID,
   1196      isTopLevel: window == this.originalWindow && this.isTopLevelTarget,
   1197      url: window.location.href,
   1198      title: window.document.title,
   1199    };
   1200  }
   1201 
   1202  // Convert docShell list to windows objects list being sent to the client
   1203  _docShellsToWindows(docshells) {
   1204    return docshells
   1205      .filter(docShell => {
   1206        // Ensure docShell.document is available.
   1207        docShell.QueryInterface(Ci.nsIWebNavigation);
   1208 
   1209        // don't include transient about:blank documents
   1210        if (docShell.document.isUncommittedInitialDocument) {
   1211          return false;
   1212        }
   1213 
   1214        return true;
   1215      })
   1216      .map(docShell => this._docShellToWindow(docShell));
   1217  }
   1218 
   1219  _notifyDocShellsUpdate(docshells) {
   1220    // Only top level target uses frameUpdate in order to update the iframe dropdown.
   1221    // This may eventually be replaced by Target listening and target switching.
   1222    if (!this.isTopLevelTarget) {
   1223      return;
   1224    }
   1225 
   1226    const windows = this._docShellsToWindows(docshells);
   1227 
   1228    // Do not send the `frameUpdate` event if the windows array is empty.
   1229    if (!windows.length) {
   1230      return;
   1231    }
   1232 
   1233    this.emit("frameUpdate", {
   1234      frames: windows,
   1235    });
   1236  }
   1237 
   1238  _updateChildDocShells() {
   1239    this._notifyDocShellsUpdate(this.docShells);
   1240  }
   1241 
   1242  _notifyDocShellDestroy(webProgress) {
   1243    // Only top level target uses frameUpdate in order to update the iframe dropdown.
   1244    // This may eventually be replaced by Target listening and target switching.
   1245    if (!this.isTopLevelTarget) {
   1246      return;
   1247    }
   1248 
   1249    webProgress = webProgress.QueryInterface(Ci.nsIWebProgress);
   1250    const id = webProgress.DOMWindow.docShell.outerWindowID;
   1251    this.emit("frameUpdate", {
   1252      frames: [
   1253        {
   1254          id,
   1255          destroy: true,
   1256        },
   1257      ],
   1258    });
   1259  }
   1260 
   1261  /**
   1262   * Creates and manages the thread actor as part of the Browsing Context Target pool.
   1263   * This sets up the content window for being debugged
   1264   */
   1265  _createThreadActor() {
   1266    this.threadActor = new ThreadActor(this);
   1267    this.manage(this.threadActor);
   1268  }
   1269 
   1270  /**
   1271   * Exits the current thread actor and removes it from the Browsing Context Target pool.
   1272   * The content window is no longer being debugged after this call.
   1273   */
   1274  _destroyThreadActor() {
   1275    if (this.threadActor) {
   1276      this.threadActor.destroy();
   1277      this.threadActor = null;
   1278    }
   1279 
   1280    if (this._sourcesManager) {
   1281      this._sourcesManager.destroy();
   1282      this._sourcesManager = null;
   1283    }
   1284  }
   1285 
   1286  // Protocol Request Handlers
   1287 
   1288  detach() {
   1289    // Destroy the actor in the next event loop in order
   1290    // to ensure responding to the `detach` request.
   1291    DevToolsUtils.executeSoon(() => {
   1292      this.destroy();
   1293    });
   1294 
   1295    return {};
   1296  }
   1297 
   1298  /**
   1299   * Bring the window global's window to front.
   1300   */
   1301  focus() {
   1302    if (this.window) {
   1303      this.window.focus();
   1304    }
   1305    return {};
   1306  }
   1307 
   1308  goForward() {
   1309    // Wait a tick so that the response packet can be dispatched before the
   1310    // subsequent navigation event packet.
   1311    Services.tm.dispatchToMainThread(
   1312      DevToolsUtils.makeInfallible(() => {
   1313        // This won't work while the browser is shutting down and we don't really
   1314        // care.
   1315        if (Services.startup.shuttingDown) {
   1316          return;
   1317        }
   1318 
   1319        this.webNavigation.goForward();
   1320      }, "WindowGlobalTargetActor.prototype.goForward's delayed body")
   1321    );
   1322 
   1323    return {};
   1324  }
   1325 
   1326  goBack() {
   1327    // Wait a tick so that the response packet can be dispatched before the
   1328    // subsequent navigation event packet.
   1329    Services.tm.dispatchToMainThread(
   1330      DevToolsUtils.makeInfallible(() => {
   1331        // This won't work while the browser is shutting down and we don't really
   1332        // care.
   1333        if (Services.startup.shuttingDown) {
   1334          return;
   1335        }
   1336 
   1337        this.webNavigation.goBack();
   1338      }, "WindowGlobalTargetActor.prototype.goBack's delayed body")
   1339    );
   1340 
   1341    return {};
   1342  }
   1343 
   1344  /**
   1345   * Reload the page in this window global.
   1346   *
   1347   * @backward-compat { legacy }
   1348   *                  reload is preserved for third party tools. See Bug 1717837.
   1349   *                  DevTools should use Descriptor::reloadDescriptor instead.
   1350   */
   1351  reload(request) {
   1352    const force = request?.options?.force;
   1353    // Wait a tick so that the response packet can be dispatched before the
   1354    // subsequent navigation event packet.
   1355    Services.tm.dispatchToMainThread(
   1356      DevToolsUtils.makeInfallible(() => {
   1357        // This won't work while the browser is shutting down and we don't really
   1358        // care.
   1359        if (Services.startup.shuttingDown) {
   1360          return;
   1361        }
   1362 
   1363        this.webNavigation.reload(
   1364          force
   1365            ? Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE
   1366            : Ci.nsIWebNavigation.LOAD_FLAGS_NONE
   1367        );
   1368      }, "WindowGlobalTargetActor.prototype.reload's delayed body")
   1369    );
   1370    return {};
   1371  }
   1372 
   1373  /**
   1374   * Navigate this window global to a new location
   1375   */
   1376  navigateTo(request) {
   1377    // Wait a tick so that the response packet can be dispatched before the
   1378    // subsequent navigation event packet.
   1379    Services.tm.dispatchToMainThread(
   1380      DevToolsUtils.makeInfallible(() => {
   1381        this.window.location = request.url;
   1382      }, "WindowGlobalTargetActor.prototype.navigateTo's delayed body:" + request.url)
   1383    );
   1384    return {};
   1385  }
   1386 
   1387  /**
   1388   * For browsing-context targets which can't use the watcher configuration
   1389   * actor (eg webextension targets), the client directly calls `reconfigure`.
   1390   * Once all targets support the watcher, this method can be removed.
   1391   */
   1392  reconfigure(request) {
   1393    const options = request.options || {};
   1394    return this.updateTargetConfiguration(options);
   1395  }
   1396 
   1397  /**
   1398   * Apply target-specific options.
   1399   *
   1400   * This will be called by the watcher when the DevTools target-configuration
   1401   * is updated, or when a target is created via JSWindowActors.
   1402   */
   1403  updateTargetConfiguration(options = {}, calledFromDocumentCreation = false) {
   1404    if (!this.docShell) {
   1405      // The window global is already closed.
   1406      return;
   1407    }
   1408 
   1409    // Also update configurations which applies to all target types
   1410    super.updateTargetConfiguration(options, calledFromDocumentCreation);
   1411 
   1412    let reload = false;
   1413    if (typeof options.touchEventsOverride !== "undefined") {
   1414      const enableTouchSimulator = options.touchEventsOverride === "enabled";
   1415 
   1416      // We want to reload the document if it's an "existing" top level target on which
   1417      // the touch simulator will be toggled and the user has turned the
   1418      // "reload on touch simulation" setting on.
   1419      if (
   1420        enableTouchSimulator !== this.touchSimulator.enabled &&
   1421        options.reloadOnTouchSimulationToggle === true &&
   1422        this.isTopLevelTarget &&
   1423        !calledFromDocumentCreation
   1424      ) {
   1425        reload = true;
   1426      }
   1427 
   1428      if (enableTouchSimulator) {
   1429        this.touchSimulator.start();
   1430      } else {
   1431        this.touchSimulator.stop();
   1432      }
   1433    }
   1434 
   1435    if (typeof options.customFormatters !== "undefined") {
   1436      this.customFormatters = options.customFormatters;
   1437    }
   1438 
   1439    if (typeof options.useSimpleHighlightersForReducedMotion == "boolean") {
   1440      this._useSimpleHighlightersForReducedMotion =
   1441        options.useSimpleHighlightersForReducedMotion;
   1442      this.emit("use-simple-highlighters-updated");
   1443    }
   1444 
   1445    if (!this.isTopLevelTarget) {
   1446      // Following DevTools target options should only apply to the top target and be
   1447      // propagated through the window global tree via the platform.
   1448      return;
   1449    }
   1450    if (typeof options.restoreFocus == "boolean") {
   1451      this._restoreFocus = options.restoreFocus;
   1452    }
   1453    if (typeof options.recordAllocations == "object") {
   1454      const actor = this._memoryActor;
   1455      if (options.recordAllocations == null) {
   1456        actor.stopRecordingAllocations();
   1457      } else {
   1458        actor.attach();
   1459        actor.startRecordingAllocations(options.recordAllocations);
   1460      }
   1461    }
   1462 
   1463    if (reload) {
   1464      this.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
   1465    }
   1466  }
   1467 
   1468  get touchSimulator() {
   1469    if (!this._touchSimulator) {
   1470      this._touchSimulator = new TouchSimulator(this);
   1471    }
   1472 
   1473    return this._touchSimulator;
   1474  }
   1475 
   1476  /**
   1477   * Opposite of the updateTargetConfiguration method, that resets document
   1478   * state when closing the toolbox.
   1479   */
   1480  _restoreTargetConfiguration() {
   1481    if (this._restoreFocus && this.browsingContext?.isActive && this.window) {
   1482      try {
   1483        this.window.focus();
   1484      } catch (e) {
   1485        // When closing devtools while navigating, focus() may throw NS_ERROR_XPC_SECURITY_MANAGER_VETO
   1486        if (e.result != Cr.NS_ERROR_XPC_SECURITY_MANAGER_VETO) {
   1487          throw e;
   1488        }
   1489      }
   1490    }
   1491  }
   1492 
   1493  _changeTopLevelDocument(window) {
   1494    // WebExtensions are still using one WindowGlobalTarget instance for many document.
   1495    // When reloading the add-on, the current docShell/window we are attached to may be being destroyed
   1496    // and throwing when accessing its properties.
   1497    // Ignore the current window and only register the new and functional window.
   1498    if (!this.docShell.isBeingDestroyed() && this.window) {
   1499      // Fake a will-navigate on the previous document
   1500      // to let a chance to unregister it
   1501      this._willNavigate({
   1502        window: this.window,
   1503        newURI: window.location.href,
   1504        request: null,
   1505        isFrameSwitching: true,
   1506        navigationStart: Date.now(),
   1507      });
   1508 
   1509      this._windowDestroyed(this.window, {
   1510        isFrozen: true,
   1511        isFrameSwitching: true,
   1512      });
   1513    }
   1514 
   1515    // Immediately change the window as this window, if in process of unload
   1516    // may already be non working on the next cycle and start throwing
   1517    this._setWindow(window);
   1518 
   1519    DevToolsUtils.executeSoon(() => {
   1520      // No need to do anything more if the actor is destroyed.
   1521      // e.g. the client has been closed and the actors destroyed in the meantime.
   1522      if (this.isDestroyed()) {
   1523        return;
   1524      }
   1525 
   1526      // Then fake window-ready and navigate on the given document
   1527      this._windowReady(window, { isFrameSwitching: true });
   1528      DevToolsUtils.executeSoon(() => {
   1529        this._navigate(window, true);
   1530      });
   1531    });
   1532  }
   1533 
   1534  _setWindow(window) {
   1535    // Here is the very important call where we switch the currently targeted
   1536    // window global (it will indirectly update this.window and many other
   1537    // attributes defined from docShell).
   1538    this.docShell = window.docShell;
   1539    this.emit("changed-toplevel-document");
   1540    this.emit("frameUpdate", {
   1541      selected: this.outerWindowID,
   1542    });
   1543  }
   1544 
   1545  /**
   1546   * Handle location changes, by clearing the previous debuggees and enabling
   1547   * debugging, which may have been disabled temporarily by the
   1548   * DebuggerProgressListener.
   1549   */
   1550  _windowReady(window, { isFrameSwitching, isBFCache } = {}) {
   1551    if (this.ignoreSubFrames) {
   1552      return;
   1553    }
   1554    const isTopLevel = window == this.window;
   1555 
   1556    // We just reset iframe list on WillNavigate, so we now list all existing
   1557    // frames when we load a new document in the original window
   1558    if (window == this._originalWindow && !isFrameSwitching) {
   1559      this._updateChildDocShells();
   1560    }
   1561 
   1562    // If this follows WindowGlobal lifecycle, a new Target actor will be spawn for the top level
   1563    // target document. Only notify about in-process iframes.
   1564    // Note that OOP iframes won't emit window-ready and will also have their dedicated target.
   1565    // Also, we allow window-ready to be fired for iframe switching of top level documents,
   1566    // otherwise the iframe dropdown no longer works with server side targets.
   1567    if (this.followWindowGlobalLifeCycle && isTopLevel && !isFrameSwitching) {
   1568      return;
   1569    }
   1570 
   1571    this.emit("window-ready", {
   1572      window,
   1573      isTopLevel,
   1574      isBFCache,
   1575      id: getWindowID(window),
   1576      isFrameSwitching,
   1577    });
   1578  }
   1579 
   1580  _windowDestroyed(
   1581    window,
   1582    { id = null, isFrozen = false, isFrameSwitching = false }
   1583  ) {
   1584    if (this.ignoreSubFrames) {
   1585      return;
   1586    }
   1587    const isTopLevel = window == this.window;
   1588 
   1589    // If this follows WindowGlobal lifecycle, this target will be destroyed, alongside its top level document.
   1590    // Only notify about in-process iframes.
   1591    // Note that OOP iframes won't emit window-ready and will also have their dedicated target.
   1592    // Also, we allow window-destroyed to be fired for iframe switching of top level documents,
   1593    // otherwise the iframe dropdown no longer works with server side targets.
   1594    if (this.followWindowGlobalLifeCycle && isTopLevel && !isFrameSwitching) {
   1595      return;
   1596    }
   1597 
   1598    this.emit("window-destroyed", {
   1599      window,
   1600      isTopLevel,
   1601      id: id || getWindowID(window),
   1602      isFrozen,
   1603    });
   1604  }
   1605 
   1606  /**
   1607   * Start notifying server and client about a new document being loaded in the
   1608   * currently targeted window global.
   1609   */
   1610  _willNavigate({
   1611    window,
   1612    newURI,
   1613    request,
   1614    isFrameSwitching = false,
   1615    navigationStart,
   1616  }) {
   1617    if (this.ignoreSubFrames) {
   1618      return;
   1619    }
   1620    let isTopLevel = window == this.window;
   1621 
   1622    let reset = false;
   1623    if (window == this._originalWindow && !isFrameSwitching) {
   1624      // If the top level document changes and we are targeting an iframe, we
   1625      // need to reset to the upcoming new top level document. But for this
   1626      // will-navigate event, we will dispatch on the old window. (The inspector
   1627      // codebase expect to receive will-navigate for the currently displayed
   1628      // document in order to cleanup the markup view)
   1629      if (this.window != this._originalWindow) {
   1630        reset = true;
   1631        window = this.window;
   1632        isTopLevel = true;
   1633      }
   1634    }
   1635 
   1636    // will-navigate event needs to be dispatched synchronously, by calling the
   1637    // listeners in the order or registration. This event fires once navigation
   1638    // starts, (all pending user prompts are dealt with), but before the first
   1639    // request starts.
   1640    this.emit("will-navigate", {
   1641      window,
   1642      isTopLevel,
   1643      newURI,
   1644      request,
   1645      navigationStart,
   1646      isFrameSwitching,
   1647    });
   1648 
   1649    // We don't do anything for inner frames here.
   1650    // (we will only update thread actor on window-ready)
   1651    if (!isTopLevel) {
   1652      return;
   1653    }
   1654 
   1655    // When the actor acts as a WindowGlobalTarget, will-navigate won't fired.
   1656    // Instead we will receive a new top level target with isTargetSwitching=true.
   1657    if (!this.followWindowGlobalLifeCycle) {
   1658      this.emit("tabNavigated", {
   1659        url: newURI,
   1660        state: "start",
   1661        isFrameSwitching,
   1662      });
   1663    }
   1664 
   1665    if (reset) {
   1666      this._setWindow(this._originalWindow);
   1667    }
   1668  }
   1669 
   1670  /**
   1671   * Notify server and client about a new document done loading in the current
   1672   * targeted window global.
   1673   */
   1674  _navigate(window, isFrameSwitching = false) {
   1675    if (this.ignoreSubFrames) {
   1676      return;
   1677    }
   1678    const isTopLevel = window == this.window;
   1679 
   1680    // navigate event needs to be dispatched synchronously,
   1681    // by calling the listeners in the order or registration.
   1682    // This event is fired once the document is loaded,
   1683    // after the load event, it's document ready-state is 'complete'.
   1684    this.emit("navigate", {
   1685      window,
   1686      isTopLevel,
   1687    });
   1688 
   1689    // We don't do anything for inner frames here.
   1690    // (we will only update thread actor on window-ready)
   1691    if (!isTopLevel) {
   1692      return;
   1693    }
   1694 
   1695    // We may still significate when the document is done loading, via navigate.
   1696    // But as we no longer fire the "will-navigate", may be it is better to find
   1697    // other ways to get to our means.
   1698    // Listening to "navigate" is misleading as the document may already be loaded
   1699    // if we just opened the DevTools. So it is better to use "watch" pattern
   1700    // and instead have the actor either emit immediately resources as they are
   1701    // already available, or later on as the load progresses.
   1702    if (this.followWindowGlobalLifeCycle) {
   1703      return;
   1704    }
   1705 
   1706    this.emit("tabNavigated", {
   1707      url: this.url,
   1708      title: this.title,
   1709      state: "stop",
   1710      isFrameSwitching,
   1711    });
   1712  }
   1713 
   1714  removeActorByName(name) {
   1715    if (name in this._extraActors) {
   1716      const actor = this._extraActors[name];
   1717      if (this._targetScopedActorPool.has(actor)) {
   1718        this._targetScopedActorPool.removeActor(actor);
   1719      }
   1720      delete this._extraActors[name];
   1721    }
   1722  }
   1723 }
   1724 
   1725 exports.WindowGlobalTargetActor = WindowGlobalTargetActor;
   1726 
   1727 class DebuggerProgressListener {
   1728  /**
   1729   * The DebuggerProgressListener class is an nsIWebProgressListener which
   1730   * handles onStateChange events for the targeted window global. If the user
   1731   * tries to navigate away from a paused page, the listener makes sure that the
   1732   * debuggee is resumed before the navigation begins.
   1733   *
   1734   * @param WindowGlobalTargetActor targetActor
   1735   *        The window global target actor associated with this listener.
   1736   */
   1737  constructor(targetActor) {
   1738    this._targetActor = targetActor;
   1739    this._onWindowCreated = this.onWindowCreated.bind(this);
   1740    this._onWindowHidden = this.onWindowHidden.bind(this);
   1741 
   1742    // Watch for windows destroyed (global observer that will need filtering)
   1743    Services.obs.addObserver(this, "inner-window-destroyed");
   1744 
   1745    // XXX: for now we maintain the list of windows we know about in this instance
   1746    // so that we can discriminate windows we care about when observing
   1747    // inner-window-destroyed events. Bug 1016952 would remove the need for this.
   1748    this._knownWindowIDs = new Map();
   1749 
   1750    this._watchedDocShells = new WeakSet();
   1751  }
   1752 
   1753  QueryInterface = ChromeUtils.generateQI([
   1754    "nsIWebProgressListener",
   1755    "nsISupportsWeakReference",
   1756  ]);
   1757 
   1758  destroy() {
   1759    Services.obs.removeObserver(this, "inner-window-destroyed");
   1760    this._knownWindowIDs.clear();
   1761    this._knownWindowIDs = null;
   1762  }
   1763 
   1764  watch(docShell) {
   1765    // Add the docshell to the watched set. We're actually adding the window,
   1766    // because docShell objects are not wrappercached and would be rejected
   1767    // by the WeakSet.
   1768    const docShellWindow = docShell.domWindow;
   1769    this._watchedDocShells.add(docShellWindow);
   1770 
   1771    const webProgress = docShell
   1772      .QueryInterface(Ci.nsIInterfaceRequestor)
   1773      .getInterface(Ci.nsIWebProgress);
   1774    webProgress.addProgressListener(
   1775      this,
   1776      Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
   1777        Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
   1778    );
   1779 
   1780    const handler = getDocShellChromeEventHandler(docShell);
   1781    handler.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
   1782    handler.addEventListener("pageshow", this._onWindowCreated, true);
   1783    handler.addEventListener("pagehide", this._onWindowHidden, true);
   1784 
   1785    // Dispatch the _windowReady event on the targetActor for pre-existing windows
   1786    const windows = this._targetActor.ignoreSubFrames
   1787      ? [docShellWindow]
   1788      : this._getWindowsInDocShell(docShell);
   1789    for (const win of windows) {
   1790      this._targetActor._windowReady(win);
   1791      this._knownWindowIDs.set(getWindowID(win), win);
   1792    }
   1793 
   1794    // Immediately enable CSS error reports on new top level docshells, if this was already enabled.
   1795    // This is specific to MBT and WebExtension targets (so the isRootActor check).
   1796    if (
   1797      this._targetActor.isRootActor &&
   1798      this._targetActor.docShell.cssErrorReportingEnabled
   1799    ) {
   1800      docShell.cssErrorReportingEnabled = true;
   1801    }
   1802  }
   1803 
   1804  unwatch(docShell) {
   1805    // If the docshell is being destroyed, we won't be able to retrieve its related window object,
   1806    // which is the key ingredient for all cleanup operations done in this method.
   1807    if (docShell.isBeingDestroyed()) {
   1808      return;
   1809    }
   1810 
   1811    const docShellWindow = docShell.domWindow;
   1812    if (!this._watchedDocShells.has(docShellWindow)) {
   1813      return;
   1814    }
   1815    this._watchedDocShells.delete(docShellWindow);
   1816 
   1817    const webProgress = docShell
   1818      .QueryInterface(Ci.nsIInterfaceRequestor)
   1819      .getInterface(Ci.nsIWebProgress);
   1820    // During process shutdown, the docshell may already be cleaned up and throw
   1821    try {
   1822      webProgress.removeProgressListener(this);
   1823    } catch (e) {
   1824      // ignore
   1825    }
   1826 
   1827    const handler = getDocShellChromeEventHandler(docShell);
   1828    handler.removeEventListener(
   1829      "DOMWindowCreated",
   1830      this._onWindowCreated,
   1831      true
   1832    );
   1833    handler.removeEventListener("pageshow", this._onWindowCreated, true);
   1834    handler.removeEventListener("pagehide", this._onWindowHidden, true);
   1835 
   1836    const windows = this._targetActor.ignoreSubFrames
   1837      ? [docShellWindow]
   1838      : this._getWindowsInDocShell(docShell);
   1839    for (const win of windows) {
   1840      this._knownWindowIDs.delete(getWindowID(win));
   1841    }
   1842  }
   1843 
   1844  _getWindowsInDocShell(docShell) {
   1845    return getChildDocShells(docShell).map(d => {
   1846      return d.domWindow;
   1847    });
   1848  }
   1849 
   1850  onWindowCreated = DevToolsUtils.makeInfallible(function (evt) {
   1851    if (this._targetActor.isDestroyed()) {
   1852      return;
   1853    }
   1854 
   1855    // If we're in a frame swap (which occurs when toggling RDM, for example), then we can
   1856    // ignore this event, as the window never really went anywhere for our purposes.
   1857    if (evt.inFrameSwap) {
   1858      return;
   1859    }
   1860 
   1861    const window = evt.target.defaultView;
   1862    if (!window) {
   1863      // Some old UIs might emit unrelated events called pageshow/pagehide on
   1864      // elements which are not documents. Bail in this case. See Bug 1669666.
   1865      return;
   1866    }
   1867 
   1868    const innerID = getWindowID(window);
   1869 
   1870    // This handler is called for two events: "DOMWindowCreated" and "pageshow".
   1871    // Bail out if we already processed this window.
   1872    if (this._knownWindowIDs.has(innerID)) {
   1873      return;
   1874    }
   1875    this._knownWindowIDs.set(innerID, window);
   1876 
   1877    // For a regular page navigation, "DOMWindowCreated" is fired before
   1878    // "pageshow". If the current event is "pageshow" but we have not processed
   1879    // the window yet, it means this is a BF cache navigation. In theory,
   1880    // `event.persisted` should be set for BF cache navigation events, but it is
   1881    // not always available, so we fallback on checking if "pageshow" is the
   1882    // first event received for a given window (see Bug 1378133).
   1883    const isBFCache = evt.type == "pageshow";
   1884 
   1885    this._targetActor._windowReady(window, { isBFCache });
   1886  }, "DebuggerProgressListener.prototype.onWindowCreated");
   1887 
   1888  onWindowHidden = DevToolsUtils.makeInfallible(function (evt) {
   1889    if (this._targetActor.isDestroyed()) {
   1890      return;
   1891    }
   1892 
   1893    // If we're in a frame swap (which occurs when toggling RDM, for example), then we can
   1894    // ignore this event, as the window isn't really going anywhere for our purposes.
   1895    if (evt.inFrameSwap) {
   1896      return;
   1897    }
   1898 
   1899    // Only act as if the window has been destroyed if the 'pagehide' event
   1900    // was sent for a persisted window (persisted is set when the page is put
   1901    // and frozen in the bfcache). If the page isn't persisted, the observer's
   1902    // inner-window-destroyed event will handle it.
   1903    if (!evt.persisted) {
   1904      return;
   1905    }
   1906 
   1907    const window = evt.target.defaultView;
   1908    if (!window) {
   1909      // Some old UIs might emit unrelated events called pageshow/pagehide on
   1910      // elements which are not documents. Bail in this case. See Bug 1669666.
   1911      return;
   1912    }
   1913 
   1914    this._targetActor._windowDestroyed(window, { isFrozen: true });
   1915    this._knownWindowIDs.delete(getWindowID(window));
   1916  }, "DebuggerProgressListener.prototype.onWindowHidden");
   1917 
   1918  observe = DevToolsUtils.makeInfallible(function (subject) {
   1919    if (this._targetActor.isDestroyed()) {
   1920      return;
   1921    }
   1922 
   1923    // Because this observer will be called for all inner-window-destroyed in
   1924    // the application, we need to filter out events for windows we are not
   1925    // watching
   1926    const innerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
   1927    const window = this._knownWindowIDs.get(innerID);
   1928    if (window) {
   1929      this._knownWindowIDs.delete(innerID);
   1930      this._targetActor._windowDestroyed(window, { id: innerID });
   1931    }
   1932 
   1933    // Bug 1598364: when debugging browser.xhtml from the Browser Toolbox
   1934    // the DOMWindowCreated/pageshow/pagehide event listeners have to be
   1935    // re-registered against the next document when we reload browser.html
   1936    // (or navigate to another doc).
   1937    // That's because we registered the listener on docShell.domWindow as
   1938    // top level windows don't have a chromeEventHandler.
   1939    if (
   1940      this._watchedDocShells.has(window) &&
   1941      // Avoid exception when the notified window is a cross origin object
   1942      // (most likely an iframe running in a distinct origin)
   1943      !Cu.isRemoteProxy(window) &&
   1944      window.docShell &&
   1945      !window.docShell.chromeEventHandler
   1946    ) {
   1947      // First cleanup all the existing listeners
   1948      this.unwatch(window.docShell);
   1949      // Re-register new ones. The docShell is already referencing the new document.
   1950      this.watch(window.docShell);
   1951    }
   1952  }, "DebuggerProgressListener.prototype.observe");
   1953 
   1954  onStateChange = DevToolsUtils.makeInfallible(function (
   1955    progress,
   1956    request,
   1957    flag
   1958  ) {
   1959    if (this._targetActor.isDestroyed()) {
   1960      return;
   1961    }
   1962    progress.QueryInterface(Ci.nsIDocShell);
   1963    if (progress.isBeingDestroyed()) {
   1964      return;
   1965    }
   1966 
   1967    const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
   1968    const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
   1969    const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
   1970    const isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
   1971 
   1972    // Ideally, we would fetch navigationStart from window.performance.timing.navigationStart
   1973    // but as WindowGlobal isn't instantiated yet we don't have access to it.
   1974    // This is ultimately handed over to DocumentEventListener, which uses this.
   1975    // See its comment about WILL_NAVIGATE_TIME_SHIFT for more details about the related workaround.
   1976    const navigationStart = Date.now();
   1977 
   1978    // Catch any iframe location change
   1979    if (isDocument && isStop) {
   1980      // Watch document stop to ensure having the new iframe url.
   1981      this._targetActor._notifyDocShellsUpdate([progress]);
   1982    }
   1983 
   1984    const window = progress.DOMWindow;
   1985    if (isDocument && isStart) {
   1986      // One of the earliest events that tells us a new URI
   1987      // is being loaded in this window.
   1988      const newURI = request instanceof Ci.nsIChannel ? request.URI.spec : null;
   1989      this._targetActor._willNavigate({
   1990        window,
   1991        newURI,
   1992        request,
   1993        isFrameSwitching: false,
   1994        navigationStart,
   1995      });
   1996    }
   1997    if (isWindow && isStop) {
   1998      // Don't dispatch "navigate" event just yet when there is a redirect to
   1999      // about:neterror page.
   2000      // Navigating to about:neterror will make `status` be something else than NS_OK.
   2001      // But for some error like NS_BINDING_ABORTED we don't want to emit any `navigate`
   2002      // event as the page load has been cancelled and the related page document is going
   2003      // to be a dead wrapper.
   2004      if (
   2005        request.status != Cr.NS_OK &&
   2006        request.status != Cr.NS_BINDING_ABORTED
   2007      ) {
   2008        // Instead, listen for DOMContentLoaded as about:neterror is loaded
   2009        // with LOAD_BACKGROUND flags and never dispatches load event.
   2010        // That may be the same reason why there is no onStateChange event
   2011        // for about:neterror loads.
   2012        const handler = getDocShellChromeEventHandler(progress);
   2013        const onLoad = evt => {
   2014          // Ignore events from iframes
   2015          if (evt.target === window.document) {
   2016            handler.removeEventListener("DOMContentLoaded", onLoad, true);
   2017            this._targetActor._navigate(window);
   2018          }
   2019        };
   2020        handler.addEventListener("DOMContentLoaded", onLoad, true);
   2021      } else {
   2022        // Somewhat equivalent of load event.
   2023        // (window.document.readyState == complete)
   2024        this._targetActor._navigate(window);
   2025      }
   2026    }
   2027  }, "DebuggerProgressListener.prototype.onStateChange");
   2028 }