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