tor-browser

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

watcher.js (33818B)


      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 const { Actor } = require("resource://devtools/shared/protocol.js");
      7 const { watcherSpec } = require("resource://devtools/shared/specs/watcher.js");
      8 
      9 const Resources = require("resource://devtools/server/actors/resources/index.js");
     10 const { TargetActorRegistry } = ChromeUtils.importESModule(
     11  "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
     12  { global: "shared" }
     13 );
     14 const { ParentProcessWatcherRegistry } = ChromeUtils.importESModule(
     15  "resource://devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs",
     16  // ParentProcessWatcherRegistry needs to be a true singleton and loads ActorManagerParent
     17  // which also has to be a true singleton.
     18  { global: "shared" }
     19 );
     20 const { getAllBrowsingContextsForContext } = ChromeUtils.importESModule(
     21  "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
     22  { global: "contextual" }
     23 );
     24 const {
     25  SESSION_TYPES,
     26 } = require("resource://devtools/server/actors/watcher/session-context.js");
     27 
     28 loader.lazyRequireGetter(
     29  this,
     30  "throttle",
     31  "resource://devtools/shared/throttle.js",
     32  true
     33 );
     34 loader.lazyRequireGetter(
     35  this,
     36  "NetworkParentActor",
     37  "resource://devtools/server/actors/network-monitor/network-parent.js",
     38  true
     39 );
     40 loader.lazyRequireGetter(
     41  this,
     42  "BlackboxingActor",
     43  "resource://devtools/server/actors/blackboxing.js",
     44  true
     45 );
     46 loader.lazyRequireGetter(
     47  this,
     48  "BreakpointListActor",
     49  "resource://devtools/server/actors/breakpoint-list.js",
     50  true
     51 );
     52 loader.lazyRequireGetter(
     53  this,
     54  "TargetConfigurationActor",
     55  "resource://devtools/server/actors/target-configuration.js",
     56  true
     57 );
     58 loader.lazyRequireGetter(
     59  this,
     60  "ThreadConfigurationActor",
     61  "resource://devtools/server/actors/thread-configuration.js",
     62  true
     63 );
     64 
     65 const RESOURCES_THROTTLING_DELAY = 100;
     66 
     67 exports.WatcherActor = class WatcherActor extends Actor {
     68  /**
     69   * Initialize a new WatcherActor which is the main entry point to debug
     70   * something. The main features of this actor are to:
     71   * - observe targets related to the context we are debugging.
     72   *   This is done via watchTargets/unwatchTargets methods, and
     73   *   target-available-form/target-destroyed-form events.
     74   * - observe resources related to the observed targets.
     75   *   This is done via watchResources/unwatchResources methods, and
     76   *   resources-available-array/resources-updated-array/resources-destroyed-array events.
     77   *   Note that these events are also emited on both the watcher actor,
     78   *   for resources observed from the parent process, as well as on the
     79   *   target actors, when the resources are observed from the target's process or thread.
     80   *
     81   * @param {DevToolsServerConnection} conn
     82   *        The connection to use in order to communicate back to the client.
     83   * @param {object} sessionContext
     84   *        The Session Context to help know what is debugged.
     85   *        See devtools/server/actors/watcher/session-context.js
     86   * @param {number} sessionContext.browserId: If this is a "browser-element" context type,
     87   *        the "browserId" of the <browser> element we would like to debug.
     88   * @param {boolean} sessionContext.isServerTargetSwitchingEnabled: Flag to to know if we should
     89   *        spawn new top level targets for the debugged context.
     90   */
     91  constructor(conn, sessionContext) {
     92    super(conn, watcherSpec);
     93    this._sessionContext = sessionContext;
     94    if (sessionContext.type == SESSION_TYPES.BROWSER_ELEMENT) {
     95      // Retrieve the <browser> element for the given browser ID
     96      const browsingContext = BrowsingContext.getCurrentTopByBrowserId(
     97        sessionContext.browserId
     98      );
     99      if (!browsingContext) {
    100        throw new Error(
    101          "Unable to retrieve the <browser> element for browserId=" +
    102            sessionContext.browserId
    103        );
    104      }
    105      this._browserElement = browsingContext.embedderElement;
    106    }
    107 
    108    this.watcherConnectionPrefix = conn.allocID("watcher");
    109 
    110    // Lists of resources available/updated/destroyed RDP packet
    111    // currently queued which will be emitted after a throttle delay.
    112    this.#throttledResources = {
    113      available: [],
    114      updated: [],
    115      destroyed: [],
    116    };
    117 
    118    this.#throttledEmitResources = throttle(
    119      this.emitResources.bind(this),
    120      RESOURCES_THROTTLING_DELAY
    121    );
    122 
    123    // Sometimes we get iframe targets before the top-level targets
    124    // mostly when doing bfcache navigations, lets cache the early iframes targets and
    125    // flush them after the top-level target is available. See Bug 1726568 for details.
    126    this._earlyIframeTargets = {};
    127 
    128    // All currently available WindowGlobal target's form, keyed by `innerWindowId`.
    129    //
    130    // This helps to:
    131    // - determine if the iframe targets are early or not.
    132    //   i.e. if it is notified before its parent target is available.
    133    // - notify the destruction of all children targets when a parent is destroyed.
    134    //   i.e. have a reliable order of destruction between parent and children.
    135    //
    136    // Note that there should be just one top-level window target at a time,
    137    // but there are certain cases when a new target is available before the
    138    // old target is destroyed.
    139    this._currentWindowGlobalTargets = new Map();
    140 
    141    // The Browser Toolbox requires to load modules in a distinct compartment in order
    142    // to be able to debug system compartments modules (most of Firefox internal codebase).
    143    // This is a requirement coming from SpiderMonkey Debugger API and relates to the thread actor.
    144    this._jsActorName =
    145      sessionContext.type == SESSION_TYPES.ALL
    146        ? "BrowserToolboxDevToolsProcess"
    147        : "DevToolsProcess";
    148  }
    149 
    150  #throttledResources;
    151  #throttledEmitResources;
    152 
    153  get sessionContext() {
    154    return this._sessionContext;
    155  }
    156 
    157  /**
    158   * If we are debugging only one Tab or Document, returns its BrowserElement.
    159   * For Tabs, it will be the <browser> element used to load the web page.
    160   *
    161   * This is typicaly used to fetch:
    162   * - its `browserId` attribute, which uniquely defines it,
    163   * - its `browsingContextID` or `browsingContext`, which helps inspecting its content.
    164   */
    165  get browserElement() {
    166    return this._browserElement;
    167  }
    168 
    169  getAllBrowsingContexts() {
    170    return getAllBrowsingContextsForContext(this.sessionContext);
    171  }
    172 
    173  /**
    174   * Helper to know if the context we are debugging has been already destroyed
    175   */
    176  isContextDestroyed() {
    177    if (this.sessionContext.type == "browser-element") {
    178      return !this.browserElement.browsingContext;
    179    } else if (this.sessionContext.type == "webextension") {
    180      // This is no obvious browsing context to target for extensions, so always consider it running
    181      return false;
    182    } else if (this.sessionContext.type == "all") {
    183      return false;
    184    }
    185    throw new Error(
    186      "Unsupported session context type: " + this.sessionContext.type
    187    );
    188  }
    189 
    190  destroy() {
    191    // Only try to notify content processes if the watcher was in the registry.
    192    // Otherwise it means that it wasn't connected to any process and the JS Process Actor
    193    // wouldn't be registered.
    194    if (ParentProcessWatcherRegistry.getWatcher(this.actorID)) {
    195      // Emit one IPC message on destroy to all the processes
    196      const domProcesses = ChromeUtils.getAllDOMProcesses();
    197      for (const domProcess of domProcesses) {
    198        domProcess.getActor(this._jsActorName).destroyWatcher({
    199          watcherActorID: this.actorID,
    200        });
    201      }
    202    }
    203 
    204    // Ensure destroying all Resource Watcher instantiated in the parent process
    205    Resources.unwatchResources(
    206      this,
    207      Resources.getParentProcessResourceTypes(Object.values(Resources.TYPES))
    208    );
    209 
    210    ParentProcessWatcherRegistry.unregisterWatcher(this.actorID);
    211 
    212    // In case the watcher actor is leaked, prevent leaking the browser window
    213    this._browserElement = null;
    214 
    215    // Destroy the actor in order to ensure destroying all its children actors.
    216    // As this actor is a pool with children actors, when the transport/connection closes
    217    // we expect all actors and its children to be destroyed.
    218    super.destroy();
    219  }
    220 
    221  /**
    222   * Get the list of the currently watched resources for this watcher.
    223   *
    224   * @return Array<String>
    225   *         Returns the list of currently watched resource types.
    226   */
    227  get sessionData() {
    228    return ParentProcessWatcherRegistry.getSessionData(this);
    229  }
    230 
    231  form() {
    232    return {
    233      actor: this.actorID,
    234      // The resources and target traits should be removed all at the same time since the
    235      // client has generic ways to deal with all of them (See Bug 1680280).
    236      traits: {
    237        ...this.sessionContext.supportedTargets,
    238        resources: this.sessionContext.supportedResources,
    239      },
    240    };
    241  }
    242 
    243  /**
    244   * Start watching for a new target type.
    245   *
    246   * This will instantiate Target Actors for existing debugging context of this type,
    247   * but will also create actors as context of this type get created.
    248   * The actors are notified to the client via "target-available-form" RDP events.
    249   * We also notify about target actors destruction via "target-destroyed-form".
    250   * Note that we are guaranteed to receive all existing target actor by the time this method
    251   * resolves.
    252   *
    253   * @param {string} targetType
    254   *        Type of context to observe. See Targets.TYPES object.
    255   */
    256  async watchTargets(targetType) {
    257    ParentProcessWatcherRegistry.watchTargets(this, targetType);
    258 
    259    // When debugging a tab, ensure processing the top level target first
    260    // (for now, other session context types are instantiating the top level target
    261    // from the descriptor's getTarget method instead of the Watcher)
    262    let topLevelTargetProcess;
    263    if (this.sessionContext.type == SESSION_TYPES.BROWSER_ELEMENT) {
    264      topLevelTargetProcess =
    265        this.browserElement.browsingContext.currentWindowGlobal?.domProcess;
    266      if (topLevelTargetProcess) {
    267        await topLevelTargetProcess.getActor(this._jsActorName).watchTargets({
    268          watcherActorID: this.actorID,
    269          targetType,
    270        });
    271        // Stop execution if we were destroyed in the meantime
    272        if (this.isDestroyed()) {
    273          return;
    274        }
    275      }
    276    }
    277 
    278    // We have to reach out all the content processes as the page may navigate
    279    // to any other content process when navigating to another origin.
    280    // It may even run in the parent process when loading about:robots.
    281    const domProcesses = ChromeUtils.getAllDOMProcesses();
    282    const promises = [];
    283    for (const domProcess of domProcesses) {
    284      if (domProcess == topLevelTargetProcess) {
    285        continue;
    286      }
    287      promises.push(
    288        domProcess
    289          .getActor(this._jsActorName)
    290          .watchTargets({
    291            watcherActorID: this.actorID,
    292            targetType,
    293          })
    294          .catch(e => {
    295            // Ignore any process that got destroyed while trying to send the request
    296            if (!domProcess.canSend) {
    297              console.warn(
    298                "Content process closed while requesting targets",
    299                domProcess.name,
    300                domProcess.remoteType
    301              );
    302              return;
    303            }
    304            throw e;
    305          })
    306      );
    307    }
    308    await Promise.all(promises);
    309  }
    310 
    311  /**
    312   * Stop watching for a given target type.
    313   *
    314   * @param {string} targetType
    315   *        Type of context to observe. See Targets.TYPES object.
    316   * @param {object} options
    317   * @param {boolean} options.isModeSwitching
    318   *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
    319   */
    320  unwatchTargets(targetType, options = {}) {
    321    const isWatchingTargets = ParentProcessWatcherRegistry.unwatchTargets(
    322      this,
    323      targetType
    324    );
    325    if (!isWatchingTargets) {
    326      return;
    327    }
    328 
    329    const domProcesses = ChromeUtils.getAllDOMProcesses();
    330    for (const domProcess of domProcesses) {
    331      domProcess.getActor(this._jsActorName).unwatchTargets({
    332        watcherActorID: this.actorID,
    333        targetType,
    334        options,
    335      });
    336    }
    337  }
    338 
    339  /**
    340   * Flush any early iframe targets relating to this top level
    341   * window target.
    342   *
    343   * @param {number} topInnerWindowID
    344   */
    345  _flushIframeTargets(topInnerWindowID) {
    346    while (this._earlyIframeTargets[topInnerWindowID]?.length > 0) {
    347      const actor = this._earlyIframeTargets[topInnerWindowID].shift();
    348      this.emit("target-available-form", actor);
    349    }
    350  }
    351 
    352  /**
    353   * Called by a Watcher module, whenever a new target is available
    354   */
    355  notifyTargetAvailable(actor) {
    356    // Emit immediately for worker, process & extension targets
    357    // as they don't have a parent browsing context.
    358    if (!actor.traits?.isBrowsingContext) {
    359      this.emit("target-available-form", actor);
    360      return;
    361    }
    362 
    363    // If isBrowsingContext trait is true, we are processing a WindowGlobalTarget.
    364    // (this trait should be renamed)
    365    this._currentWindowGlobalTargets.set(actor.innerWindowId, actor);
    366 
    367    // The top-level is always the same for the browser-toolbox
    368    if (this.sessionContext.type == "all") {
    369      this.emit("target-available-form", actor);
    370      return;
    371    }
    372 
    373    if (actor.isTopLevelTarget) {
    374      this.emit("target-available-form", actor);
    375      // Flush any existing early iframe targets
    376      this._flushIframeTargets(actor.innerWindowId);
    377 
    378      if (this.sessionContext.type == SESSION_TYPES.BROWSER_ELEMENT) {
    379        // Ignore any pending exception as this request may be pending
    380        // while the toolbox closes. And we don't want to delay target emission
    381        // on this as this is a implementation detail.
    382        this.updateDomainSessionDataForServiceWorkers(actor.url).catch(
    383          () => {}
    384        );
    385      }
    386    } else if (this._currentWindowGlobalTargets.has(actor.topInnerWindowId)) {
    387      // Emit the event immediately if the top-level target is already available
    388      this.emit("target-available-form", actor);
    389    } else if (this._earlyIframeTargets[actor.topInnerWindowId]) {
    390      // Add the early iframe target to the list of other early targets.
    391      this._earlyIframeTargets[actor.topInnerWindowId].push(actor);
    392    } else {
    393      // Set the first early iframe target
    394      this._earlyIframeTargets[actor.topInnerWindowId] = [actor];
    395    }
    396  }
    397 
    398  /**
    399   * Called by a Watcher module, whenever a target has been destroyed
    400   *
    401   * @param {object} actor
    402   *        the actor form of the target being destroyed
    403   * @param {object} options
    404   * @param {boolean} options.isModeSwitching
    405   *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
    406   */
    407  async notifyTargetDestroyed(actor, options = {}) {
    408    // Emit immediately for worker, process & extension targets
    409    // as they don't have a parent browsing context.
    410    if (!actor.innerWindowId) {
    411      this.emit("target-destroyed-form", actor, options);
    412      return;
    413    }
    414    // Flush all iframe targets if we are destroying a top level target.
    415    if (actor.isTopLevelTarget) {
    416      // First compute the list of children actors, as notifyTargetDestroy will mutate _currentWindowGlobalTargets
    417      const childrenActors = [
    418        ...this._currentWindowGlobalTargets.values(),
    419      ].filter(
    420        form =>
    421          form.topInnerWindowId == actor.innerWindowId &&
    422          // Ignore the top level target itself, because its topInnerWindowId will be its innerWindowId
    423          form.innerWindowId != actor.innerWindowId
    424      );
    425      childrenActors.map(form => this.notifyTargetDestroyed(form, options));
    426    }
    427    if (this._earlyIframeTargets[actor.innerWindowId]) {
    428      delete this._earlyIframeTargets[actor.innerWindowId];
    429    }
    430    this._currentWindowGlobalTargets.delete(actor.innerWindowId);
    431    const documentEventWatcher = Resources.getResourceWatcher(
    432      this,
    433      Resources.TYPES.DOCUMENT_EVENT
    434    );
    435    // If we have a Watcher class instantiated, ensure that target-destroyed is sent
    436    // *after* DOCUMENT_EVENT's will-navigate. Otherwise this resource will have an undefined
    437    // `targetFront` attribute, as it is associated with the target from which we navigate
    438    // and not the one we navigate to.
    439    //
    440    // About documentEventWatcher check: We won't have any watcher class if we aren't
    441    // using server side Watcher classes.
    442    // i.e. when we are using the legacy listener for DOCUMENT_EVENT.
    443    // This is still the case for all toolboxes but the one for local and remote tabs.
    444    //
    445    // About isServerTargetSwitchingEnabled check: if we are using the watcher class
    446    // we may still use client side target, which will still use legacy listeners for
    447    // will-navigate and so will-navigate will be emitted by the target actor itself.
    448    //
    449    // About isTopLevelTarget check: only top level targets emit will-navigate,
    450    // so there is no reason to delay target-destroy for remote iframes.
    451    if (
    452      documentEventWatcher &&
    453      this.sessionContext.isServerTargetSwitchingEnabled &&
    454      actor.isTopLevelTarget
    455    ) {
    456      await documentEventWatcher.onceWillNavigateIsEmitted(actor.innerWindowId);
    457    }
    458    this.emit("target-destroyed-form", actor, options);
    459  }
    460 
    461  /**
    462   * Given a browsingContextID, returns its parent browsingContextID. Returns null if a
    463   * parent browsing context couldn't be found. Throws if the browsing context
    464   * corresponding to the passed browsingContextID couldn't be found.
    465   *
    466   * @param {Integer} browsingContextID
    467   * @returns {Integer|null}
    468   */
    469  getParentBrowsingContextID(browsingContextID) {
    470    const browsingContext = BrowsingContext.get(browsingContextID);
    471    if (!browsingContext) {
    472      throw new Error(
    473        `BrowsingContext with ID=${browsingContextID} doesn't exist.`
    474      );
    475    }
    476    // Top-level documents of tabs, loaded in a <browser> element expose a null `parent`.
    477    // i.e. Their BrowsingContext has no parent and is considered top level.
    478    // But... in the context of the Browser Toolbox, we still consider them as child of the browser window.
    479    // So, for them, fallback on `embedderWindowGlobal`, which will typically be the WindowGlobal for browser.xhtml.
    480    if (browsingContext.parent) {
    481      return browsingContext.parent.id;
    482    }
    483    if (browsingContext.embedderWindowGlobal) {
    484      return browsingContext.embedderWindowGlobal.browsingContext.id;
    485    }
    486    return null;
    487  }
    488 
    489  /**
    490   * Called by Resource Watchers, when new resources are available, updated or destroyed.
    491   *
    492   * @param String updateType
    493   *        Can be "available", "updated" or "destroyed"
    494   * @param String resourceType
    495   *        The type of resources to be notified about.
    496   * @param Array<json> resources
    497   *        List of all resource's form. A resource is a JSON object piped over to the client.
    498   *        It can contain actor IDs, actor forms, to be manually marshalled by the client.
    499   */
    500  notifyResources(updateType, resourceType, resources) {
    501    if (resources.length === 0) {
    502      // Don't try to emit if the resources array is empty.
    503      return;
    504    }
    505 
    506    const shouldEmitSynchronously =
    507      resourceType == Resources.TYPES.DOCUMENT_EVENT &&
    508      resources.some(resource => resource.name == "will-navigate");
    509 
    510    // If the last throttled resources were of the same resource type,
    511    // augment the resources array with the new resources
    512    const lastResourceInThrottleCache =
    513      this.#throttledResources[updateType].at(-1);
    514    if (
    515      lastResourceInThrottleCache &&
    516      lastResourceInThrottleCache[0] === resourceType
    517    ) {
    518      lastResourceInThrottleCache[1].push.apply(
    519        lastResourceInThrottleCache[1],
    520        resources
    521      );
    522    } else {
    523      // Otherwise, add a new item in the throttle queue with the resource type
    524      this.#throttledResources[updateType].push([resourceType, resources]);
    525    }
    526 
    527    // Force firing resources immediately when the DOCUMENT_EVENT's will-navigate is received
    528    // This will force clearing resources on the client side ASAP.
    529    // Otherwise we might emit some other RDP event (outside of resources),
    530    // which will be cleared by the throttled/delayed will-navigate.
    531    if (shouldEmitSynchronously) {
    532      this.emitResources();
    533    } else {
    534      this.#throttledEmitResources();
    535    }
    536  }
    537 
    538  /**
    539   * Flush resources to DevTools transport layer, actually sending all resource update packets
    540   */
    541  emitResources() {
    542    if (this.isDestroyed()) {
    543      return;
    544    }
    545    for (const updateType of ["available", "updated", "destroyed"]) {
    546      const resources = this.#throttledResources[updateType];
    547      if (!resources.length) {
    548        continue;
    549      }
    550      this.#throttledResources[updateType] = [];
    551      this.emit(`resources-${updateType}-array`, resources);
    552    }
    553  }
    554 
    555  /**
    556   * Try to retrieve Target Actors instantiated in the parent process which aren't
    557   * instantiated via the Watcher actor (and its dependencies):
    558   * - top level target for the browser toolboxes
    559   * - xpcshell targets for xpcshell debugging
    560   *
    561   * See comment in `watchResources`.
    562   *
    563   * @return {Set<TargetActor>} Matching target actors.
    564   */
    565  getTargetActorsInParentProcess() {
    566    if (TargetActorRegistry.xpcShellTargetActors.size) {
    567      return TargetActorRegistry.xpcShellTargetActors;
    568    }
    569 
    570    // Note: For browser-element debugging, the WindowGlobalTargetActor returned here is created
    571    // for a parent process page and lives in the parent process.
    572    const actors = TargetActorRegistry.getTargetActors(
    573      this.sessionContext,
    574      // Note that we aren't using watcherConnectionPrefix as the ParentProcessTargetActor
    575      // are registered in `this.conn` (i.e The connection which is bound to the client)
    576      // directly and not in the DevToolsServerConnection running in the content process with `watcherConnectionPrefix`
    577      this.conn.prefix
    578    );
    579 
    580    switch (this.sessionContext.type) {
    581      case "all": {
    582        const parentProcessTargetActor = actors.find(
    583          actor => actor.typeName === "parentProcessTarget"
    584        );
    585        if (parentProcessTargetActor) {
    586          return new Set([parentProcessTargetActor]);
    587        }
    588        return new Set();
    589      }
    590      case "browser-element":
    591      case "webextension":
    592        // All target actors for browser-element and webextension sessions
    593        // should be created using the JS Window actors.
    594        return new Set();
    595      default:
    596        throw new Error(
    597          "Unsupported session context type: " + this.sessionContext.type
    598        );
    599    }
    600  }
    601 
    602  /**
    603   * Start watching for a list of resource types.
    604   * This should only resolve once all "already existing" resources of these types
    605   * are notified to the client via resources-available-array event on related target actors.
    606   *
    607   * @param {Array<string>} resourceTypes
    608   *        List of all types to listen to.
    609   */
    610  async watchResources(resourceTypes) {
    611    // First process resources which have to be listened from the parent process
    612    // (the watcher actor always runs in the parent process)
    613    await Resources.watchResources(
    614      this,
    615      Resources.getParentProcessResourceTypes(resourceTypes)
    616    );
    617 
    618    // Bail out early if all resources were watched from parent process.
    619    // In this scenario, we do not need to update these resource types in the ParentProcessWatcherRegistry
    620    // as targets do not care about them.
    621    if (!Resources.hasResourceTypesForTargets(resourceTypes)) {
    622      return;
    623    }
    624 
    625    ParentProcessWatcherRegistry.watchResources(this, resourceTypes);
    626 
    627    const promises = [];
    628    const domProcesses = ChromeUtils.getAllDOMProcesses();
    629    for (const domProcess of domProcesses) {
    630      promises.push(
    631        domProcess
    632          .getActor(this._jsActorName)
    633          .addOrSetSessionDataEntry({
    634            watcherActorID: this.actorID,
    635            sessionContext: this.sessionContext,
    636            type: "resources",
    637            entries: resourceTypes,
    638            updateType: "add",
    639          })
    640          .catch(e => {
    641            // Ignore any process that got destroyed while trying to send the request
    642            if (!domProcess.canSend) {
    643              console.warn(
    644                "Content process closed while requesting resources",
    645                domProcess.name,
    646                domProcess.remoteType
    647              );
    648              return;
    649            }
    650            throw e;
    651          })
    652      );
    653    }
    654    await Promise.all(promises);
    655 
    656    // Stop execution if we were destroyed in the meantime
    657    if (this.isDestroyed()) {
    658      return;
    659    }
    660 
    661    /*
    662     * The Watcher actor doesn't support watching the top level target
    663     * (bug 1644397 and possibly some other followup).
    664     *
    665     * Because of that, we miss reaching these targets in the previous lines of this function.
    666     * Since all BrowsingContext target actors register themselves to the TargetActorRegistry,
    667     * we use it here in order to reach those missing targets, which are running in the
    668     * parent process (where this WatcherActor lives as well):
    669     *  - the parent process target (which inherits from WindowGlobalTargetActor)
    670     *  - top level tab target for documents loaded in the parent process (e.g. about:robots).
    671     *    When the tab loads document in the content process, the FrameTargetHelper will
    672     *    reach it via the JSWindowActor API. Even if it uses MessageManager for anything
    673     *    else (RDP packet forwarding, creation and destruction).
    674     *
    675     * We will eventually get rid of this code once all targets are properly supported by
    676     * the Watcher Actor and we have target helpers for all of them.
    677     */
    678    const targetActors = this.getTargetActorsInParentProcess();
    679    for (const targetActor of targetActors) {
    680      const targetActorResourceTypes = Resources.getResourceTypesForTargetType(
    681        resourceTypes,
    682        targetActor.targetType
    683      );
    684      await targetActor.addOrSetSessionDataEntry(
    685        "resources",
    686        targetActorResourceTypes,
    687        false,
    688        "add"
    689      );
    690    }
    691  }
    692 
    693  /**
    694   * Stop watching for a list of resource types.
    695   *
    696   * @param {Array<string>} resourceTypes
    697   *        List of all types to listen to.
    698   */
    699  unwatchResources(resourceTypes) {
    700    // First process resources which are listened from the parent process
    701    // (the watcher actor always runs in the parent process)
    702    Resources.unwatchResources(
    703      this,
    704      Resources.getParentProcessResourceTypes(resourceTypes)
    705    );
    706 
    707    // Bail out early if all resources were all watched from parent process.
    708    // In this scenario, we do not need to update these resource types in the ParentProcessWatcherRegistry
    709    // as targets do not care about them.
    710    if (!Resources.hasResourceTypesForTargets(resourceTypes)) {
    711      return;
    712    }
    713 
    714    const isWatchingResources = ParentProcessWatcherRegistry.unwatchResources(
    715      this,
    716      resourceTypes
    717    );
    718    if (!isWatchingResources) {
    719      return;
    720    }
    721 
    722    // Prevent trying to unwatch when the related BrowsingContext has already
    723    // been destroyed
    724    if (!this.isContextDestroyed()) {
    725      const domProcesses = ChromeUtils.getAllDOMProcesses();
    726      for (const domProcess of domProcesses) {
    727        domProcess.getActor(this._jsActorName).removeSessionDataEntry({
    728          watcherActorID: this.actorID,
    729          sessionContext: this.sessionContext,
    730          type: "resources",
    731          entries: resourceTypes,
    732        });
    733      }
    734    }
    735 
    736    // See comment in watchResources.
    737    const targetActors = this.getTargetActorsInParentProcess();
    738    for (const targetActor of targetActors) {
    739      const targetActorResourceTypes = Resources.getResourceTypesForTargetType(
    740        resourceTypes,
    741        targetActor.targetType
    742      );
    743      targetActor.removeSessionDataEntry("resources", targetActorResourceTypes);
    744    }
    745  }
    746 
    747  clearResources(resourceTypes) {
    748    // First process resources which have to be listened from the parent process
    749    // (the watcher actor always runs in the parent process)
    750    // TODO: content process / worker thread resources are not cleared. See Bug 1774573
    751    Resources.clearResources(
    752      this,
    753      Resources.getParentProcessResourceTypes(resourceTypes)
    754    );
    755  }
    756 
    757  /**
    758   * Returns the network actor.
    759   *
    760   * @return {object} actor
    761   *        The network actor.
    762   */
    763  getNetworkParentActor() {
    764    if (!this._networkParentActor) {
    765      this._networkParentActor = new NetworkParentActor(this);
    766    }
    767 
    768    return this._networkParentActor;
    769  }
    770 
    771  /**
    772   * Returns the blackboxing actor.
    773   *
    774   * @return {object} actor
    775   *        The blackboxing actor.
    776   */
    777  getBlackboxingActor() {
    778    if (!this._blackboxingActor) {
    779      this._blackboxingActor = new BlackboxingActor(this);
    780    }
    781 
    782    return this._blackboxingActor;
    783  }
    784 
    785  /**
    786   * Returns the breakpoint list actor.
    787   *
    788   * @return {object} actor
    789   *        The breakpoint list actor.
    790   */
    791  getBreakpointListActor() {
    792    if (!this._breakpointListActor) {
    793      this._breakpointListActor = new BreakpointListActor(this);
    794    }
    795 
    796    return this._breakpointListActor;
    797  }
    798 
    799  /**
    800   * Returns the target configuration actor.
    801   *
    802   * @return {object} actor
    803   *        The configuration actor.
    804   */
    805  getTargetConfigurationActor() {
    806    if (!this._targetConfigurationListActor) {
    807      this._targetConfigurationListActor = new TargetConfigurationActor(this);
    808    }
    809    return this._targetConfigurationListActor;
    810  }
    811 
    812  /**
    813   * Returns the thread configuration actor.
    814   *
    815   * @return {object} actor
    816   *        The configuration actor.
    817   */
    818  getThreadConfigurationActor() {
    819    if (!this._threadConfigurationListActor) {
    820      this._threadConfigurationListActor = new ThreadConfigurationActor(this);
    821    }
    822    return this._threadConfigurationListActor;
    823  }
    824 
    825  /**
    826   * Server internal API, called by other actors, but not by the client.
    827   * Used to agrement some new entries for a given data type (watchers target, resources,
    828   * breakpoints,...)
    829   *
    830   * @param {string} type
    831   *        Data type to contribute to.
    832   * @param {Array<*>} entries
    833   *        List of values to add or set for this data type.
    834   * @param {string} updateType
    835   *        "add" will only add the new entries in the existing data set.
    836   *        "set" will update the data set with the new entries.
    837   */
    838  async addOrSetDataEntry(type, entries, updateType) {
    839    ParentProcessWatcherRegistry.addOrSetSessionDataEntry(
    840      this,
    841      type,
    842      entries,
    843      updateType
    844    );
    845 
    846    const promises = [];
    847    const domProcesses = ChromeUtils.getAllDOMProcesses();
    848    for (const domProcess of domProcesses) {
    849      promises.push(
    850        domProcess
    851          .getActor(this._jsActorName)
    852          .addOrSetSessionDataEntry({
    853            watcherActorID: this.actorID,
    854            sessionContext: this.sessionContext,
    855            type,
    856            entries,
    857            updateType,
    858          })
    859          .catch(e => {
    860            // Ignore any process that got destroyed while trying to send the request
    861            if (!domProcess.canSend) {
    862              console.warn(
    863                "Content process closed while sending session data",
    864                domProcess.name,
    865                domProcess.remoteType
    866              );
    867              return;
    868            }
    869            throw e;
    870          })
    871      );
    872    }
    873    await Promise.all(promises);
    874 
    875    // Stop execution if we were destroyed in the meantime
    876    if (this.isDestroyed()) {
    877      return;
    878    }
    879 
    880    // See comment in watchResources
    881    const targetActors = this.getTargetActorsInParentProcess();
    882    for (const targetActor of targetActors) {
    883      await targetActor.addOrSetSessionDataEntry(
    884        type,
    885        entries,
    886        false,
    887        updateType
    888      );
    889    }
    890  }
    891 
    892  /**
    893   * Server internal API, called by other actors, but not by the client.
    894   * Used to remve some existing entries for a given data type (watchers target, resources,
    895   * breakpoints,...)
    896   *
    897   * @param {string} type
    898   *        Data type to modify.
    899   * @param {Array<*>} entries
    900   *        List of values to remove from this data type.
    901   */
    902  removeDataEntry(type, entries) {
    903    ParentProcessWatcherRegistry.removeSessionDataEntry(this, type, entries);
    904 
    905    const domProcesses = ChromeUtils.getAllDOMProcesses();
    906    for (const domProcess of domProcesses) {
    907      domProcess.getActor(this._jsActorName).removeSessionDataEntry({
    908        watcherActorID: this.actorID,
    909        sessionContext: this.sessionContext,
    910        type,
    911        entries,
    912      });
    913    }
    914 
    915    // See comment in addOrSetDataEntry
    916    const targetActors = this.getTargetActorsInParentProcess();
    917    for (const targetActor of targetActors) {
    918      targetActor.removeSessionDataEntry(type, entries);
    919    }
    920  }
    921 
    922  /**
    923   * Retrieve the current watched data for the provided type.
    924   *
    925   * @param {string} type
    926   *        Data type to retrieve.
    927   */
    928  getSessionDataForType(type) {
    929    return this.sessionData?.[type];
    930  }
    931 
    932  /**
    933   * Special code dedicated to Service Worker debugging.
    934   * This will notify the Service Worker JS Process Actors about the new top level page domain.
    935   * So that we start tracking that domain's workers.
    936   *
    937   * @param {string} newTargetUrl
    938   */
    939  async updateDomainSessionDataForServiceWorkers(newTargetUrl) {
    940    // If the url could not be parsed the host defaults to an empty string.
    941    const host = URL.parse(newTargetUrl)?.host ?? "";
    942 
    943    ParentProcessWatcherRegistry.addOrSetSessionDataEntry(
    944      this,
    945      "browser-element-host",
    946      [host],
    947      "set"
    948    );
    949 
    950    return this.addOrSetDataEntry("browser-element-host", [host], "set");
    951  }
    952 };