tor-browser

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

window-global.sys.mjs (28184B)


      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 import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs";
      6 
      7 const lazy = {};
      8 ChromeUtils.defineESModuleGetters(
      9  lazy,
     10  {
     11    HTMLSourcesCache:
     12      "resource://devtools/server/actors/utils/HTMLSourcesCache.sys.mjs",
     13    isWindowGlobalPartOfContext:
     14      "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
     15    WEBEXTENSION_FALLBACK_DOC_URL:
     16      "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
     17  },
     18  { global: "contextual" }
     19 );
     20 
     21 // TargetActorRegistery has to be shared between all devtools instances
     22 // and so is loaded into the shared global.
     23 ChromeUtils.defineESModuleGetters(
     24  lazy,
     25  {
     26    TargetActorRegistry:
     27      "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
     28  },
     29  { global: "shared" }
     30 );
     31 
     32 function watch() {
     33  // Set the following preference in this function, so that we can easily
     34  // toggle these preferences on and off from tests and have the new value being picked up.
     35 
     36  // bfcache-in-parent changes significantly how navigation behaves.
     37  // We may start reusing previously existing WindowGlobal and so reuse
     38  // previous set of JSWindowActor pairs (i.e. DevToolsProcessParent/DevToolsProcessChild).
     39  // When enabled, regular navigations may also change and spawn new BrowsingContexts.
     40  // If the page we navigate from supports being stored in bfcache,
     41  // the navigation will use a new BrowsingContext. And so force spawning
     42  // a new top-level target.
     43  ChromeUtils.defineLazyGetter(
     44    lazy,
     45    "isBfcacheInParentEnabled",
     46    () =>
     47      Services.appinfo.sessionHistoryInParent &&
     48      Services.prefs.getBoolPref("fission.bfcacheInParent", false)
     49  );
     50 
     51  // Observe for all necessary event to track new and destroyed WindowGlobals.
     52  Services.obs.addObserver(observe, "content-document-global-created");
     53  Services.obs.addObserver(observe, "chrome-document-global-created");
     54  Services.obs.addObserver(observe, "content-page-shown");
     55  Services.obs.addObserver(observe, "chrome-page-shown");
     56  Services.obs.addObserver(observe, "content-page-hidden");
     57  Services.obs.addObserver(observe, "chrome-page-hidden");
     58  Services.obs.addObserver(observe, "inner-window-destroyed");
     59  Services.obs.addObserver(observe, "initial-document-element-inserted");
     60 }
     61 
     62 function unwatch() {
     63  // Observe for all necessary event to track new and destroyed WindowGlobals.
     64  Services.obs.removeObserver(observe, "content-document-global-created");
     65  Services.obs.removeObserver(observe, "chrome-document-global-created");
     66  Services.obs.removeObserver(observe, "content-page-shown");
     67  Services.obs.removeObserver(observe, "chrome-page-shown");
     68  Services.obs.removeObserver(observe, "content-page-hidden");
     69  Services.obs.removeObserver(observe, "chrome-page-hidden");
     70  Services.obs.removeObserver(observe, "inner-window-destroyed");
     71  Services.obs.removeObserver(observe, "initial-document-element-inserted");
     72 }
     73 
     74 function createTargetsForWatcher(watcherDataObject, isProcessActorStartup) {
     75  const { sessionContext } = watcherDataObject;
     76  // Bug 1785266 - For now, in browser, when debugging the parent process (childID == 0),
     77  // we spawn only the ParentProcessTargetActor, which will debug all the BrowsingContext running in the process.
     78  // So that we have to avoid instantiating any here.
     79  if (
     80    sessionContext.type == "all" &&
     81    ChromeUtils.domProcessChild.childID === 0
     82  ) {
     83    return;
     84  }
     85 
     86  function lookupForTargets(window) {
     87    // Do not only track top level BrowsingContext in this content process,
     88    // but also any nested iframe which may be running in the same process.
     89    for (const browsingContext of window.docShell.browsingContext.getAllBrowsingContextsInSubtree()) {
     90      const { currentWindowContext } = browsingContext;
     91      // Only consider Window Global which are running in this process
     92      if (!currentWindowContext || !currentWindowContext.isInProcess) {
     93        continue;
     94      }
     95 
     96      // WindowContext's windowGlobalChild should be defined for WindowGlobal running in this process
     97      const { windowGlobalChild } = currentWindowContext;
     98 
     99      // getWindowEnumerator will expose somewhat unexpected WindowGlobal when a tab navigated.
    100      // This will expose WindowGlobals of past navigations. Document which are in the bfcache
    101      // and aren't the current WindowGlobal of their BrowsingContext.
    102      if (!windowGlobalChild.isCurrentGlobal) {
    103        continue;
    104      }
    105 
    106      // Accept the initial about:blank document:
    107      //  - only from createTargetsForWatcher, when instantiating the target for the already existing WindowGlobals,
    108      //  - when we do that on toolbox opening, to prevent creating one when the process is starting.
    109      //
    110      // This is to allow debugging blank tabs, which are on an initial about:blank document.
    111      //
    112      // We want to avoid creating transient targets for initial about blank when a new WindowGlobal
    113      // just get created as it will most likely navigate away just after and confuse the frontend with short lived target.
    114      const acceptUncommitedInitialDocument = !isProcessActorStartup;
    115 
    116      if (
    117        lazy.isWindowGlobalPartOfContext(windowGlobalChild, sessionContext, {
    118          acceptUncommitedInitialDocument,
    119        })
    120      ) {
    121        createWindowGlobalTargetActor(watcherDataObject, windowGlobalChild);
    122      } else if (
    123        !browsingContext.parent &&
    124        sessionContext.browserId &&
    125        browsingContext.browserId == sessionContext.browserId &&
    126        browsingContext.window.document.isInitialDocument
    127      ) {
    128        // In order to succesfully get the devtools-html-content event in SourcesManager,
    129        // we have to ensure flagging the initial about:blank document...
    130        // While we don't create a target for it, we need to set this flag for this event to be emitted.
    131        browsingContext.watchedByDevTools = true;
    132      }
    133    }
    134  }
    135 
    136  const topWindows = [];
    137  // Do not use a for loop on `windowEnumerator` as underlying call to `getNext` may throw on some windows
    138  const windowEnumerator = Services.ww.getWindowEnumerator();
    139  while (windowEnumerator.hasMoreElements()) {
    140    try {
    141      const window = windowEnumerator.getNext();
    142      topWindows.push(window);
    143    } catch (e) {
    144      console.error("Failed to process a top level window", e);
    145    }
    146  }
    147 
    148  // When debugging an extension, we have to ensure create the top level target first.
    149  // The top level target is the one for the fallback document.
    150  if (sessionContext.type == "webextension") {
    151    const fallbackWindowIndex = topWindows.findIndex(window =>
    152      window.location.href.startsWith(lazy.WEBEXTENSION_FALLBACK_DOC_URL)
    153    );
    154    if (fallbackWindowIndex != -1) {
    155      const [fallbackWindow] = topWindows.splice(fallbackWindowIndex, 1);
    156      topWindows.unshift(fallbackWindow);
    157    }
    158  }
    159 
    160  for (const window of topWindows) {
    161    lookupForTargets(window);
    162 
    163    // `lookupForTargets` uses `getAllBrowsingContextsInSubTree`, but this will ignore browser elements
    164    // using type="content". So manually retrieve the windows for these browser elements,
    165    // in case we have tabs opened on document loaded in the same process.
    166    // This codepath is meant when we are in the parent process, with browser.xhtml having these <browser type="content">
    167    // elements for tabs.
    168    for (const browser of window.document.querySelectorAll(
    169      `browser[type="content"]`
    170    )) {
    171      // Bug 1947777 - the browser context may not have an active DOM Window
    172      const childWindow = browser.browsingContext?.window;
    173      // If the tab isn't on a document loaded in the parent process,
    174      // the window will be null.
    175      if (childWindow) {
    176        lookupForTargets(childWindow);
    177      }
    178    }
    179  }
    180 
    181  // Utility class to watch for HTML Sources text content emitted by the HTML Parser.
    182  // As this can come from the previous WindowGlobal, we need this logic to be running outside
    183  // of individual WindowGlobal targets.
    184  if (sessionContext.type != "all") {
    185    lazy.HTMLSourcesCache.watch(sessionContext.browserId);
    186  }
    187 }
    188 
    189 function destroyTargetsForWatcher(watcherDataObject, options) {
    190  // Unregister and destroy the existing target actors for this target type
    191  const actorsToDestroy = watcherDataObject.actors.filter(
    192    actor => actor.targetType == "frame"
    193  );
    194  watcherDataObject.actors = watcherDataObject.actors.filter(
    195    actor => actor.targetType != "frame"
    196  );
    197 
    198  for (const actor of actorsToDestroy) {
    199    ContentProcessWatcherRegistry.destroyTargetActor(
    200      watcherDataObject,
    201      actor,
    202      options
    203    );
    204  }
    205 
    206  if (watcherDataObject.sessionContext.type != "all") {
    207    lazy.HTMLSourcesCache.unwatch(watcherDataObject.sessionContext.browserId);
    208  }
    209 }
    210 
    211 /**
    212 * Called whenever a new WindowGlobal is instantiated either:
    213 *  - when navigating to a new page (DOMWindowCreated)
    214 *  - by a bfcache navigation (pageshow)
    215 *
    216 * @param {Window} window
    217 * @param {object} options
    218 * @param {boolean} options.isBFCache
    219 *        True, if the request to instantiate a new target comes from a bfcache navigation.
    220 *        i.e. when we receive a pageshow event with persisted=true.
    221 *        This will be true regardless of bfcacheInParent being enabled or disabled.
    222 * @param {boolean} options.ignoreIfExisting
    223 *        By default to false. If true is passed, we avoid instantiating a target actor
    224 *        if one already exists for this windowGlobal.
    225 */
    226 function onWindowGlobalCreated(
    227  window,
    228  { isBFCache = false, ignoreIfExisting = false } = {}
    229 ) {
    230  try {
    231    const windowGlobal = window.windowGlobalChild;
    232 
    233    // For bfcache navigations, we only create new targets when bfcacheInParent is enabled,
    234    // as this would be the only case where new DocShells will be created. This requires us to spawn a
    235    // new WindowGlobalTargetActor as such actor is bound to a unique DocShell.
    236    const forceAcceptTopLevelTarget =
    237      isBFCache && lazy.isBfcacheInParentEnabled;
    238 
    239    for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects(
    240      "frame"
    241    )) {
    242      const { sessionContext } = watcherDataObject;
    243      /*
    244      try {
    245        windowGlobal.browsingContext.watchedByDevTools = true;
    246      } catch (e) {}
    247      try {
    248        windowGlobal.browsingContext.top.watchedByDevTools = true;
    249      } catch (e) {}
    250      */
    251      if (
    252        lazy.isWindowGlobalPartOfContext(windowGlobal, sessionContext, {
    253          forceAcceptTopLevelTarget,
    254        })
    255      ) {
    256        // If this was triggered because of a navigation, we want to retrieve the existing
    257        // target we were debugging so we can destroy it before creating the new target.
    258        // This is important because we had cases where the destruction of an old target
    259        // was unsetting a flag on the **new** target document, breaking the toolbox (See Bug 1721398).
    260 
    261        // We're checking for an existing target given a watcherActorID + browserId + browsingContext.
    262        // Note that a target switch might create a new browsing context, so we wouldn't
    263        // retrieve the existing target here. We are okay with this as:
    264        // - this shouldn't happen much
    265        // - in such case we weren't seeing the issue of Bug 1721398 (the old target can't access the new document)
    266        const existingTarget = findTargetActor({
    267          watcherDataObject,
    268          innerWindowId: windowGlobal.innerWindowId,
    269          // Also use a loose match per browsing context's ID instead of only window global's innerWindowId
    270          // as we would like to destroy eagerly the previous target actor,
    271          // which will anyway be wiped by the frontend as soon as we notify it about this new target.
    272          // TargetCommand wipes all existing targets as soon as we receive a new Top Level target.
    273          //
    274          // Matching only per innerWindowId was making browser_webconsole_message_categories.js test to fail
    275          // because some very early message resource was attached to this `existingTarget` actor.
    276          // The `existingTarget` actor is actually destroyed and their related resource ignored.
    277          // Instead we bind these early resources to the new target actor.
    278          browsingContextID: windowGlobal.browsingContext.id,
    279        });
    280 
    281        // See comment in `observe()` method and `DOMDocElementInserted` condition to know why we sometime
    282        // ignore this method call if a target actor already exists.
    283        // It means that we got a previous DOMWindowCreated event, related to a non-about:blank document,
    284        // and we should ignore the DOMDocElementInserted.
    285        // In any other scenario, destroy the already existing target and re-create a new one.
    286        if (existingTarget && ignoreIfExisting) {
    287          continue;
    288        }
    289 
    290        // Bail if there is already an existing WindowGlobalTargetActor which wasn't
    291        // created from a JSWIndowActor.
    292        // This means we are reloading or navigating (same-process) a Target
    293        // which has not been created using the Watcher, but from the client (most likely
    294        // the initial target of a local-tab toolbox).
    295        // However, we force overriding the first message manager based target in case of
    296        // BFCache navigations.
    297        if (
    298          existingTarget &&
    299          !existingTarget.createdFromJsWindowActor &&
    300          !isBFCache
    301        ) {
    302          continue;
    303        }
    304 
    305        // If we decide to instantiate a new target and there was one before,
    306        // first destroy the previous one.
    307        // Otherwise its destroy sequence will be executed *after* the new one
    308        // is being initialized and may easily revert changes made against platform API.
    309        // (typically toggle platform boolean attributes back to default…)
    310        if (existingTarget) {
    311          existingTarget.destroy({ isTargetSwitching: true });
    312        }
    313 
    314        // When navigating to another process, the Watcher Actor won't have sent any query
    315        // to the new process JS Actor as the debugged tab was on another process before navigation.
    316        // But `sharedData` will have data about all the current watchers.
    317        // Here we have to ensure calling watchTargetsForWatcher in order to populate #connections
    318        // for the currently processed watcher actor and start listening for future targets.
    319        if (
    320          !ContentProcessWatcherRegistry.has(watcherDataObject.watcherActorID)
    321        ) {
    322          throw new Error("Watcher data seems out of sync");
    323        }
    324 
    325        createWindowGlobalTargetActor(watcherDataObject, windowGlobal, true);
    326      }
    327    }
    328  } catch (e) {
    329    // Ensure logging exception as they are silently ignore otherwise
    330    dump(
    331      " Exception while observing a new window: " + e + "\n" + e.stack + "\n"
    332    );
    333  }
    334 }
    335 
    336 /**
    337 * Called whenever a WindowGlobal just got destroyed, when closing the tab, or navigating to another one.
    338 *
    339 * @param {innerWindowId} innerWindowId
    340 *        The WindowGlobal's unique identifier.
    341 */
    342 function onWindowGlobalDestroyed(innerWindowId) {
    343  for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects(
    344    "frame"
    345  )) {
    346    const existingTarget = findTargetActor({
    347      watcherDataObject,
    348      innerWindowId,
    349    });
    350 
    351    if (!existingTarget) {
    352      continue;
    353    }
    354 
    355    // Do not do anything if both bfcache in parent and server targets are disabled
    356    // As history navigations will be handled within the same DocShell and by the
    357    // same WindowGlobalTargetActor. The actor will listen to pageshow/pagehide by itself.
    358    // We should not destroy any target.
    359    if (
    360      !lazy.isBfcacheInParentEnabled &&
    361      !watcherDataObject.sessionContext.isServerTargetSwitchingEnabled
    362    ) {
    363      continue;
    364    }
    365    // If the target actor isn't in watcher data object, it is a top level actor
    366    // instantiated via a Descriptor's getTarget method. It isn't registered into Watcher objects.
    367    // But we still want to destroy such target actor, and need to manually emit the targetDestroyed to the parent process.
    368    // Hopefully bug 1754452 should allow us to get rid of this workaround by making the top level actor
    369    // be created and managed by the watcher universe, like all the others.
    370    const isTopLevelActorRegisteredOutsideOfWatcherActor =
    371      !watcherDataObject.actors.find(
    372        actor => actor.innerWindowId == innerWindowId
    373      );
    374    const targetActorForm = isTopLevelActorRegisteredOutsideOfWatcherActor
    375      ? existingTarget.form()
    376      : null;
    377 
    378    existingTarget.destroy();
    379 
    380    if (isTopLevelActorRegisteredOutsideOfWatcherActor) {
    381      watcherDataObject.jsProcessActor.sendAsyncMessage(
    382        "DevToolsProcessChild:targetDestroyed",
    383        {
    384          actors: [
    385            {
    386              watcherActorID: watcherDataObject.watcherActorID,
    387              targetActorForm,
    388            },
    389          ],
    390          options: {},
    391        }
    392      );
    393    }
    394  }
    395 }
    396 
    397 /**
    398 * Instantiate a WindowGlobal target actor for a given browsing context
    399 * and for a given watcher actor.
    400 *
    401 * @param {object} watcherDataObject
    402 * @param {BrowsingContext} windowGlobalChild
    403 * @param {boolean} isDocumentCreation
    404 */
    405 function createWindowGlobalTargetActor(
    406  watcherDataObject,
    407  windowGlobalChild,
    408  isDocumentCreation = false
    409 ) {
    410  // When debugging privileged pages running a the shared system compartment, and we aren't in the browser toolbox (which already uses a distinct loader),
    411  // we have to use the distinct loader in order to ensure running DevTools in a distinct compartment than the page we are about to debug
    412  // Such page could be about:addons, chrome://browser/content/browser.xhtml,...
    413  const { browsingContext } = windowGlobalChild;
    414  const useDistinctLoader =
    415    browsingContext.associatedWindow.document.nodePrincipal.isSystemPrincipal;
    416  const { connection, loader } =
    417    ContentProcessWatcherRegistry.getOrCreateConnectionForWatcher(
    418      watcherDataObject.watcherActorID,
    419      useDistinctLoader
    420    );
    421 
    422  const { WindowGlobalTargetActor } = loader.require(
    423    "devtools/server/actors/targets/window-global"
    424  );
    425 
    426  // In the case of the browser toolbox, tab's BrowsingContext don't have
    427  // any parent BC and shouldn't be considered as top-level.
    428  // This is why we check for browserId's.
    429  const { sessionContext } = watcherDataObject;
    430  let isTopLevelTarget =
    431    !browsingContext.parent &&
    432    browsingContext.browserId == sessionContext.browserId;
    433 
    434  // Web Extension are many of many "top level" browsing context.
    435  // i.e. browsing context with no parent.
    436  // The background page and popup top browsing context have no parent.
    437  // Given that none of these browsing context are guaranteed to be active,
    438  // not guarantee to be always active while debugging an add-on,
    439  // the WebExtension Descriptor Actor will spawn an "fallback document",
    440  // which is guaranteed to be always active and dedicated to eachd debugged addon.
    441  // Thus, allowing us to have an identifier top level document to be provided
    442  // to an unique top level target actor.
    443  if (sessionContext.type == "webextension") {
    444    isTopLevelTarget = browsingContext.window.location.href.startsWith(
    445      lazy.WEBEXTENSION_FALLBACK_DOC_URL
    446    );
    447  }
    448 
    449  // Create the actual target actor.
    450  const targetActor = new WindowGlobalTargetActor(connection, {
    451    docShell: browsingContext.docShell,
    452    // Targets created from the server side, via Watcher actor and DevToolsProcess JSWindow
    453    // actor pairs are following WindowGlobal lifecycle. i.e. will be destroyed on any
    454    // type of navigation/reload.
    455    followWindowGlobalLifeCycle: true,
    456    isTopLevelTarget,
    457    ignoreSubFrames: true,
    458    sessionContext,
    459  });
    460  targetActor.createdFromJsWindowActor = true;
    461 
    462  ContentProcessWatcherRegistry.onNewTargetActor(
    463    watcherDataObject,
    464    targetActor,
    465    isDocumentCreation
    466  );
    467 }
    468 
    469 /**
    470 * Observer service notification handler.
    471 *
    472 * @param {DOMWindow|Document} subject
    473 *        A window for *-document-global-created
    474 *        A document for *-page-{shown|hide}
    475 * @param {string} topic
    476 */
    477 function observe(subject, topic) {
    478  if (
    479    topic == "content-document-global-created" ||
    480    topic == "chrome-document-global-created"
    481  ) {
    482    if (subject.isUncommittedInitialDocument) {
    483      // If this is the initial document, it might be a short-lived transient one, and
    484      // onWindowGlobalCreated will ignore such documents. If we receive a load
    485      // event, the document has been committed to, and we know the initial document
    486      // will persist. In that case, we need to call onWindowGlobalCreated again.
    487      subject.addEventListener("DOMContentLoaded", handleEvent, {
    488        capture: true,
    489        once: true,
    490      });
    491    }
    492    onWindowGlobalCreated(subject);
    493  } else if (topic == "inner-window-destroyed") {
    494    const innerWindowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
    495    onWindowGlobalDestroyed(innerWindowId);
    496  } else if (topic == "content-page-shown" || topic == "chrome-page-shown") {
    497    // The observer service notification doesn't receive the "persisted" DOM Event attribute,
    498    // but thanksfully is fired just before the dispatching of that DOM event.
    499    subject.defaultView.addEventListener("pageshow", handleEvent, {
    500      capture: true,
    501      once: true,
    502    });
    503  } else if (topic == "content-page-hidden" || topic == "chrome-page-hidden") {
    504    // Same as previous elseif branch
    505    subject.defaultView.addEventListener("pagehide", handleEvent, {
    506      capture: true,
    507      once: true,
    508    });
    509  } else if (topic == "initial-document-element-inserted") {
    510    // We may be notified about SVG documents which we don't care about here.
    511    if (!subject.location || !subject.defaultView) {
    512      return;
    513    }
    514    // We might have ignored the DOMWindowCreated event because it was the initial about:blank document.
    515    // But when loading same-process iframes, we reuse the WindowGlobal of the previously ignored about:bank document
    516    // to load the actual URL loaded in the iframe. This means we won't have a new DOMWindowCreated
    517    // for the actual document. But there is a DOMDocElementInserted fired just after, that we are processing here
    518    // to create a target for same-process iframes. We only have to tell onWindowGlobalCreated to ignore
    519    // the call if a target was created on the DOMWindowCreated event (if that was a non-about:blank document).
    520    //
    521    // All this means that we still do not create any target for the initial documents.
    522    // It is complex to instantiate targets for initial documents because:
    523    // - it would mean spawning two targets for the same WindowGlobal and sharing the same innerWindowId
    524    // - or have WindowGlobalTargets to handle more than one document (it would mean reusing will-navigate/window-ready events
    525    // both on client and server)
    526    onWindowGlobalCreated(subject.defaultView, { ignoreIfExisting: true });
    527  }
    528 }
    529 
    530 /**
    531 * DOM Event handler.
    532 *
    533 * @param {string} type
    534 *        DOM event name
    535 * @param {boolean} persisted
    536 *        A flag set to true in cache of BFCache navigation
    537 * @param {Document} target
    538 *        The navigating document
    539 */
    540 function handleEvent({ type, persisted, target }) {
    541  // If persisted=true, this is a BFCache navigation.
    542  //
    543  // With Fission enabled and bfcacheInParent, BFCache navigations will spawn a new DocShell
    544  // in the same process:
    545  // * the previous page won't be destroyed, its JSWindowActor will stay alive (`didDestroy` won't be called)
    546  //   and a 'pagehide' with persisted=true will be emitted on it.
    547  // * the new page page won't emit any DOMWindowCreated, but instead a pageshow with persisted=true
    548  //   will be emitted.
    549  if (type === "pageshow" && persisted) {
    550    // Notify all bfcache navigations, even the one for which we don't create a new target
    551    // as that's being useful for parent process storage resource watchers.
    552    for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) {
    553      watcherDataObject.jsProcessActor.sendAsyncMessage(
    554        "DevToolsProcessChild:bf-cache-navigation-pageshow",
    555        {
    556          browsingContextId: target.defaultView.browsingContext.id,
    557        }
    558      );
    559    }
    560 
    561    // Here we are going to re-instantiate a target that got destroyed before while processing a pagehide event.
    562    // We force instantiating a new top level target, within `instantiate()` even if server targets are disabled.
    563    // But we only do that if bfcacheInParent is enabled. Otherwise for same-process, same-docshell bfcache navigation,
    564    // we don't want to spawn new targets.
    565    onWindowGlobalCreated(target.defaultView, {
    566      isBFCache: true,
    567    });
    568  }
    569 
    570  if (type === "pagehide" && persisted) {
    571    // Notify all bfcache navigations, even the one for which we don't create a new target
    572    // as that's being useful for parent process storage resource watchers.
    573    for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) {
    574      watcherDataObject.jsProcessActor.sendAsyncMessage(
    575        "DevToolsProcessChild:bf-cache-navigation-pagehide",
    576        {
    577          browsingContextId: target.defaultView.browsingContext.id,
    578        }
    579      );
    580    }
    581 
    582    // We might navigate away for the first top level target,
    583    // which isn't using JSWindowActor (it still uses messages manager and is created by the client, via TabDescriptor.getTarget).
    584    // We have to unregister it from the TargetActorRegistry, otherwise,
    585    // if we navigate back to it, the next DOMWindowCreated won't create a new target for it.
    586    onWindowGlobalDestroyed(target.defaultView.windowGlobalChild.innerWindowId);
    587  }
    588 
    589  if (type == "DOMContentLoaded") {
    590    if (!target.isInitialDocument) {
    591      return;
    592    }
    593 
    594    // This is similar to initial-document-element-inserted. onWindowGlobalCreated likely
    595    // ignored the earlier call for this document because it was the uncommitted initial one. Now
    596    // that we got a load event we know that the document is not transient but the destination of a
    597    // load. Its state will have changed and onWindowGlobalCreated won't skip it anymore.
    598    onWindowGlobalCreated(target.defaultView, {
    599      ignoreIfExisting: true,
    600    });
    601  }
    602 }
    603 
    604 /**
    605 * Return an existing Window Global target for given a WatcherActor
    606 * and against a given WindowGlobal.
    607 *
    608 * @param {object} options
    609 * @param {string} options.watcherDataObject
    610 * @param {number} options.innerWindowId
    611 *                 The WindowGlobal inner window ID.
    612 *
    613 * @returns {WindowGlobalTargetActor|null}
    614 */
    615 function findTargetActor({
    616  watcherDataObject,
    617  innerWindowId,
    618  browsingContextID,
    619 }) {
    620  // First let's check if a target was created for this watcher actor in this specific
    621  // DevToolsProcessChild instance.
    622  //
    623  // And start by checking if there is a perfect match first by doing a WindowGlobal / innerWindowId lookup,
    624  // before falling back to a BrowsingContext / browsingContextID lookup.
    625  let targetActor = watcherDataObject.actors.find(
    626    actor => actor.innerWindowId == innerWindowId
    627  );
    628  if (!targetActor && browsingContextID) {
    629    targetActor = watcherDataObject.actors.find(
    630      actor => actor.browsingContextID == browsingContextID
    631    );
    632  }
    633  if (targetActor) {
    634    return targetActor;
    635  }
    636 
    637  // Ensure retrieving the one target actor related to this connection.
    638  // This allows to distinguish actors created for various toolboxes.
    639  // For ex, regular toolbox versus browser console versus browser toolbox
    640  const connectionPrefix = watcherDataObject.watcherActorID.replace(
    641    /watcher\d+$/,
    642    ""
    643  );
    644  const targetActors = lazy.TargetActorRegistry.getTargetActors(
    645    watcherDataObject.sessionContext,
    646    connectionPrefix
    647  );
    648 
    649  return targetActors.find(actor => actor.innerWindowId == innerWindowId);
    650 }
    651 
    652 export const WindowGlobalTargetWatcher = {
    653  watch,
    654  unwatch,
    655  createTargetsForWatcher,
    656  destroyTargetsForWatcher,
    657 };