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/. */
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(
20 ["refreshTargets", "registerTarget", "unregisterTarget"],
21 "resource://devtools/shared/commands/target/actions/targets.js",
25 class TargetCommand extends EventEmitter {
28 * This class helps managing, iterating over and listening for Targets.
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.
39 * @fires target-tread-wrong-order-on-resume : An event that is emitted when resuming
40 * the thread throws with the "wrongOrder" error.
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/
49 constructor({ descriptorFront, watcherFront, commands }) {
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
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(
75 this.onLocalTabRemotenessChange
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));
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);
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 ===
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);
131 get selectedTargetFront() {
132 return this.#selectedTargetFront || this.targetFront;
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.
141 _updateBrowserToolboxScope() {
142 const browserToolboxScope = Services.prefs.getCharPref(
143 BROWSERTOOLBOX_SCOPE_PREF
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,
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,
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
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) {
189 "Target is already registered in the TargetCommand",
196 if (this.isDestroyed() || targetFront.isDestroyedOrBeingDestroyed()) {
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();
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();
220 // Map the descriptor typeName to a target type.
221 const targetType = this.getTargetType(targetFront);
222 targetFront.setTargetType(targetType);
224 this._targets.add(targetFront);
226 await targetFront.attachAndInitThread(this);
228 console.error("Error when attaching target:", e);
229 this._targets.delete(targetFront);
233 for (const targetFrontsSet of this._pendingWatchTargetInitialization.values()) {
234 targetFrontsSet.delete(targetFront);
237 if (this.isDestroyed() || targetFront.isDestroyedOrBeingDestroyed()) {
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, {
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 });
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);
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.
278 (!isServiceWorker || this.destroyServiceWorkersOnNavigation)
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,
285 destroyedTargets.push(target);
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
296 for (const target of destroyedTargets) {
297 this._targets.delete(target);
302 * Function fired everytime a target is destroyed.
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.
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
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.
336 isModeSwitching = false,
337 isTargetSwitching = false,
338 shouldDestroyTargetFront = true,
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;
348 this._destroyListeners.emit(targetFront.targetType, {
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;
363 // Otherwise we want to select the top level target
364 this.selectTarget(this.targetFront);
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;
387 * @param {TargetFront} targetFront
389 async _onTargetSelected(targetFront) {
390 if (this.#selectedTargetFront == targetFront) {
391 // Target is already selected, we can bail out.
395 this.#selectedTargetFront = targetFront;
396 const targetType = this.getTargetType(targetFront);
397 await this._selectListeners.emitAsync(targetType, {
402 _setListening(type, value) {
404 this._listenersStarted.add(type);
406 this._listenersStarted.delete(type);
411 return this._listenersStarted.has(type);
415 * Check if the watcher is currently supported.
417 * When no typeOrTrait is provided, we will only check that the watcher is
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
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
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
435 return !!this.watcherFront?.traits[targetTypeOrTrait];
438 return !!this.watcherFront;
442 * Start listening for targets from the server
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.
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.
457 async startListening({ isTargetSwitching = false } = {}) {
458 // The first time we call this method, we pull the current top level target from the descriptor
460 !this.isServerTargetSwitchingEnabled() &&
461 !this._gotFirstTopLevelTarget
463 await this._createFirstTarget();
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)) {
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);
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](
491 this._onTargetAvailable,
492 this._onTargetDestroyed,
496 await this.legacyImplementation[type].listen();
498 throw new Error(`Unsupported target type '${type}'`);
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],
512 onAvailable: this._onResourceAvailable,
517 if (this.isServerTargetSwitchingEnabled()) {
518 await this._onFirstTarget;
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));
539 _computeTargetTypes() {
542 // We also check for watcher support as some xpcshell tests uses legacy APIs and don't support frames.
544 this.descriptorFront.isTabDescriptor &&
545 this.hasTargetWatcherSupport(TargetCommand.TYPES.FRAME)
547 types = [TargetCommand.TYPES.FRAME];
548 } else if (this.descriptorFront.isBrowserProcessDescriptor) {
549 const browserToolboxScope = Services.prefs.getCharPref(
550 BROWSERTOOLBOX_SCOPE_PREF
552 if (browserToolboxScope == BROWSERTOOLBOX_SCOPE_EVERYTHING) {
553 types = TargetCommand.ALL_TYPES;
556 if (this.listenForWorkers && !types.includes(TargetCommand.TYPES.WORKER)) {
557 types.push(TargetCommand.TYPES.WORKER);
560 this.listenForWorkers &&
561 !types.includes(TargetCommand.TYPES.SHARED_WORKER)
563 types.push(TargetCommand.TYPES.SHARED_WORKER);
566 this.listenForServiceWorkers &&
567 !types.includes(TargetCommand.TYPES.SERVICE_WORKER)
569 types.push(TargetCommand.TYPES.SERVICE_WORKER);
576 * Stop listening for targets from the server
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.
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],
591 onAvailable: this._onResourceAvailable,
594 this._watchingDocumentEvent = false;
597 for (const type of TargetCommand.ALL_TYPES) {
598 this.stopListeningForType(type, { isTargetSwitching });
603 * Stop listening for targets of a given type from the server
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.
616 stopListeningForType(type, { isTargetSwitching, isModeSwitching }) {
617 if (!this._isListening(type)) {
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 });
633 } else if (this.legacyImplementation[type]) {
634 this.legacyImplementation[type].unlisten({
639 throw new Error(`Unsupported target type '${type}'`);
643 getTargetType(target) {
644 const { typeName } = target;
645 if (typeName == "windowGlobalTarget") {
646 return TargetCommand.TYPES.FRAME;
650 typeName == "contentProcessTarget" ||
651 typeName == "parentProcessTarget"
653 return TargetCommand.TYPES.PROCESS;
656 if (typeName == "workerDescriptor" || typeName == "workerTarget") {
657 if (target.isSharedWorker) {
658 return TargetCommand.TYPES.SHARED_WORKER;
661 if (target.isServiceWorker) {
662 return TargetCommand.TYPES.SERVICE_WORKER;
665 return TargetCommand.TYPES.WORKER;
668 throw new Error("Unsupported target typeName: " + typeName);
671 _matchTargetType(type, target) {
672 return type === target.targetType;
675 _onResourceAvailable(resources) {
676 for (const resource of resources) {
678 resource.resourceType ===
679 this.commands.resourceCommand.TYPES.DOCUMENT_EVENT
681 const { targetFront } = resource;
682 if (resource.title !== undefined && targetFront?.setTitle) {
683 targetFront.setTitle(resource.title);
685 if (resource.url !== undefined && targetFront?.setUrl) {
686 targetFront.setUrl(resource.url);
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"
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());
704 * Listen for the creation and/or destruction of target fronts matching one of the provided types.
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
723 async watchTargets(options = {}) {
724 const availableOptions = [
730 const unsupportedKeys = Object.keys(options).filter(
731 key => !availableOptions.includes(key)
733 if (unsupportedKeys.length) {
735 `TargetCommand.watchTargets does not expect the following options: ${unsupportedKeys.join(
741 const { types, onAvailable, onDestroyed, onSelected } = options;
742 if (typeof onAvailable != "function") {
744 "TargetCommand.watchTargets expects a function for the onAvailable option"
748 for (const type of types) {
749 if (!this._isValidTargetType(type)) {
751 `TargetCommand.watchTargets invoked with an unknown type: "${type}"`
756 // Notify about already existing target of these types
757 const targetFronts = [...this._targets].filter(targetFront =>
758 types.includes(targetFront.targetType)
760 this._pendingWatchTargetInitialization.set(
762 new Set(targetFronts)
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.
768 await targetFront.attachAndInitThread(this);
770 console.error("Error when attaching target:", e);
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.
778 this._pendingWatchTargetInitialization &&
779 this._pendingWatchTargetInitialization.has(onAvailable) &&
780 !this._pendingWatchTargetInitialization
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
793 isTargetSwitching: false,
796 // Prevent throwing when onAvailable handler throws on one target
797 // so that it can try to register the other targets
799 "Exception when calling onAvailable handler",
806 for (const type of types) {
807 this._createListeners.on(type, onAvailable);
809 this._destroyListeners.on(type, onDestroyed);
812 this._selectListeners.on(type, onSelected);
816 await Promise.all(promises);
817 this._pendingWatchTargetInitialization.delete(onAvailable);
821 * Stop listening for the creation and/or destruction of a given type of target fronts.
822 * See `watchTargets()` for documentation of the arguments.
824 unwatchTargets(options = {}) {
825 const availableOptions = [
831 const unsupportedKeys = Object.keys(options).filter(
832 key => !availableOptions.includes(key)
834 if (unsupportedKeys.length) {
836 `TargetCommand.unwatchTargets does not expect the following options: ${unsupportedKeys.join(
842 const { types, onAvailable, onDestroyed, onSelected } = options;
843 if (typeof onAvailable != "function") {
845 "TargetCommand.unwatchTargets expects a function for the onAvailable option"
849 for (const type of types) {
850 if (!this._isValidTargetType(type)) {
852 `TargetCommand.unwatchTargets invoked with an unknown type: "${type}"`
856 this._createListeners.off(type, onAvailable);
858 this._destroyListeners.off(type, onDestroyed);
861 this._selectListeners.off(type, onSelected);
864 this._pendingWatchTargetInitialization.delete(onAvailable);
868 * Retrieve all the current target fronts of a given type.
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
875 getAllTargets(types) {
876 if (!types?.length) {
877 throw new Error("getAllTargets expects a non-empty array of types");
880 const targets = [...this._targets].filter(target =>
881 types.some(type => this._matchTargetType(type, target))
888 * Retrieve all the target fronts in the selected target tree (including the selected
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.
895 async getAllTargetsInSelectedTargetTree(types) {
896 const allTargets = this.getAllTargets(types);
897 if (this.isTopLevelTargetSelected()) {
901 const targets = [this.selectedTargetFront];
902 for (const target of allTargets) {
903 const isInSelectedTree = await target.isTargetAnAncestor(
904 this.selectedTargetFront
907 if (isInSelectedTree) {
908 targets.push(target);
915 * For all the target fronts of given types, retrieve all the target-scoped fronts of the given types.
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.
929 { onlyInSelectedTargetTree = false } = {}
931 if (!Array.isArray(targetTypes) || !targetTypes?.length) {
932 throw new Error("getAllFronts expects a non-empty array of target types");
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.
944 (frontType == "thread" && !target.targetForm.threadActor) ||
945 (frontType == "console" && !target.targetForm.consoleActor) ||
951 promises.push(target.getFront(frontType));
953 return Promise.all(promises);
957 * This function is triggered by an event sent by the TabDescriptor when
958 * the tab navigates to a distinct process.
960 * @param TargetFront targetFront
961 * The WindowGlobalTargetFront instance that navigated to another process
963 async onLocalTabRemotenessChange(targetFront) {
964 if (this.isServerTargetSwitchingEnabled()) {
965 // For server-side target switching, everything will be handled by the
966 // _onTargetAvailable callback.
970 // TabDescriptor may emit the event with a null targetFront, interpret that as if the previous target
971 // has already been destroyed
973 // Wait for the target to be destroyed so that LocalTabCommandsFactory clears its memoized target for this tab
974 await targetFront.once("target-destroyed");
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.
985 `Couldn't get the target for descriptor ${this.descriptorFront.actorID}`
990 this.switchToTarget(newTarget);
994 * Reload the current top level target.
995 * This only works for targets inheriting from WindowGlobalTarget.
997 * @param {Boolean} bypassCache
998 * If true, the reload will be forced to bypass any cache.
1000 async reloadTopLevelTarget(bypassCache = false) {
1001 if (!this.descriptorFront.traits.supportsReloadDescriptor) {
1002 throw new Error("The top level target doesn't support being reloaded");
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,
1013 ignoreExistingResources: true,
1014 predicate(resource) {
1015 return resource.name == "dom-complete";
1020 await this.descriptorFront.reloadDescriptor({ bypassCache });
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.
1029 * @param {TargetFront} newTarget
1030 * The new top level target to debug.
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);
1039 * Called when the user selects a frame in the iframe picker.
1041 * @param {WindowGlobalTargetFront} targetFront
1042 * The target front we want the toolbox to focus on.
1044 selectTarget(targetFront) {
1045 return this._onTargetSelected(targetFront);
1049 * Returns true if the top-level frame is the selected one
1051 * @returns {Boolean}
1053 isTopLevelTargetSelected() {
1054 return this.selectedTargetFront === this.targetFront;
1058 * Returns true if a non top-level frame is the selected one in the iframe picker.
1060 * @returns {Boolean}
1062 isNonTopLevelTargetSelected() {
1063 return this.selectedTargetFront !== this.targetFront;
1066 isTargetRegistered(targetFront) {
1067 return this._targets.has(targetFront);
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
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.
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
1107 return this._isDestroyed;
1110 isServerTargetSwitchingEnabled() {
1111 if (this.descriptorFront.isServerTargetSwitchingEnabled) {
1112 return this.descriptorFront.isServerTargetSwitchingEnabled();
1117 _isValidTargetType(type) {
1118 return this.ALL_TYPES.includes(type);
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
1138 * All types of target:
1140 TargetCommand.TYPES = TargetCommand.prototype.TYPES = {
1144 SHARED_WORKER: "shared_worker",
1145 SERVICE_WORKER: "service_worker",
1147 TargetCommand.ALL_TYPES = TargetCommand.prototype.ALL_TYPES = Object.values(
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;