tor-browser

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

target-command.js (47850B)


      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 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
      8 
      9 const BROWSERTOOLBOX_SCOPE_PREF = "devtools.browsertoolbox.scope";
     10 // Possible values of the previous pref:
     11 const BROWSERTOOLBOX_SCOPE_EVERYTHING = "everything";
     12 const BROWSERTOOLBOX_SCOPE_PARENTPROCESS = "parent-process";
     13 
     14 const SHOW_CONTENT_SCRIPTS_PREF = "devtools.debugger.show-content-scripts";
     15 
     16 // eslint-disable-next-line mozilla/reject-some-requires
     17 const createStore = require("resource://devtools/client/shared/redux/create-store.js");
     18 const reducer = require("resource://devtools/shared/commands/target/reducers/targets.js");
     19 
     20 loader.lazyRequireGetter(
     21  this,
     22  ["refreshTargets", "registerTarget", "unregisterTarget"],
     23  "resource://devtools/shared/commands/target/actions/targets.js",
     24  true
     25 );
     26 
     27 class TargetCommand extends EventEmitter {
     28  #selectedTargetFront;
     29  /**
     30   * This class helps managing, iterating over and listening for Targets.
     31   *
     32   * It exposes:
     33   *  - the top level target, typically the main process target for the browser toolbox
     34   *    or the browsing context target for a regular web toolbox
     35   *  - target of remoted iframe, in case Fission is enabled and some <iframe>
     36   *    are running in a distinct process
     37   *  - target switching. If the top level target changes for a new one,
     38   *    all the targets are going to be declared as destroyed and the new ones
     39   *    will be notified to the user of this API.
     40   *
     41   * @fires target-tread-wrong-order-on-resume : An event that is emitted when resuming
     42   *        the thread throws with the "wrongOrder" error.
     43   *
     44   * @param {DescriptorFront} descriptorFront
     45   *        The context to inspector identified by this descriptor.
     46   * @param {WatcherFront} watcherFront
     47   *        If available, a reference to the related Watcher Front.
     48   * @param {object} commands
     49   *        The commands object with all interfaces defined from devtools/shared/commands/
     50   */
     51  constructor({ descriptorFront, watcherFront, commands }) {
     52    super();
     53 
     54    this.commands = commands;
     55    this.descriptorFront = descriptorFront;
     56    this.watcherFront = watcherFront;
     57    this.rootFront = descriptorFront.client.mainRoot;
     58 
     59    this.store = createStore(reducer);
     60    // Name of the store used when calling createProvider.
     61    this.storeId = "target-store";
     62 
     63    this._updateBrowserToolboxScope =
     64      this._updateBrowserToolboxScope.bind(this);
     65    this._updateContentScriptListening =
     66      this._updateContentScriptListening.bind(this);
     67 
     68    Services.prefs.addObserver(
     69      BROWSERTOOLBOX_SCOPE_PREF,
     70      this._updateBrowserToolboxScope
     71    );
     72    Services.prefs.addObserver(
     73      SHOW_CONTENT_SCRIPTS_PREF,
     74      this._updateContentScriptListening
     75    );
     76    // Until Watcher actor notify about new top level target when navigating to another process
     77    // we have to manually switch to a new target from the client side
     78    this.onLocalTabRemotenessChange =
     79      this.onLocalTabRemotenessChange.bind(this);
     80    if (this.descriptorFront.isTabDescriptor) {
     81      this.descriptorFront.on(
     82        "remoteness-change",
     83        this.onLocalTabRemotenessChange
     84      );
     85    }
     86 
     87    if (this.isServerTargetSwitchingEnabled()) {
     88      // XXX: Will only be used for local tab server side target switching if
     89      // the first target is generated from the server.
     90      this._onFirstTarget = new Promise(r => (this._resolveOnFirstTarget = r));
     91    }
     92 
     93    // Reports if we have at least one listener for the given target type
     94    this._listenersStarted = new Set();
     95 
     96    // List of all the target fronts
     97    this._targets = new Set();
     98    // {Map<Function, Set<targetFront>>} A Map keyed by `onAvailable` function passed to
     99    // `watchTargets`, whose initial value is a Set of the existing target fronts at the
    100    // time watchTargets is called.
    101    this._pendingWatchTargetInitialization = new Map();
    102 
    103    // Listeners for target creation, destruction and selection
    104    this._createListeners = new EventEmitter();
    105    this._destroyListeners = new EventEmitter();
    106    this._selectListeners = new EventEmitter();
    107 
    108    this._onTargetAvailable = this._onTargetAvailable.bind(this);
    109    this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
    110    this._onTargetSelected = this._onTargetSelected.bind(this);
    111    // Bug 1675763: Watcher actor is not available in all situations yet.
    112    if (this.watcherFront) {
    113      this.watcherFront.on("target-available", this._onTargetAvailable);
    114      this.watcherFront.on("target-destroyed", this._onTargetDestroyed);
    115    }
    116 
    117    this.legacyImplementation = {};
    118 
    119    // Public flag to allow listening for workers even if the fission pref is off
    120    // This allows listening for workers in the content toolbox outside of fission contexts
    121    // For now, this is only toggled by tests.
    122    this.listenForWorkers =
    123      this.rootFront.traits.workerConsoleApiMessagesDispatchedToMainThread ===
    124      false;
    125    this.listenForServiceWorkers = false;
    126    this.listenForContentScripts = false;
    127 
    128    // Tells us if we received the first top level target.
    129    // If target switching is done on:
    130    // * client side, this is done from startListening => _createFirstTarget
    131    //                and pull from the Descriptor front.
    132    // * server side, this is also done from startListening,
    133    //                but we wait for the watcher actor to notify us about it
    134    //                via target-available-form avent.
    135    this._gotFirstTopLevelTarget = false;
    136    this._onResourceAvailable = this._onResourceAvailable.bind(this);
    137  }
    138 
    139  get selectedTargetFront() {
    140    return this.#selectedTargetFront || this.targetFront;
    141  }
    142 
    143  /**
    144   * Called fired when BROWSERTOOLBOX_SCOPE_PREF pref changes.
    145   * This will enable/disable the full multiprocess debugging.
    146   * When enabled we will watch for content process targets and debug all the processes.
    147   * When disabled we will only watch for FRAME and WORKER and restrict ourself to parent process resources.
    148   */
    149  _updateBrowserToolboxScope() {
    150    const browserToolboxScope = Services.prefs.getCharPref(
    151      BROWSERTOOLBOX_SCOPE_PREF
    152    );
    153    if (browserToolboxScope == BROWSERTOOLBOX_SCOPE_EVERYTHING) {
    154      // Force listening to new additional target types
    155      this.startListening();
    156    } else if (browserToolboxScope == BROWSERTOOLBOX_SCOPE_PARENTPROCESS) {
    157      const disabledTargetTypes = [
    158        TargetCommand.TYPES.FRAME,
    159        TargetCommand.TYPES.PROCESS,
    160      ];
    161      // Force unwatching for additional targets types
    162      // (we keep listening to workers)
    163      // The related targets will be destroyed by the server
    164      // and reported as destroyed to the frontend.
    165      for (const type of disabledTargetTypes) {
    166        this.stopListeningForType(type, {
    167          isTargetSwitching: false,
    168          isModeSwitching: true,
    169        });
    170      }
    171    }
    172  }
    173 
    174  /**
    175   * Toggle listening to CONTENT_SCRIPT target type based on SHOW_CONTENT_SCRIPTS_PREF pref changes.
    176   */
    177  _updateContentScriptListening() {
    178    const showContentScripts = Services.prefs.getBoolPref(
    179      SHOW_CONTENT_SCRIPTS_PREF,
    180      false
    181    );
    182    if (showContentScripts) {
    183      // `_computeTargetTypes`, which is used by `startListening` to compute the types to listen to,
    184      // will read the pref and force listening to CONTENT_SCRIPT type
    185      this.startListening();
    186    } else {
    187      this.stopListeningForType(TargetCommand.TYPES.CONTENT_SCRIPT, {
    188        isTargetSwitching: false,
    189        isModeSwitching: false,
    190      });
    191    }
    192  }
    193 
    194  // Called whenever a new Target front is available.
    195  // Either because a target was already available as we started calling startListening
    196  // or if it has just been created
    197  // eslint-disable-next-line complexity
    198  async _onTargetAvailable(targetFront) {
    199    // We put the `commands` on the targetFront so it can be retrieved from any front easily.
    200    // Without this, protocol.js fronts won't have any easy access to it.
    201    // Ideally, Fronts would all be migrated to commands and we would no longer need this hack.
    202    targetFront.commands = this.commands;
    203 
    204    // If the new target is a top level target, we are target switching.
    205    // Target-switching is only triggered for "local-tab" browsing-context
    206    // targets which should always have the topLevelTarget flag initialized
    207    // on the server.
    208    const isTargetSwitching = targetFront.isTopLevel;
    209    const isFirstTarget =
    210      targetFront.isTopLevel && !this._gotFirstTopLevelTarget;
    211 
    212    if (this._targets.has(targetFront)) {
    213      // The top level target front can be reported via listProcesses in the
    214      // case of the BrowserToolbox. For any other target, log an error if it is
    215      // already registered.
    216      if (targetFront != this.targetFront) {
    217        console.error(
    218          "Target is already registered in the TargetCommand",
    219          targetFront.actorID
    220        );
    221      }
    222      return;
    223    }
    224 
    225    if (this.isDestroyed() || targetFront.isDestroyedOrBeingDestroyed()) {
    226      return;
    227    }
    228 
    229    // Handle top level target switching (when debugging a tab, navigation of the tab to a new document)
    230    if (targetFront.isTopLevel) {
    231      // First report that all existing targets are destroyed
    232      if (!isFirstTarget) {
    233        this._destroyExistingTargetsOnTargetSwitching();
    234      }
    235 
    236      // Update the reference to the memoized top level target
    237      this.targetFront = targetFront;
    238      this.descriptorFront.setTarget(targetFront);
    239 
    240      // When reloading a Web Extension, the fallback document, which is the top level may be notified *after* the background page.
    241      // So avoid resetting it being selected on late arrival of the fallback document.
    242      if (!this.descriptorFront.isWebExtensionDescriptor) {
    243        this.#selectedTargetFront = null;
    244      }
    245 
    246      if (isFirstTarget && this.isServerTargetSwitchingEnabled()) {
    247        this._gotFirstTopLevelTarget = true;
    248        this._resolveOnFirstTarget();
    249      }
    250    }
    251 
    252    this._targets.add(targetFront);
    253    try {
    254      await targetFront.attachAndInitThread(this);
    255    } catch (e) {
    256      console.error("Error when attaching target:", e);
    257      this._targets.delete(targetFront);
    258      return;
    259    }
    260 
    261    for (const targetFrontsSet of this._pendingWatchTargetInitialization.values()) {
    262      targetFrontsSet.delete(targetFront);
    263    }
    264 
    265    if (this.isDestroyed() || targetFront.isDestroyedOrBeingDestroyed()) {
    266      return;
    267    }
    268 
    269    this.store.dispatch(registerTarget(targetFront));
    270 
    271    // Then, once the target is attached, notify the target front creation listeners
    272    await this._createListeners.emitAsync(targetFront.targetType, {
    273      targetFront,
    274      isTargetSwitching,
    275    });
    276 
    277    // Re-register the listeners as the top level target changed
    278    // and some targets are fetched from it
    279    if (targetFront.isTopLevel && !isFirstTarget) {
    280      await this.startListening({ isTargetSwitching: true });
    281    }
    282 
    283    // These two events are used by tests using the production codepath (i.e. disabling flags.testing)
    284    // To be consumed by tests triggering frame navigations, spawning workers...
    285    this.emit("processed-available-target", targetFront);
    286 
    287    if (isTargetSwitching) {
    288      this.emit("switched-target", targetFront);
    289    }
    290 
    291    const autoSelectTarget =
    292      // When we navigate to a new top level document, automatically select that new document's target,
    293      // so that tools can start selecting and displaying this new document.
    294      isTargetSwitching ||
    295      // When debugging Web Extension, workaround the fallback document by automatically selected any incoming target
    296      // as soon as we are currently selecting that fallback document.
    297      (this.descriptorFront.isWebExtensionDescriptor &&
    298        this.#selectedTargetFront?.isFallbackExtensionDocument);
    299 
    300    if (autoSelectTarget) {
    301      await this.selectTarget(targetFront);
    302    }
    303  }
    304 
    305  _destroyExistingTargetsOnTargetSwitching() {
    306    const destroyedTargets = [];
    307    for (const target of this._targets) {
    308      // We only consider the top level target to be switched
    309      const isDestroyedTargetSwitching = target == this.targetFront;
    310      const isServiceWorker = target.targetType === this.TYPES.SERVICE_WORKER;
    311      const isPopup = target.targetForm.isPopup;
    312 
    313      // Never destroy the popup targets when the top level target is destroyed
    314      // as the popup follow a different lifecycle.
    315      // Also avoid destroying service worker targets for similar reason.
    316      if (!isPopup && !isServiceWorker) {
    317        this._onTargetDestroyed(target, {
    318          isTargetSwitching: isDestroyedTargetSwitching,
    319          // Do not destroy service worker front as we may want to keep using it.
    320          shouldDestroyTargetFront: !isServiceWorker,
    321        });
    322        destroyedTargets.push(target);
    323      }
    324    }
    325 
    326    // Stop listening to legacy listeners as we now have to listen
    327    // on the new target.
    328    this.stopListening({ isTargetSwitching: true });
    329 
    330    // Remove destroyed target from the cached target list. We don't simply clear the
    331    // Map as SW targets might not have been destroyed.
    332    for (const target of destroyedTargets) {
    333      this._targets.delete(target);
    334    }
    335  }
    336 
    337  /**
    338   * Function fired everytime a target is destroyed.
    339   *
    340   * This is called either:
    341   * - via target-destroyed event fired by the WatcherFront,
    342   *   event which is a simple translation of the target-destroyed-form emitted by the WatcherActor.
    343   *   Watcher Actor emits this is various condition when the debugged target is meant to be destroyed:
    344   *   - the related target context is destroyed (tab closed, worker shut down, content process destroyed, ...),
    345   *   - when the DevToolsServerConnection used on the server side to communicate to the client is closed.
    346 
    347   * - by TargetCommand._onTargetAvailable, when a top level target switching happens and all previously
    348   *   registered target fronts should be destroyed.
    349 
    350   * - by the legacy Targets listeners, calling this method directly.
    351   *   This usecase is meant to be removed someday when all target targets are supported by the Watcher.
    352   *   (bug 1687459)
    353   *
    354   * @param {TargetFront} targetFront
    355   *        The target that just got destroyed.
    356   * @param {object} options
    357   * @param {boolean} [options.isTargetSwitching]
    358   *        To be set to true when this is about the top level target which is being replaced
    359   *        by a new one.
    360   *        The passed target should be still the one store in TargetCommand.targetFront
    361   *        and will be replaced via a call to onTargetAvailable with a new target front.
    362   * @param {boolean} [options.isModeSwitching]
    363   *        To be set to true when the target was destroyed was called as the result of a
    364   *        change to the devtools.browsertoolbox.scope pref.
    365   * @param {boolean} [options.shouldDestroyTargetFront]
    366   *        By default, the passed target front will be destroyed. But in some cases like
    367   *        legacy listeners for service workers we want to keep the front alive.
    368   */
    369  _onTargetDestroyed(
    370    targetFront,
    371    {
    372      isModeSwitching = false,
    373      isTargetSwitching = false,
    374      shouldDestroyTargetFront = true,
    375    } = {}
    376  ) {
    377    // The watcher actor may notify us about the destruction of the top level target.
    378    // But second argument to this method, isTargetSwitching is only passed from the frontend.
    379    // So automatically toggle the isTargetSwitching flag for server side destructions
    380    // only if that's about the existing top level target.
    381    if (targetFront == this.targetFront) {
    382      isTargetSwitching = true;
    383    }
    384    this._destroyListeners.emit(targetFront.targetType, {
    385      targetFront,
    386      isTargetSwitching,
    387      isModeSwitching,
    388    });
    389    this._targets.delete(targetFront);
    390 
    391    this.store.dispatch(unregisterTarget(targetFront));
    392 
    393    // If the destroyed target was the selected one, we need to do some cleanup
    394    if (this.#selectedTargetFront == targetFront) {
    395      // If we're doing a targetSwitch, simply nullify #selectedTargetFront
    396      if (isTargetSwitching) {
    397        this.#selectedTargetFront = null;
    398      } else {
    399        // Otherwise we want to select the top level target
    400        let fallbackTarget = this.targetFront;
    401 
    402        // When debugging Web Extension we don't want to immediately fallback to the top level target, which is the fallback document.
    403        // Instead, try to lookup for the background page.
    404        if (this.descriptorFront.isWebExtensionDescriptor) {
    405          const backgroundPageTargetFront = [...this._targets].find(
    406            target => !target.isFallbackExtensionDocument
    407          );
    408          if (backgroundPageTargetFront) {
    409            fallbackTarget = backgroundPageTargetFront;
    410          }
    411        }
    412 
    413        this.selectTarget(fallbackTarget);
    414      }
    415    }
    416 
    417    if (shouldDestroyTargetFront) {
    418      // When calling targetFront.destroy(), we will first call TargetFrontMixin.destroy,
    419      // which will try to call `detach` RDP method.
    420      // Unfortunately, this request will never complete in some cases like bfcache navigations.
    421      // Because of that, the target front will never be completely destroy as it will prevent
    422      // calling super.destroy and Front.destroy.
    423      // Workaround that by manually calling Front class destroy method:
    424      targetFront.baseFrontClassDestroy();
    425 
    426      targetFront.destroy();
    427 
    428      // Delete the attribute we set from _onTargetAvailable so that we avoid leaking commands
    429      // if any target front is leaked.
    430      delete targetFront.commands;
    431    }
    432  }
    433 
    434  /**
    435   *
    436   * @param {TargetFront} targetFront
    437   */
    438  async _onTargetSelected(targetFront) {
    439    if (this.#selectedTargetFront == targetFront) {
    440      // Target is already selected, we can bail out.
    441      return;
    442    }
    443 
    444    this.#selectedTargetFront = targetFront;
    445    await this._selectListeners.emitAsync(targetFront.targetType, {
    446      targetFront,
    447    });
    448  }
    449 
    450  _setListening(type, value) {
    451    if (value) {
    452      this._listenersStarted.add(type);
    453    } else {
    454      this._listenersStarted.delete(type);
    455    }
    456  }
    457 
    458  _isListening(type) {
    459    return this._listenersStarted.has(type);
    460  }
    461 
    462  /**
    463   * Check if the watcher is currently supported.
    464   *
    465   * When no typeOrTrait is provided, we will only check that the watcher is
    466   * available.
    467   *
    468   * When a typeOrTrait is provided, we will check for an explicit trait on the
    469   * watcherFront that indicates either that:
    470   *   - a target type is supported
    471   *   - or that a custom trait is true
    472   *
    473   * @param {string} [targetTypeOrTrait]
    474   *        Optional target type or trait.
    475   * @return {boolean} true if the watcher is available and supports the
    476   *          optional targetTypeOrTrait
    477   */
    478  hasTargetWatcherSupport(targetTypeOrTrait) {
    479    if (targetTypeOrTrait) {
    480      // Target types are also exposed as traits, where resource types are
    481      // exposed under traits.resources (cf hasResourceWatcherSupport
    482      // implementation).
    483      return !!this.watcherFront?.traits[targetTypeOrTrait];
    484    }
    485 
    486    return !!this.watcherFront;
    487  }
    488 
    489  /**
    490   * Start listening for targets from the server
    491   *
    492   * Interact with the actors in order to start listening for new types of targets.
    493   * This will fire the _onTargetAvailable function for all already-existing targets,
    494   * as well as the next one to be created. It will also call _onTargetDestroyed
    495   * everytime a target is reported as destroyed by the actors.
    496   * By the time this function resolves, all the already-existing targets will be
    497   * reported to _onTargetAvailable.
    498   *
    499   * @param Object options
    500   * @param Boolean options.isTargetSwitching
    501   *        Set to true when this is called while a target switching happens. In such case,
    502   *        we won't register listener set on the Watcher Actor, but still register listeners
    503   *        set via Legacy Listeners.
    504   */
    505  async startListening({ isTargetSwitching = false } = {}) {
    506    // The first time we call this method, we pull the current top level target from the descriptor
    507    if (
    508      !this.isServerTargetSwitchingEnabled() &&
    509      !this._gotFirstTopLevelTarget
    510    ) {
    511      await this._createFirstTarget();
    512    }
    513 
    514    // If no pref are set to true, nor is listenForWorkers set to true,
    515    // we won't listen for any additional target. Only the top level target
    516    // will be managed. We may still do target-switching.
    517    const types = this._computeTargetTypes();
    518 
    519    for (const type of types) {
    520      if (this._isListening(type)) {
    521        continue;
    522      }
    523      this._setListening(type, true);
    524 
    525      // Only a few top level targets support the watcher actor at the moment (see WatcherActor
    526      // traits in the _form method). Bug 1675763 tracks watcher actor support for all targets.
    527      if (this.hasTargetWatcherSupport(type)) {
    528        // When we switch to a new top level target, we don't have to stop and restart
    529        // Watcher listener as it is independant from the top level target.
    530        // This isn't the case for some Legacy Listeners, which fetch targets from the top level target
    531        if (!isTargetSwitching) {
    532          await this.watcherFront.watchTargets(type);
    533        }
    534      } else if (LegacyTargetWatchers[type]) {
    535        // Instantiate the legacy listener only once for each TargetCommand, and reuse it if we stop and restart listening
    536        if (!this.legacyImplementation[type]) {
    537          this.legacyImplementation[type] = new LegacyTargetWatchers[type](
    538            this,
    539            this._onTargetAvailable,
    540            this._onTargetDestroyed,
    541            this.commands
    542          );
    543        }
    544        await this.legacyImplementation[type].listen();
    545      } else {
    546        throw new Error(`Unsupported target type '${type}'`);
    547      }
    548    }
    549 
    550    if (!this._watchingDocumentEvent && !this.isDestroyed()) {
    551      // We want to watch DOCUMENT_EVENT in order to update the url and title of target fronts,
    552      // as the initial value that is set in them might be erroneous (if the target was
    553      // created so early that the document url is still pointing to about:blank and the
    554      // html hasn't be parsed yet, so we can't know the <title> content).
    555 
    556      this._watchingDocumentEvent = true;
    557      await this.commands.resourceCommand.watchResources(
    558        [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
    559        {
    560          onAvailable: this._onResourceAvailable,
    561        }
    562      );
    563    }
    564 
    565    if (this.isServerTargetSwitchingEnabled()) {
    566      await this._onFirstTarget;
    567    }
    568  }
    569 
    570  async _createFirstTarget() {
    571    // Note that this is a public attribute, used outside of this class
    572    // and helps knowing what is the current top level target we debug.
    573    this.targetFront = await this.descriptorFront.getTarget();
    574    this.targetFront.setIsTopLevel(true);
    575    this._gotFirstTopLevelTarget = true;
    576 
    577    // See _onTargetAvailable. As this target isn't going through that method
    578    // we have to replicate doing that here.
    579    this.targetFront.commands = this.commands;
    580 
    581    // Add the top-level target to the list of targets.
    582    this._targets.add(this.targetFront);
    583    this.store.dispatch(registerTarget(this.targetFront));
    584  }
    585 
    586  _computeTargetTypes() {
    587    let types = [];
    588 
    589    // We also check for watcher support as some xpcshell tests uses legacy APIs and don't support frames.
    590    if (
    591      this.descriptorFront.isTabDescriptor &&
    592      this.hasTargetWatcherSupport(TargetCommand.TYPES.FRAME)
    593    ) {
    594      types = [TargetCommand.TYPES.FRAME];
    595      const showContentScripts = Services.prefs.getBoolPref(
    596        SHOW_CONTENT_SCRIPTS_PREF,
    597        false
    598      );
    599      if (
    600        showContentScripts &&
    601        this.hasTargetWatcherSupport(TargetCommand.TYPES.CONTENT_SCRIPT)
    602      ) {
    603        types.push(TargetCommand.TYPES.CONTENT_SCRIPT);
    604      }
    605    } else if (
    606      this.descriptorFront.isWebExtensionDescriptor &&
    607      this.descriptorFront.isServerTargetSwitchingEnabled()
    608    ) {
    609      types = [TargetCommand.TYPES.FRAME];
    610    } else if (this.descriptorFront.isBrowserProcessDescriptor) {
    611      const browserToolboxScope = Services.prefs.getCharPref(
    612        BROWSERTOOLBOX_SCOPE_PREF
    613      );
    614      if (browserToolboxScope == BROWSERTOOLBOX_SCOPE_EVERYTHING) {
    615        // Listen for all target types when the browser toolbox is in multiprocess mode.
    616        //
    617        // Except for CONTENT_SCRIPT targets, as their scripts are already debuggable
    618        // via the content process targets.
    619        if (!this.listenForContentScripts) {
    620          types = TargetCommand.ALL_TYPES.filter(
    621            t => t != TargetCommand.TYPES.CONTENT_SCRIPT
    622          );
    623        } else {
    624          types = TargetCommand.ALL_TYPES;
    625        }
    626      }
    627    }
    628    if (this.listenForWorkers && !types.includes(TargetCommand.TYPES.WORKER)) {
    629      types.push(TargetCommand.TYPES.WORKER);
    630    }
    631 
    632    // Bug 1607778 - For now, shared workers are only displayed in the Browser Toolbox.
    633    // The server doesn't expose them yet. See `getWatcherSupportedTargets()` and
    634    // `WorkerTargetWatcherClass.shouldHandleWorker`.
    635    if (
    636      this.listenForWorkers &&
    637      this.descriptorFront.isBrowserProcessDescriptor &&
    638      !types.includes(TargetCommand.TYPES.SHARED_WORKER)
    639    ) {
    640      types.push(TargetCommand.TYPES.SHARED_WORKER);
    641    }
    642 
    643    if (
    644      this.listenForServiceWorkers &&
    645      !types.includes(TargetCommand.TYPES.SERVICE_WORKER)
    646    ) {
    647      types.push(TargetCommand.TYPES.SERVICE_WORKER);
    648    }
    649 
    650    return types;
    651  }
    652 
    653  /**
    654   * Stop listening for targets from the server
    655   *
    656   * @param Object options
    657   * @param Boolean options.isTargetSwitching
    658   *        Set to true when this is called while a target switching happens. In such case,
    659   *        we won't unregister listener set on the Watcher Actor, but still unregister
    660   *        listeners set via Legacy Listeners.
    661   */
    662  stopListening({ isTargetSwitching = false } = {}) {
    663    // As DOCUMENT_EVENT isn't using legacy listener,
    664    // there is no need to stop and restart it in case of target switching.
    665    if (this._watchingDocumentEvent && !isTargetSwitching) {
    666      this.commands.resourceCommand.unwatchResources(
    667        [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
    668        {
    669          onAvailable: this._onResourceAvailable,
    670        }
    671      );
    672      this._watchingDocumentEvent = false;
    673    }
    674 
    675    for (const type of TargetCommand.ALL_TYPES) {
    676      this.stopListeningForType(type, { isTargetSwitching });
    677    }
    678  }
    679 
    680  /**
    681   * Stop listening for targets of a given type from the server
    682   *
    683   * @param String type
    684   *        target type we want to stop listening for
    685   * @param Object options
    686   * @param Boolean options.isTargetSwitching
    687   *        Set to true when this is called while a target switching happens. In such case,
    688   *        we won't unregister listener set on the Watcher Actor, but still unregister
    689   *        listeners set via Legacy Listeners.
    690   * @param Boolean options.isModeSwitching
    691   *        Set to true when this is called as the result of a change to the
    692   *        devtools.browsertoolbox.scope pref.
    693   */
    694  stopListeningForType(type, { isTargetSwitching, isModeSwitching }) {
    695    if (!this._isListening(type)) {
    696      return;
    697    }
    698    this._setListening(type, false);
    699 
    700    // Only a few top level targets support the watcher actor at the moment (see WatcherActor
    701    // traits in the _form method). Bug 1675763 tracks watcher actor support for all targets.
    702    if (this.hasTargetWatcherSupport(type)) {
    703      // When we switch to a new top level target, we don't have to stop and restart
    704      // Watcher listener as it is independant from the top level target.
    705      // This isn't the case for some Legacy Listeners, which fetch targets from the top level target
    706      // Also, TargetCommand.destroy may be called after the client is closed.
    707      // So avoid calling the RDP method in that situation.
    708      if (!isTargetSwitching && !this.watcherFront.isDestroyed()) {
    709        this.watcherFront.unwatchTargets(type, { isModeSwitching });
    710      }
    711    } else if (this.legacyImplementation[type]) {
    712      this.legacyImplementation[type].unlisten({
    713        isTargetSwitching,
    714        isModeSwitching,
    715      });
    716    } else {
    717      throw new Error(`Unsupported target type '${type}'`);
    718    }
    719  }
    720 
    721  _matchTargetType(type, target) {
    722    return type === target.targetType;
    723  }
    724 
    725  _onResourceAvailable(resources) {
    726    for (const resource of resources) {
    727      if (
    728        resource.resourceType ===
    729        this.commands.resourceCommand.TYPES.DOCUMENT_EVENT
    730      ) {
    731        const { targetFront } = resource;
    732        if (resource.title !== undefined && targetFront?.setTitle) {
    733          targetFront.setTitle(resource.title);
    734        }
    735        if (resource.url !== undefined && targetFront?.setUrl) {
    736          targetFront.setUrl(resource.url);
    737        }
    738        if (
    739          !resource.isFrameSwitching &&
    740          // `url` is set on the targetFront when we receive dom-loading, and `title` when
    741          // `dom-interactive` is received. Here we're only updating the window title in
    742          // the "newer" event.
    743          resource.name === "dom-interactive"
    744        ) {
    745          // We just updated the targetFront title and url, force a refresh
    746          // so that the EvaluationContext selector update them.
    747          this.store.dispatch(refreshTargets());
    748        }
    749      }
    750    }
    751  }
    752 
    753  /**
    754   * Listen for the creation and/or destruction of target fronts matching one of the provided types.
    755   *
    756   * @param {object} options
    757   * @param {Array<string>} options.types
    758   *        The type of target to listen for. Constant of TargetCommand.TYPES.
    759   * @param {Function} options.onAvailable
    760   *        Mandatory callback fired when a target has been just created or was already available.
    761   *        The function is called with a single object argument containing the following properties:
    762   *        - {TargetFront} targetFront: The target Front
    763   *        - {Boolean} isTargetSwitching: Is this target relates to a navigation and
    764   *                    this replaced a previously available target, this flag will be true
    765   * @param {Function} options.onDestroyed
    766   *        Optional callback fired in case of target front destruction.
    767   *        The function is called with the same arguments than onAvailable.
    768   * @param {Function} options.onSelected
    769   *        Optional callback fired for any new top level target (on startup and navigations),
    770   *        as well as when the user select a new target from the console context selector,
    771   *        debugger threads list and toolbox iframe dropdown.
    772   *        The function is called with a single object argument containing the following properties:
    773   *        - {TargetFront} targetFront: The target Front
    774   */
    775  async watchTargets(options = {}) {
    776    const availableOptions = [
    777      "types",
    778      "onAvailable",
    779      "onDestroyed",
    780      "onSelected",
    781    ];
    782    const unsupportedKeys = Object.keys(options).filter(
    783      key => !availableOptions.includes(key)
    784    );
    785    if (unsupportedKeys.length) {
    786      throw new Error(
    787        `TargetCommand.watchTargets does not expect the following options: ${unsupportedKeys.join(
    788          ", "
    789        )}`
    790      );
    791    }
    792 
    793    const { types, onAvailable, onDestroyed, onSelected } = options;
    794    if (typeof onAvailable != "function") {
    795      throw new Error(
    796        "TargetCommand.watchTargets expects a function for the onAvailable option"
    797      );
    798    }
    799 
    800    for (const type of types) {
    801      if (!this._isValidTargetType(type)) {
    802        throw new Error(
    803          `TargetCommand.watchTargets invoked with an unknown type: "${type}"`
    804        );
    805      }
    806    }
    807 
    808    // Notify about already existing target of these types
    809    const targetFronts = [...this._targets].filter(targetFront =>
    810      types.includes(targetFront.targetType)
    811    );
    812    this._pendingWatchTargetInitialization.set(
    813      onAvailable,
    814      new Set(targetFronts)
    815    );
    816    const promises = targetFronts.map(async targetFront => {
    817      // Attach the targets that aren't attached yet (e.g. the initial top-level target),
    818      // and wait for the other ones to be fully attached.
    819      try {
    820        await targetFront.attachAndInitThread(this);
    821      } catch (e) {
    822        console.error("Error when attaching target:", e);
    823        return;
    824      }
    825 
    826      // It can happen that onAvailable was already called with this targetFront at
    827      // this time (via _onTargetAvailable). If that's the case, we don't want to call
    828      // onAvailable a second time.
    829      if (
    830        this._pendingWatchTargetInitialization &&
    831        this._pendingWatchTargetInitialization.has(onAvailable) &&
    832        !this._pendingWatchTargetInitialization
    833          .get(onAvailable)
    834          .has(targetFront)
    835      ) {
    836        return;
    837      }
    838 
    839      try {
    840        // Ensure waiting for eventual async create listeners
    841        // which may setup things regarding the existing targets
    842        // and listen callsite may care about the full initialization
    843        await onAvailable({
    844          targetFront,
    845          isTargetSwitching: false,
    846        });
    847      } catch (e) {
    848        // Prevent throwing when onAvailable handler throws on one target
    849        // so that it can try to register the other targets
    850        console.error(
    851          "Exception when calling onAvailable handler",
    852          e.message,
    853          e
    854        );
    855      }
    856    });
    857 
    858    for (const type of types) {
    859      this._createListeners.on(type, onAvailable);
    860      if (onDestroyed) {
    861        this._destroyListeners.on(type, onDestroyed);
    862      }
    863      if (onSelected) {
    864        this._selectListeners.on(type, onSelected);
    865      }
    866    }
    867 
    868    await Promise.all(promises);
    869    this._pendingWatchTargetInitialization.delete(onAvailable);
    870 
    871    try {
    872      if (
    873        onSelected &&
    874        this.selectedTargetFront &&
    875        types.includes(this.selectedTargetFront.targetType)
    876      ) {
    877        await onSelected({
    878          targetFront: this.selectedTargetFront,
    879        });
    880      }
    881    } catch (e) {
    882      // Prevent throwing when onSelected handler throws on one target
    883      // (this may make test to fail when closing the toolbox quickly after opening)
    884      console.error("Exception when calling onSelected handler", e.message, e);
    885    }
    886  }
    887 
    888  /**
    889   * Stop listening for the creation and/or destruction of a given type of target fronts.
    890   * See `watchTargets()` for documentation of the arguments.
    891   */
    892  unwatchTargets(options = {}) {
    893    const availableOptions = [
    894      "types",
    895      "onAvailable",
    896      "onDestroyed",
    897      "onSelected",
    898    ];
    899    const unsupportedKeys = Object.keys(options).filter(
    900      key => !availableOptions.includes(key)
    901    );
    902    if (unsupportedKeys.length) {
    903      throw new Error(
    904        `TargetCommand.unwatchTargets does not expect the following options: ${unsupportedKeys.join(
    905          ", "
    906        )}`
    907      );
    908    }
    909 
    910    const { types, onAvailable, onDestroyed, onSelected } = options;
    911    if (typeof onAvailable != "function") {
    912      throw new Error(
    913        "TargetCommand.unwatchTargets expects a function for the onAvailable option"
    914      );
    915    }
    916 
    917    for (const type of types) {
    918      if (!this._isValidTargetType(type)) {
    919        throw new Error(
    920          `TargetCommand.unwatchTargets invoked with an unknown type: "${type}"`
    921        );
    922      }
    923 
    924      this._createListeners.off(type, onAvailable);
    925      if (onDestroyed) {
    926        this._destroyListeners.off(type, onDestroyed);
    927      }
    928      if (onSelected) {
    929        this._selectListeners.off(type, onSelected);
    930      }
    931    }
    932    this._pendingWatchTargetInitialization.delete(onAvailable);
    933  }
    934 
    935  /**
    936   * Retrieve all the current target fronts of a given type.
    937   *
    938   * @param {Array<string>} types
    939   *        The types of target to retrieve. Array of TargetCommand.TYPES
    940   * @return {Array<TargetFront>} Array of target fronts matching any of the
    941   *         provided types.
    942   */
    943  getAllTargets(types) {
    944    if (!types?.length) {
    945      throw new Error("getAllTargets expects a non-empty array of types");
    946    }
    947 
    948    const targets = [...this._targets].filter(target =>
    949      types.some(type => this._matchTargetType(type, target))
    950    );
    951 
    952    return targets;
    953  }
    954 
    955  /**
    956   * Retrieve all the target fronts in the selected target tree (including the selected
    957   * target itself).
    958   *
    959   * @param {Array<string>} types
    960   *        The types of target to retrieve. Array of TargetCommand.TYPES
    961   * @return {Promise<Array<TargetFront>>} Promise that resolves to an array of target fronts.
    962   */
    963  async getAllTargetsInSelectedTargetTree(types) {
    964    const allTargets = this.getAllTargets(types);
    965    if (this.isTopLevelTargetSelected()) {
    966      return allTargets;
    967    }
    968 
    969    const targets = [this.selectedTargetFront];
    970    for (const target of allTargets) {
    971      const isInSelectedTree = await target.isTargetAnAncestor(
    972        this.selectedTargetFront
    973      );
    974 
    975      if (isInSelectedTree) {
    976        targets.push(target);
    977      }
    978    }
    979    return targets;
    980  }
    981 
    982  /**
    983   * For all the target fronts of given types, retrieve all the target-scoped fronts of the given types.
    984   *
    985   * @param {Array<string>} targetTypes
    986   *        The types of target to iterate over. Constant of TargetCommand.TYPES.
    987   * @param {string} frontType
    988   *        The type of target-scoped front to retrieve. It can be "inspector", "console", "thread",...
    989   * @param {object} options
    990   * @param {boolean} options.onlyInSelectedTargetTree
    991   *        Set to true to only get the fronts for targets who are in the "targets tree"
    992   *        of the selected target.
    993   */
    994  async getAllFronts(
    995    targetTypes,
    996    frontType,
    997    { onlyInSelectedTargetTree = false } = {}
    998  ) {
    999    if (!Array.isArray(targetTypes) || !targetTypes?.length) {
   1000      throw new Error("getAllFronts expects a non-empty array of target types");
   1001    }
   1002    const promises = [];
   1003    const targets = !onlyInSelectedTargetTree
   1004      ? this.getAllTargets(targetTypes)
   1005      : await this.getAllTargetsInSelectedTargetTree(targetTypes);
   1006    for (const target of targets) {
   1007      // For still-attaching worker targets, the thread or console front may not yet be available,
   1008      // whereas TargetMixin.getFront will throw if the actorID isn't available in targetForm.
   1009      // Also ignore destroyed targets. For some reason the previous methods fetching targets
   1010      // can sometime return destroyed targets.
   1011      if (
   1012        (frontType == "thread" && !target.targetForm.threadActor) ||
   1013        (frontType == "console" && !target.targetForm.consoleActor) ||
   1014        target.isDestroyed()
   1015      ) {
   1016        continue;
   1017      }
   1018 
   1019      promises.push(target.getFront(frontType));
   1020    }
   1021    return Promise.all(promises);
   1022  }
   1023 
   1024  /**
   1025   * This function is triggered by an event sent by the TabDescriptor when
   1026   * the tab navigates to a distinct process.
   1027   *
   1028   * @param TargetFront targetFront
   1029   *        The WindowGlobalTargetFront instance that navigated to another process
   1030   */
   1031  async onLocalTabRemotenessChange(targetFront) {
   1032    if (this.isServerTargetSwitchingEnabled()) {
   1033      // For server-side target switching, everything will be handled by the
   1034      // _onTargetAvailable callback.
   1035      return;
   1036    }
   1037 
   1038    // TabDescriptor may emit the event with a null targetFront, interpret that as if the previous target
   1039    // has already been destroyed
   1040    if (targetFront) {
   1041      // Wait for the target to be destroyed so that LocalTabCommandsFactory clears its memoized target for this tab
   1042      await targetFront.once("target-destroyed");
   1043    }
   1044 
   1045    // Fetch the new target from the descriptor.
   1046    const newTarget = await this.descriptorFront.getTarget();
   1047 
   1048    // If a navigation happens while we try to get the target for the page that triggered
   1049    // the remoteness change, `getTarget` will return null. In such case, we'll get the
   1050    // "next" target through onTargetAvailable so it's safe to bail here.
   1051    if (!newTarget) {
   1052      console.warn(
   1053        `Couldn't get the target for descriptor ${this.descriptorFront.actorID}`
   1054      );
   1055      return;
   1056    }
   1057 
   1058    this.switchToTarget(newTarget);
   1059  }
   1060 
   1061  /**
   1062   * Reload the current top level target.
   1063   * This only works for targets inheriting from WindowGlobalTarget.
   1064   *
   1065   * @param {boolean} bypassCache
   1066   *        If true, the reload will be forced to bypass any cache.
   1067   */
   1068  async reloadTopLevelTarget(bypassCache = false) {
   1069    if (!this.descriptorFront.traits.supportsReloadDescriptor) {
   1070      throw new Error("The top level target doesn't support being reloaded");
   1071    }
   1072 
   1073    // Wait for the next DOCUMENT_EVENT's dom-complete event
   1074    // Wait for waitForNextResource completion before reloading, otherwise we might miss the dom-complete event.
   1075    // This can happen if `ResourceCommand.watchResources` made by `waitForNextResource` is still pending
   1076    // while the reload already started and finished loading the document early.
   1077    const { onResource: onReloaded } =
   1078      await this.commands.resourceCommand.waitForNextResource(
   1079        this.commands.resourceCommand.TYPES.DOCUMENT_EVENT,
   1080        {
   1081          ignoreExistingResources: true,
   1082          predicate(resource) {
   1083            return resource.name == "dom-complete";
   1084          },
   1085        }
   1086      );
   1087 
   1088    await this.descriptorFront.reloadDescriptor({ bypassCache });
   1089 
   1090    await onReloaded;
   1091  }
   1092 
   1093  /**
   1094   * Navigate the top level document to a new URL.
   1095   *
   1096   * @param {string} url
   1097   * @param {boolean} waitForLoad
   1098   *        Default to true and wait for the document to be fully loaded before resolving.
   1099   * @return Promise
   1100   *        Promise resolved once the navigation has been proceeded by the remote runtime,
   1101   *        and if waitForLoad is true, resolved only once the target url is fully loaded.
   1102   */
   1103  navigateTo(url, waitForLoad = true) {
   1104    if (!this.descriptorFront.traits.supportsNavigation) {
   1105      throw new Error("Descriptor doesn't support navigation");
   1106    }
   1107 
   1108    return this.descriptorFront.navigateTo(url, waitForLoad);
   1109  }
   1110 
   1111  goBack() {
   1112    if (!this.descriptorFront.traits.supportsNavigation) {
   1113      throw new Error("Descriptor doesn't support navigation");
   1114    }
   1115 
   1116    return this.descriptorFront.goBack();
   1117  }
   1118 
   1119  goForward() {
   1120    if (!this.descriptorFront.traits.supportsNavigation) {
   1121      throw new Error("Descriptor doesn't support navigation");
   1122    }
   1123    return this.descriptorFront.goForward();
   1124  }
   1125 
   1126  /**
   1127   * Called when the top level target is replaced by a new one.
   1128   * Typically when we navigate to another domain which requires to be loaded in a distinct process.
   1129   *
   1130   * @param {TargetFront} newTarget
   1131   *        The new top level target to debug.
   1132   */
   1133  async switchToTarget(newTarget) {
   1134    // Notify about this new target to creation listeners
   1135    // _onTargetAvailable will also destroy all previous target before notifying about this new one.
   1136    await this._onTargetAvailable(newTarget);
   1137  }
   1138 
   1139  /**
   1140   * Called when the user selects a frame in the iframe picker.
   1141   *
   1142   * @param {WindowGlobalTargetFront} targetFront
   1143   *        The target front we want the toolbox to focus on.
   1144   */
   1145  async selectTarget(targetFront) {
   1146    // Ignore any target which we may try to select, but is already being destroyed
   1147    if (targetFront.isDestroyedOrBeingDestroyed()) {
   1148      return;
   1149    }
   1150    await this._onTargetSelected(targetFront);
   1151  }
   1152 
   1153  /**
   1154   * Returns true if the top-level frame is the selected one
   1155   *
   1156   * @returns {boolean}
   1157   */
   1158  isTopLevelTargetSelected() {
   1159    return this.selectedTargetFront === this.targetFront;
   1160  }
   1161 
   1162  /**
   1163   * Returns true if a non top-level frame is the selected one in the iframe picker.
   1164   *
   1165   * @returns {boolean}
   1166   */
   1167  isNonTopLevelTargetSelected() {
   1168    return this.selectedTargetFront !== this.targetFront;
   1169  }
   1170 
   1171  isTargetRegistered(targetFront) {
   1172    return this._targets.has(targetFront);
   1173  }
   1174 
   1175  getParentTarget(targetFront) {
   1176    // Note that there are edgecases:
   1177    // * Until bug 1741927 is fixed and we remove non-EFT codepath entirely,
   1178    //   we may receive a `parentInnerWindowId` that doesn't relate to any target.
   1179    //   This happens when the parent document of the targetFront is a document loaded in the
   1180    //   same process as its parent document. In such scenario, and only when EFT is disabled,
   1181    //   we won't instantiate a target for the parent document of the targetFront.
   1182    // * `parentInnerWindowId` could be null in some case like for tabs in the MBT
   1183    //   we should report the top level target as parent. That's what `getParentWindowGlobalTarget` does.
   1184    //   Once we can stop using getParentWindowGlobalTarget for the other edgecase we will be able to
   1185    //   replace it with such fallback: `return this.targetFront;`.
   1186    //   browser_target_command_frames.js will help you get things right.
   1187    const { parentInnerWindowId } = targetFront.targetForm;
   1188    if (parentInnerWindowId) {
   1189      const targets = this.getAllTargets([TargetCommand.TYPES.FRAME]);
   1190      const parent = targets.find(
   1191        target => target.innerWindowId == parentInnerWindowId
   1192      );
   1193      // Until EFT is the only codepath supported (bug 1741927), we will fallback to `getParentWindowGlobalTarget`
   1194      // as we may not have a target if the parent is an iframe running in the same process as its parent.
   1195      if (parent) {
   1196        return parent;
   1197      }
   1198    }
   1199 
   1200    // Note that all callsites which care about FRAME additional target
   1201    // should all have a toolbox using the watcher actor.
   1202    // It should be: MBT, regular tab toolbox and web extension.
   1203    // The others which still don't support watcher don't spawn FRAME targets:
   1204    // browser content toolbox and service workers.
   1205 
   1206    return this.watcherFront.getParentWindowGlobalTarget(
   1207      targetFront.browsingContextID
   1208    );
   1209  }
   1210 
   1211  isDestroyed() {
   1212    return this._isDestroyed;
   1213  }
   1214 
   1215  isServerTargetSwitchingEnabled() {
   1216    if (this.descriptorFront.isServerTargetSwitchingEnabled) {
   1217      return this.descriptorFront.isServerTargetSwitchingEnabled();
   1218    }
   1219    return false;
   1220  }
   1221 
   1222  _isValidTargetType(type) {
   1223    return this.ALL_TYPES.includes(type);
   1224  }
   1225 
   1226  destroy() {
   1227    this.stopListening();
   1228    this._createListeners.off();
   1229    this._destroyListeners.off();
   1230    this._selectListeners.off();
   1231 
   1232    this.#selectedTargetFront = null;
   1233    this._isDestroyed = true;
   1234 
   1235    Services.prefs.removeObserver(
   1236      BROWSERTOOLBOX_SCOPE_PREF,
   1237      this._updateBrowserToolboxScope
   1238    );
   1239    Services.prefs.removeObserver(
   1240      SHOW_CONTENT_SCRIPTS_PREF,
   1241      this._updateContentScriptListening
   1242    );
   1243  }
   1244 }
   1245 
   1246 /**
   1247 * All types of target:
   1248 */
   1249 TargetCommand.TYPES = TargetCommand.prototype.TYPES = {
   1250  PROCESS: "process",
   1251  FRAME: "frame",
   1252  WORKER: "worker",
   1253  SHARED_WORKER: "shared_worker",
   1254  SERVICE_WORKER: "service_worker",
   1255  CONTENT_SCRIPT: "content_script",
   1256 };
   1257 TargetCommand.ALL_TYPES = TargetCommand.prototype.ALL_TYPES = Object.values(
   1258  TargetCommand.TYPES
   1259 );
   1260 
   1261 const LegacyTargetWatchers = {};
   1262 loader.lazyRequireGetter(
   1263  LegacyTargetWatchers,
   1264  TargetCommand.TYPES.PROCESS,
   1265  "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js"
   1266 );
   1267 loader.lazyRequireGetter(
   1268  LegacyTargetWatchers,
   1269  TargetCommand.TYPES.WORKER,
   1270  "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js"
   1271 );
   1272 loader.lazyRequireGetter(
   1273  LegacyTargetWatchers,
   1274  TargetCommand.TYPES.SHARED_WORKER,
   1275  "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-sharedworkers-watcher.js"
   1276 );
   1277 loader.lazyRequireGetter(
   1278  LegacyTargetWatchers,
   1279  TargetCommand.TYPES.SERVICE_WORKER,
   1280  "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js"
   1281 );
   1282 
   1283 module.exports = TargetCommand;