summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/target/target-command.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /devtools/shared/commands/target/target-command.js
parentInitial commit. (diff)
downloadfirefox-esr-upstream/115.8.0esr.tar.xz
firefox-esr-upstream/115.8.0esr.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared/commands/target/target-command.js')
-rw-r--r--devtools/shared/commands/target/target-command.js1173
1 files changed, 1173 insertions, 0 deletions
diff --git a/devtools/shared/commands/target/target-command.js b/devtools/shared/commands/target/target-command.js
new file mode 100644
index 0000000000..28e70c9f4b
--- /dev/null
+++ b/devtools/shared/commands/target/target-command.js
@@ -0,0 +1,1173 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+const BROWSERTOOLBOX_SCOPE_PREF = "devtools.browsertoolbox.scope";
+// Possible values of the previous pref:
+const BROWSERTOOLBOX_SCOPE_EVERYTHING = "everything";
+const BROWSERTOOLBOX_SCOPE_PARENTPROCESS = "parent-process";
+
+// eslint-disable-next-line mozilla/reject-some-requires
+const createStore = require("resource://devtools/client/shared/redux/create-store.js");
+const reducer = require("resource://devtools/shared/commands/target/reducers/targets.js");
+
+loader.lazyRequireGetter(
+ this,
+ ["refreshTargets", "registerTarget", "unregisterTarget"],
+ "resource://devtools/shared/commands/target/actions/targets.js",
+ true
+);
+
+class TargetCommand extends EventEmitter {
+ #selectedTargetFront;
+ /**
+ * This class helps managing, iterating over and listening for Targets.
+ *
+ * It exposes:
+ * - the top level target, typically the main process target for the browser toolbox
+ * or the browsing context target for a regular web toolbox
+ * - target of remoted iframe, in case Fission is enabled and some <iframe>
+ * are running in a distinct process
+ * - target switching. If the top level target changes for a new one,
+ * all the targets are going to be declared as destroyed and the new ones
+ * will be notified to the user of this API.
+ *
+ * @fires target-tread-wrong-order-on-resume : An event that is emitted when resuming
+ * the thread throws with the "wrongOrder" error.
+ *
+ * @param {DescriptorFront} descriptorFront
+ * The context to inspector identified by this descriptor.
+ * @param {WatcherFront} watcherFront
+ * If available, a reference to the related Watcher Front.
+ * @param {Object} commands
+ * The commands object with all interfaces defined from devtools/shared/commands/
+ */
+ constructor({ descriptorFront, watcherFront, commands }) {
+ super();
+
+ this.commands = commands;
+ this.descriptorFront = descriptorFront;
+ this.watcherFront = watcherFront;
+ this.rootFront = descriptorFront.client.mainRoot;
+
+ this.store = createStore(reducer);
+ // Name of the store used when calling createProvider.
+ this.storeId = "target-store";
+
+ this._updateBrowserToolboxScope =
+ this._updateBrowserToolboxScope.bind(this);
+
+ Services.prefs.addObserver(
+ BROWSERTOOLBOX_SCOPE_PREF,
+ this._updateBrowserToolboxScope
+ );
+ // Until Watcher actor notify about new top level target when navigating to another process
+ // we have to manually switch to a new target from the client side
+ this.onLocalTabRemotenessChange =
+ this.onLocalTabRemotenessChange.bind(this);
+ if (this.descriptorFront.isTabDescriptor) {
+ this.descriptorFront.on(
+ "remoteness-change",
+ this.onLocalTabRemotenessChange
+ );
+ }
+
+ if (this.isServerTargetSwitchingEnabled()) {
+ // XXX: Will only be used for local tab server side target switching if
+ // the first target is generated from the server.
+ this._onFirstTarget = new Promise(r => (this._resolveOnFirstTarget = r));
+ }
+
+ // Reports if we have at least one listener for the given target type
+ this._listenersStarted = new Set();
+
+ // List of all the target fronts
+ this._targets = new Set();
+ // {Map<Function, Set<targetFront>>} A Map keyed by `onAvailable` function passed to
+ // `watchTargets`, whose initial value is a Set of the existing target fronts at the
+ // time watchTargets is called.
+ this._pendingWatchTargetInitialization = new Map();
+
+ // Listeners for target creation, destruction and selection
+ this._createListeners = new EventEmitter();
+ this._destroyListeners = new EventEmitter();
+ this._selectListeners = new EventEmitter();
+
+ this._onTargetAvailable = this._onTargetAvailable.bind(this);
+ this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
+ this._onTargetSelected = this._onTargetSelected.bind(this);
+ // Bug 1675763: Watcher actor is not available in all situations yet.
+ if (this.watcherFront) {
+ this.watcherFront.on("target-available", this._onTargetAvailable);
+ this.watcherFront.on("target-destroyed", this._onTargetDestroyed);
+ }
+
+ this.legacyImplementation = {};
+
+ // Public flag to allow listening for workers even if the fission pref is off
+ // This allows listening for workers in the content toolbox outside of fission contexts
+ // For now, this is only toggled by tests.
+ this.listenForWorkers =
+ this.rootFront.traits.workerConsoleApiMessagesDispatchedToMainThread ===
+ false;
+ this.listenForServiceWorkers = false;
+ this.destroyServiceWorkersOnNavigation = false;
+
+ // Tells us if we received the first top level target.
+ // If target switching is done on:
+ // * client side, this is done from startListening => _createFirstTarget
+ // and pull from the Descriptor front.
+ // * server side, this is also done from startListening,
+ // but we wait for the watcher actor to notify us about it
+ // via target-available-form avent.
+ this._gotFirstTopLevelTarget = false;
+ this._onResourceAvailable = this._onResourceAvailable.bind(this);
+ }
+
+ get selectedTargetFront() {
+ return this.#selectedTargetFront || this.targetFront;
+ }
+
+ /**
+ * Called fired when BROWSERTOOLBOX_SCOPE_PREF pref changes.
+ * This will enable/disable the full multiprocess debugging.
+ * When enabled we will watch for content process targets and debug all the processes.
+ * When disabled we will only watch for FRAME and WORKER and restrict ourself to parent process resources.
+ */
+ _updateBrowserToolboxScope() {
+ const browserToolboxScope = Services.prefs.getCharPref(
+ BROWSERTOOLBOX_SCOPE_PREF
+ );
+ if (browserToolboxScope == BROWSERTOOLBOX_SCOPE_EVERYTHING) {
+ // Force listening to new additional target types
+ this.startListening();
+ } else if (browserToolboxScope == BROWSERTOOLBOX_SCOPE_PARENTPROCESS) {
+ const disabledTargetTypes = [
+ TargetCommand.TYPES.FRAME,
+ TargetCommand.TYPES.PROCESS,
+ ];
+ // Force unwatching for additional targets types
+ // (we keep listening to workers)
+ // The related targets will be destroyed by the server
+ // and reported as destroyed to the frontend.
+ for (const type of disabledTargetTypes) {
+ this.stopListeningForType(type, {
+ isTargetSwitching: false,
+ isModeSwitching: true,
+ });
+ }
+ }
+ }
+
+ // Called whenever a new Target front is available.
+ // Either because a target was already available as we started calling startListening
+ // or if it has just been created
+ async _onTargetAvailable(targetFront) {
+ // We put the `commands` on the targetFront so it can be retrieved from any front easily.
+ // Without this, protocol.js fronts won't have any easy access to it.
+ // Ideally, Fronts would all be migrated to commands and we would no longer need this hack.
+ targetFront.commands = this.commands;
+
+ // If the new target is a top level target, we are target switching.
+ // Target-switching is only triggered for "local-tab" browsing-context
+ // targets which should always have the topLevelTarget flag initialized
+ // on the server.
+ const isTargetSwitching = targetFront.isTopLevel;
+ const isFirstTarget =
+ targetFront.isTopLevel && !this._gotFirstTopLevelTarget;
+
+ if (this._targets.has(targetFront)) {
+ // The top level target front can be reported via listProcesses in the
+ // case of the BrowserToolbox. For any other target, log an error if it is
+ // already registered.
+ if (targetFront != this.targetFront) {
+ console.error(
+ "Target is already registered in the TargetCommand",
+ targetFront.actorID
+ );
+ }
+ return;
+ }
+
+ if (this.isDestroyed() || targetFront.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ // Handle top level target switching
+ // Note that, for now, `_onTargetAvailable` isn't called for the *initial* top level target.
+ // i.e. the one that is passed to TargetCommand constructor.
+ if (targetFront.isTopLevel) {
+ // First report that all existing targets are destroyed
+ if (!isFirstTarget) {
+ this._destroyExistingTargetsOnTargetSwitching();
+ }
+
+ // Update the reference to the memoized top level target
+ this.targetFront = targetFront;
+ this.descriptorFront.setTarget(targetFront);
+ this.#selectedTargetFront = null;
+
+ if (isFirstTarget && this.isServerTargetSwitchingEnabled()) {
+ this._gotFirstTopLevelTarget = true;
+ this._resolveOnFirstTarget();
+ }
+ }
+
+ // Map the descriptor typeName to a target type.
+ const targetType = this.getTargetType(targetFront);
+ targetFront.setTargetType(targetType);
+
+ this._targets.add(targetFront);
+ try {
+ await targetFront.attachAndInitThread(this);
+ } catch (e) {
+ console.error("Error when attaching target:", e);
+ this._targets.delete(targetFront);
+ return;
+ }
+
+ for (const targetFrontsSet of this._pendingWatchTargetInitialization.values()) {
+ targetFrontsSet.delete(targetFront);
+ }
+
+ if (this.isDestroyed() || targetFront.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ this.store.dispatch(registerTarget(targetFront));
+
+ // Then, once the target is attached, notify the target front creation listeners
+ await this._createListeners.emitAsync(targetType, {
+ targetFront,
+ isTargetSwitching,
+ });
+
+ // Re-register the listeners as the top level target changed
+ // and some targets are fetched from it
+ if (targetFront.isTopLevel && !isFirstTarget) {
+ await this.startListening({ isTargetSwitching: true });
+ }
+
+ // These two events are used by tests using the production codepath (i.e. disabling flags.testing)
+ // To be consumed by tests triggering frame navigations, spawning workers...
+ this.emit("processed-available-target", targetFront);
+
+ if (isTargetSwitching) {
+ this.emit("switched-target", targetFront);
+ }
+ }
+
+ _destroyExistingTargetsOnTargetSwitching() {
+ const destroyedTargets = [];
+ for (const target of this._targets) {
+ // We only consider the top level target to be switched
+ const isDestroyedTargetSwitching = target == this.targetFront;
+ const isServiceWorker = target.targetType === this.TYPES.SERVICE_WORKER;
+ const isPopup = target.targetForm.isPopup;
+
+ // Never destroy the popup targets when the top level target is destroyed
+ // as the popup follow a different lifecycle.
+ // Also avoid destroying service worker targets for similar reason,
+ // unless this.destroyServiceWorkersOnNavigation is true.
+ if (
+ !isPopup &&
+ (!isServiceWorker || this.destroyServiceWorkersOnNavigation)
+ ) {
+ this._onTargetDestroyed(target, {
+ isTargetSwitching: isDestroyedTargetSwitching,
+ // Do not destroy service worker front as we may want to keep using it.
+ shouldDestroyTargetFront: !isServiceWorker,
+ });
+ destroyedTargets.push(target);
+ }
+ }
+
+ // Stop listening to legacy listeners as we now have to listen
+ // on the new target.
+ this.stopListening({ isTargetSwitching: true });
+
+ // Remove destroyed target from the cached target list. We don't simply clear the
+ // Map as SW targets might not have been destroyed (i.e. when destroyServiceWorkersOnNavigation
+ // is set to false).
+ for (const target of destroyedTargets) {
+ this._targets.delete(target);
+ }
+ }
+
+ /**
+ * Function fired everytime a target is destroyed.
+ *
+ * This is called either:
+ * - via target-destroyed event fired by the WatcherFront,
+ * event which is a simple translation of the target-destroyed-form emitted by the WatcherActor.
+ * Watcher Actor emits this is various condition when the debugged target is meant to be destroyed:
+ * - the related target context is destroyed (tab closed, worker shut down, content process destroyed, ...),
+ * - when the DevToolsServerConnection used on the server side to communicate to the client is closed.
+
+ * - by TargetCommand._onTargetAvailable, when a top level target switching happens and all previously
+ * registered target fronts should be destroyed.
+
+ * - by the legacy Targets listeners, calling this method directly.
+ * This usecase is meant to be removed someday when all target targets are supported by the Watcher.
+ * (bug 1687459)
+ *
+ * @param {TargetFront} targetFront
+ * The target that just got destroyed.
+ * @param {Object} options
+ * @param {Boolean} [options.isTargetSwitching]
+ * To be set to true when this is about the top level target which is being replaced
+ * by a new one.
+ * The passed target should be still the one store in TargetCommand.targetFront
+ * and will be replaced via a call to onTargetAvailable with a new target front.
+ * @param {Boolean} [options.isModeSwitching]
+ * To be set to true when the target was destroyed was called as the result of a
+ * change to the devtools.browsertoolbox.scope pref.
+ * @param {Boolean} [options.shouldDestroyTargetFront]
+ * By default, the passed target front will be destroyed. But in some cases like
+ * legacy listeners for service workers we want to keep the front alive.
+ */
+ _onTargetDestroyed(
+ targetFront,
+ {
+ isModeSwitching = false,
+ isTargetSwitching = false,
+ shouldDestroyTargetFront = true,
+ } = {}
+ ) {
+ // The watcher actor may notify us about the destruction of the top level target.
+ // But second argument to this method, isTargetSwitching is only passed from the frontend.
+ // So automatically toggle the isTargetSwitching flag for server side destructions
+ // only if that's about the existing top level target.
+ if (targetFront == this.targetFront) {
+ isTargetSwitching = true;
+ }
+ this._destroyListeners.emit(targetFront.targetType, {
+ targetFront,
+ isTargetSwitching,
+ isModeSwitching,
+ });
+ this._targets.delete(targetFront);
+
+ this.store.dispatch(unregisterTarget(targetFront));
+
+ // If the destroyed target was the selected one, we need to do some cleanup
+ if (this.#selectedTargetFront == targetFront) {
+ // If we're doing a targetSwitch, simply nullify #selectedTargetFront
+ if (isTargetSwitching) {
+ this.#selectedTargetFront = null;
+ } else {
+ // Otherwise we want to select the top level target
+ this.selectTarget(this.targetFront);
+ }
+ }
+
+ if (shouldDestroyTargetFront) {
+ // When calling targetFront.destroy(), we will first call TargetFrontMixin.destroy,
+ // which will try to call `detach` RDP method.
+ // Unfortunately, this request will never complete in some cases like bfcache navigations.
+ // Because of that, the target front will never be completely destroy as it will prevent
+ // calling super.destroy and Front.destroy.
+ // Workaround that by manually calling Front class destroy method:
+ targetFront.baseFrontClassDestroy();
+
+ targetFront.destroy();
+
+ // Delete the attribute we set from _onTargetAvailable so that we avoid leaking commands
+ // if any target front is leaked.
+ delete targetFront.commands;
+ }
+ }
+
+ /**
+ *
+ * @param {TargetFront} targetFront
+ */
+ async _onTargetSelected(targetFront) {
+ if (this.#selectedTargetFront == targetFront) {
+ // Target is already selected, we can bail out.
+ return;
+ }
+
+ this.#selectedTargetFront = targetFront;
+ const targetType = this.getTargetType(targetFront);
+ await this._selectListeners.emitAsync(targetType, {
+ targetFront,
+ });
+ }
+
+ _setListening(type, value) {
+ if (value) {
+ this._listenersStarted.add(type);
+ } else {
+ this._listenersStarted.delete(type);
+ }
+ }
+
+ _isListening(type) {
+ return this._listenersStarted.has(type);
+ }
+
+ /**
+ * Check if the watcher is currently supported.
+ *
+ * When no typeOrTrait is provided, we will only check that the watcher is
+ * available.
+ *
+ * When a typeOrTrait is provided, we will check for an explicit trait on the
+ * watcherFront that indicates either that:
+ * - a target type is supported
+ * - or that a custom trait is true
+ *
+ * @param {String} [targetTypeOrTrait]
+ * Optional target type or trait.
+ * @return {Boolean} true if the watcher is available and supports the
+ * optional targetTypeOrTrait
+ */
+ hasTargetWatcherSupport(targetTypeOrTrait) {
+ if (targetTypeOrTrait) {
+ // Target types are also exposed as traits, where resource types are
+ // exposed under traits.resources (cf hasResourceWatcherSupport
+ // implementation).
+ return !!this.watcherFront?.traits[targetTypeOrTrait];
+ }
+
+ return !!this.watcherFront;
+ }
+
+ /**
+ * Start listening for targets from the server
+ *
+ * Interact with the actors in order to start listening for new types of targets.
+ * This will fire the _onTargetAvailable function for all already-existing targets,
+ * as well as the next one to be created. It will also call _onTargetDestroyed
+ * everytime a target is reported as destroyed by the actors.
+ * By the time this function resolves, all the already-existing targets will be
+ * reported to _onTargetAvailable.
+ *
+ * @param Object options
+ * @param Boolean options.isTargetSwitching
+ * Set to true when this is called while a target switching happens. In such case,
+ * we won't register listener set on the Watcher Actor, but still register listeners
+ * set via Legacy Listeners.
+ */
+ async startListening({ isTargetSwitching = false } = {}) {
+ // The first time we call this method, we pull the current top level target from the descriptor
+ if (
+ !this.isServerTargetSwitchingEnabled() &&
+ !this._gotFirstTopLevelTarget
+ ) {
+ await this._createFirstTarget();
+ }
+
+ // If no pref are set to true, nor is listenForWorkers set to true,
+ // we won't listen for any additional target. Only the top level target
+ // will be managed. We may still do target-switching.
+ const types = this._computeTargetTypes();
+
+ for (const type of types) {
+ if (this._isListening(type)) {
+ continue;
+ }
+ this._setListening(type, true);
+
+ // Only a few top level targets support the watcher actor at the moment (see WatcherActor
+ // traits in the _form method). Bug 1675763 tracks watcher actor support for all targets.
+ if (this.hasTargetWatcherSupport(type)) {
+ // When we switch to a new top level target, we don't have to stop and restart
+ // Watcher listener as it is independant from the top level target.
+ // This isn't the case for some Legacy Listeners, which fetch targets from the top level target
+ if (!isTargetSwitching) {
+ await this.watcherFront.watchTargets(type);
+ }
+ } else if (LegacyTargetWatchers[type]) {
+ // Instantiate the legacy listener only once for each TargetCommand, and reuse it if we stop and restart listening
+ if (!this.legacyImplementation[type]) {
+ this.legacyImplementation[type] = new LegacyTargetWatchers[type](
+ this,
+ this._onTargetAvailable,
+ this._onTargetDestroyed,
+ this.commands
+ );
+ }
+ await this.legacyImplementation[type].listen();
+ } else {
+ throw new Error(`Unsupported target type '${type}'`);
+ }
+ }
+
+ if (!this._watchingDocumentEvent && !this.isDestroyed()) {
+ // We want to watch DOCUMENT_EVENT in order to update the url and title of target fronts,
+ // as the initial value that is set in them might be erroneous (if the target was
+ // created so early that the document url is still pointing to about:blank and the
+ // html hasn't be parsed yet, so we can't know the <title> content).
+
+ this._watchingDocumentEvent = true;
+ await this.commands.resourceCommand.watchResources(
+ [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this._onResourceAvailable,
+ }
+ );
+ }
+
+ if (this.isServerTargetSwitchingEnabled()) {
+ await this._onFirstTarget;
+ }
+ }
+
+ async _createFirstTarget() {
+ // Note that this is a public attribute, used outside of this class
+ // and helps knowing what is the current top level target we debug.
+ this.targetFront = await this.descriptorFront.getTarget();
+ this.targetFront.setTargetType(this.getTargetType(this.targetFront));
+ this.targetFront.setIsTopLevel(true);
+ this._gotFirstTopLevelTarget = true;
+
+ // See _onTargetAvailable. As this target isn't going through that method
+ // we have to replicate doing that here.
+ this.targetFront.commands = this.commands;
+
+ // Add the top-level target to the list of targets.
+ this._targets.add(this.targetFront);
+ this.store.dispatch(registerTarget(this.targetFront));
+ }
+
+ _computeTargetTypes() {
+ let types = [];
+
+ // We also check for watcher support as some xpcshell tests uses legacy APIs and don't support frames.
+ if (
+ this.descriptorFront.isTabDescriptor &&
+ this.hasTargetWatcherSupport(TargetCommand.TYPES.FRAME)
+ ) {
+ types = [TargetCommand.TYPES.FRAME];
+ } else if (this.descriptorFront.isBrowserProcessDescriptor) {
+ const browserToolboxScope = Services.prefs.getCharPref(
+ BROWSERTOOLBOX_SCOPE_PREF
+ );
+ if (browserToolboxScope == BROWSERTOOLBOX_SCOPE_EVERYTHING) {
+ types = TargetCommand.ALL_TYPES;
+ }
+ }
+ if (this.listenForWorkers && !types.includes(TargetCommand.TYPES.WORKER)) {
+ types.push(TargetCommand.TYPES.WORKER);
+ }
+ if (
+ this.listenForWorkers &&
+ !types.includes(TargetCommand.TYPES.SHARED_WORKER)
+ ) {
+ types.push(TargetCommand.TYPES.SHARED_WORKER);
+ }
+ if (
+ this.listenForServiceWorkers &&
+ !types.includes(TargetCommand.TYPES.SERVICE_WORKER)
+ ) {
+ types.push(TargetCommand.TYPES.SERVICE_WORKER);
+ }
+
+ return types;
+ }
+
+ /**
+ * Stop listening for targets from the server
+ *
+ * @param Object options
+ * @param Boolean options.isTargetSwitching
+ * Set to true when this is called while a target switching happens. In such case,
+ * we won't unregister listener set on the Watcher Actor, but still unregister
+ * listeners set via Legacy Listeners.
+ */
+ stopListening({ isTargetSwitching = false } = {}) {
+ // As DOCUMENT_EVENT isn't using legacy listener,
+ // there is no need to stop and restart it in case of target switching.
+ if (this._watchingDocumentEvent && !isTargetSwitching) {
+ this.commands.resourceCommand.unwatchResources(
+ [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this._onResourceAvailable,
+ }
+ );
+ this._watchingDocumentEvent = false;
+ }
+
+ for (const type of TargetCommand.ALL_TYPES) {
+ this.stopListeningForType(type, { isTargetSwitching });
+ }
+ }
+
+ /**
+ * Stop listening for targets of a given type from the server
+ *
+ * @param String type
+ * target type we want to stop listening for
+ * @param Object options
+ * @param Boolean options.isTargetSwitching
+ * Set to true when this is called while a target switching happens. In such case,
+ * we won't unregister listener set on the Watcher Actor, but still unregister
+ * listeners set via Legacy Listeners.
+ * @param Boolean options.isModeSwitching
+ * Set to true when this is called as the result of a change to the
+ * devtools.browsertoolbox.scope pref.
+ */
+ stopListeningForType(type, { isTargetSwitching, isModeSwitching }) {
+ if (!this._isListening(type)) {
+ return;
+ }
+ this._setListening(type, false);
+
+ // Only a few top level targets support the watcher actor at the moment (see WatcherActor
+ // traits in the _form method). Bug 1675763 tracks watcher actor support for all targets.
+ if (this.hasTargetWatcherSupport(type)) {
+ // When we switch to a new top level target, we don't have to stop and restart
+ // Watcher listener as it is independant from the top level target.
+ // This isn't the case for some Legacy Listeners, which fetch targets from the top level target
+ // Also, TargetCommand.destroy may be called after the client is closed.
+ // So avoid calling the RDP method in that situation.
+ if (!isTargetSwitching && !this.watcherFront.isDestroyed()) {
+ this.watcherFront.unwatchTargets(type, { isModeSwitching });
+ }
+ } else if (this.legacyImplementation[type]) {
+ this.legacyImplementation[type].unlisten({
+ isTargetSwitching,
+ isModeSwitching,
+ });
+ } else {
+ throw new Error(`Unsupported target type '${type}'`);
+ }
+ }
+
+ getTargetType(target) {
+ const { typeName } = target;
+ if (typeName == "windowGlobalTarget") {
+ return TargetCommand.TYPES.FRAME;
+ }
+
+ if (
+ typeName == "contentProcessTarget" ||
+ typeName == "parentProcessTarget"
+ ) {
+ return TargetCommand.TYPES.PROCESS;
+ }
+
+ if (typeName == "workerDescriptor" || typeName == "workerTarget") {
+ if (target.isSharedWorker) {
+ return TargetCommand.TYPES.SHARED_WORKER;
+ }
+
+ if (target.isServiceWorker) {
+ return TargetCommand.TYPES.SERVICE_WORKER;
+ }
+
+ return TargetCommand.TYPES.WORKER;
+ }
+
+ throw new Error("Unsupported target typeName: " + typeName);
+ }
+
+ _matchTargetType(type, target) {
+ return type === target.targetType;
+ }
+
+ _onResourceAvailable(resources) {
+ for (const resource of resources) {
+ if (
+ resource.resourceType ===
+ this.commands.resourceCommand.TYPES.DOCUMENT_EVENT
+ ) {
+ const { targetFront } = resource;
+ if (resource.title !== undefined && targetFront?.setTitle) {
+ targetFront.setTitle(resource.title);
+ }
+ if (resource.url !== undefined && targetFront?.setUrl) {
+ targetFront.setUrl(resource.url);
+ }
+ if (
+ !resource.isFrameSwitching &&
+ // `url` is set on the targetFront when we receive dom-loading, and `title` when
+ // `dom-interactive` is received. Here we're only updating the window title in
+ // the "newer" event.
+ resource.name === "dom-interactive"
+ ) {
+ // We just updated the targetFront title and url, force a refresh
+ // so that the EvaluationContext selector update them.
+ this.store.dispatch(refreshTargets());
+ }
+ }
+ }
+ }
+
+ /**
+ * Listen for the creation and/or destruction of target fronts matching one of the provided types.
+ *
+ * @param {Object} options
+ * @param {Array<String>} options.types
+ * The type of target to listen for. Constant of TargetCommand.TYPES.
+ * @param {Function} options.onAvailable
+ * Mandatory callback fired when a target has been just created or was already available.
+ * The function is called with a single object argument containing the following properties:
+ * - {TargetFront} targetFront: The target Front
+ * - {Boolean} isTargetSwitching: Is this target relates to a navigation and
+ * this replaced a previously available target, this flag will be true
+ * @param {Function} options.onDestroyed
+ * Optional callback fired in case of target front destruction.
+ * The function is called with the same arguments than onAvailable.
+ * @param {Function} options.onSelected
+ * Optional callback fired when a given target is selected from the iframe picker
+ * The function is called with a single object argument containing the following properties:
+ * - {TargetFront} targetFront: The target Front
+ */
+ async watchTargets(options = {}) {
+ const availableOptions = [
+ "types",
+ "onAvailable",
+ "onDestroyed",
+ "onSelected",
+ ];
+ const unsupportedKeys = Object.keys(options).filter(
+ key => !availableOptions.includes(key)
+ );
+ if (unsupportedKeys.length) {
+ throw new Error(
+ `TargetCommand.watchTargets does not expect the following options: ${unsupportedKeys.join(
+ ", "
+ )}`
+ );
+ }
+
+ const { types, onAvailable, onDestroyed, onSelected } = options;
+ if (typeof onAvailable != "function") {
+ throw new Error(
+ "TargetCommand.watchTargets expects a function for the onAvailable option"
+ );
+ }
+
+ for (const type of types) {
+ if (!this._isValidTargetType(type)) {
+ throw new Error(
+ `TargetCommand.watchTargets invoked with an unknown type: "${type}"`
+ );
+ }
+ }
+
+ // Notify about already existing target of these types
+ const targetFronts = [...this._targets].filter(targetFront =>
+ types.includes(targetFront.targetType)
+ );
+ this._pendingWatchTargetInitialization.set(
+ onAvailable,
+ new Set(targetFronts)
+ );
+ const promises = targetFronts.map(async targetFront => {
+ // Attach the targets that aren't attached yet (e.g. the initial top-level target),
+ // and wait for the other ones to be fully attached.
+ try {
+ await targetFront.attachAndInitThread(this);
+ } catch (e) {
+ console.error("Error when attaching target:", e);
+ return;
+ }
+
+ // It can happen that onAvailable was already called with this targetFront at
+ // this time (via _onTargetAvailable). If that's the case, we don't want to call
+ // onAvailable a second time.
+ if (
+ this._pendingWatchTargetInitialization &&
+ this._pendingWatchTargetInitialization.has(onAvailable) &&
+ !this._pendingWatchTargetInitialization
+ .get(onAvailable)
+ .has(targetFront)
+ ) {
+ return;
+ }
+
+ try {
+ // Ensure waiting for eventual async create listeners
+ // which may setup things regarding the existing targets
+ // and listen callsite may care about the full initialization
+ await onAvailable({
+ targetFront,
+ isTargetSwitching: false,
+ });
+ } catch (e) {
+ // Prevent throwing when onAvailable handler throws on one target
+ // so that it can try to register the other targets
+ console.error(
+ "Exception when calling onAvailable handler",
+ e.message,
+ e
+ );
+ }
+ });
+
+ for (const type of types) {
+ this._createListeners.on(type, onAvailable);
+ if (onDestroyed) {
+ this._destroyListeners.on(type, onDestroyed);
+ }
+ if (onSelected) {
+ this._selectListeners.on(type, onSelected);
+ }
+ }
+
+ await Promise.all(promises);
+ this._pendingWatchTargetInitialization.delete(onAvailable);
+ }
+
+ /**
+ * Stop listening for the creation and/or destruction of a given type of target fronts.
+ * See `watchTargets()` for documentation of the arguments.
+ */
+ unwatchTargets(options = {}) {
+ const availableOptions = [
+ "types",
+ "onAvailable",
+ "onDestroyed",
+ "onSelected",
+ ];
+ const unsupportedKeys = Object.keys(options).filter(
+ key => !availableOptions.includes(key)
+ );
+ if (unsupportedKeys.length) {
+ throw new Error(
+ `TargetCommand.unwatchTargets does not expect the following options: ${unsupportedKeys.join(
+ ", "
+ )}`
+ );
+ }
+
+ const { types, onAvailable, onDestroyed, onSelected } = options;
+ if (typeof onAvailable != "function") {
+ throw new Error(
+ "TargetCommand.unwatchTargets expects a function for the onAvailable option"
+ );
+ }
+
+ for (const type of types) {
+ if (!this._isValidTargetType(type)) {
+ throw new Error(
+ `TargetCommand.unwatchTargets invoked with an unknown type: "${type}"`
+ );
+ }
+
+ this._createListeners.off(type, onAvailable);
+ if (onDestroyed) {
+ this._destroyListeners.off(type, onDestroyed);
+ }
+ if (onSelected) {
+ this._selectListeners.off(type, onSelected);
+ }
+ }
+ this._pendingWatchTargetInitialization.delete(onAvailable);
+ }
+
+ /**
+ * Retrieve all the current target fronts of a given type.
+ *
+ * @param {Array<String>} types
+ * The types of target to retrieve. Array of TargetCommand.TYPES
+ * @return {Array<TargetFront>} Array of target fronts matching any of the
+ * provided types.
+ */
+ getAllTargets(types) {
+ if (!types?.length) {
+ throw new Error("getAllTargets expects a non-empty array of types");
+ }
+
+ const targets = [...this._targets].filter(target =>
+ types.some(type => this._matchTargetType(type, target))
+ );
+
+ return targets;
+ }
+
+ /**
+ * Retrieve all the target fronts in the selected target tree (including the selected
+ * target itself).
+ *
+ * @param {Array<String>} types
+ * The types of target to retrieve. Array of TargetCommand.TYPES
+ * @return {Promise<Array<TargetFront>>} Promise that resolves to an array of target fronts.
+ */
+ async getAllTargetsInSelectedTargetTree(types) {
+ const allTargets = this.getAllTargets(types);
+ if (this.isTopLevelTargetSelected()) {
+ return allTargets;
+ }
+
+ const targets = [this.selectedTargetFront];
+ for (const target of allTargets) {
+ const isInSelectedTree = await target.isTargetAnAncestor(
+ this.selectedTargetFront
+ );
+
+ if (isInSelectedTree) {
+ targets.push(target);
+ }
+ }
+ return targets;
+ }
+
+ /**
+ * For all the target fronts of given types, retrieve all the target-scoped fronts of the given types.
+ *
+ * @param {Array<String>} targetTypes
+ * The types of target to iterate over. Constant of TargetCommand.TYPES.
+ * @param {String} frontType
+ * The type of target-scoped front to retrieve. It can be "inspector", "console", "thread",...
+ * @param {Object} options
+ * @param {Boolean} options.onlyInSelectedTargetTree
+ * Set to true to only get the fronts for targets who are in the "targets tree"
+ * of the selected target.
+ */
+ async getAllFronts(
+ targetTypes,
+ frontType,
+ { onlyInSelectedTargetTree = false } = {}
+ ) {
+ if (!Array.isArray(targetTypes) || !targetTypes?.length) {
+ throw new Error("getAllFronts expects a non-empty array of target types");
+ }
+ const promises = [];
+ const targets = !onlyInSelectedTargetTree
+ ? this.getAllTargets(targetTypes)
+ : await this.getAllTargetsInSelectedTargetTree(targetTypes);
+ for (const target of targets) {
+ // For still-attaching worker targets, the thread or console front may not yet be available,
+ // whereas TargetMixin.getFront will throw if the actorID isn't available in targetForm.
+ // Also ignore destroyed targets. For some reason the previous methods fetching targets
+ // can sometime return destroyed targets.
+ if (
+ (frontType == "thread" && !target.targetForm.threadActor) ||
+ (frontType == "console" && !target.targetForm.consoleActor) ||
+ target.isDestroyed()
+ ) {
+ continue;
+ }
+
+ promises.push(target.getFront(frontType));
+ }
+ return Promise.all(promises);
+ }
+
+ /**
+ * This function is triggered by an event sent by the TabDescriptor when
+ * the tab navigates to a distinct process.
+ *
+ * @param TargetFront targetFront
+ * The WindowGlobalTargetFront instance that navigated to another process
+ */
+ async onLocalTabRemotenessChange(targetFront) {
+ if (this.isServerTargetSwitchingEnabled()) {
+ // For server-side target switching, everything will be handled by the
+ // _onTargetAvailable callback.
+ return;
+ }
+
+ // TabDescriptor may emit the event with a null targetFront, interpret that as if the previous target
+ // has already been destroyed
+ if (targetFront) {
+ // Wait for the target to be destroyed so that LocalTabCommandsFactory clears its memoized target for this tab
+ await targetFront.once("target-destroyed");
+ }
+
+ // Fetch the new target from the descriptor.
+ const newTarget = await this.descriptorFront.getTarget();
+
+ // If a navigation happens while we try to get the target for the page that triggered
+ // the remoteness change, `getTarget` will return null. In such case, we'll get the
+ // "next" target through onTargetAvailable so it's safe to bail here.
+ if (!newTarget) {
+ console.warn(
+ `Couldn't get the target for descriptor ${this.descriptorFront.actorID}`
+ );
+ return;
+ }
+
+ this.switchToTarget(newTarget);
+ }
+
+ /**
+ * Reload the current top level target.
+ * This only works for targets inheriting from WindowGlobalTarget.
+ *
+ * @param {Boolean} bypassCache
+ * If true, the reload will be forced to bypass any cache.
+ */
+ async reloadTopLevelTarget(bypassCache = false) {
+ if (!this.descriptorFront.traits.supportsReloadDescriptor) {
+ throw new Error("The top level target doesn't support being reloaded");
+ }
+
+ // Wait for the next DOCUMENT_EVENT's dom-complete event
+ // Wait for waitForNextResource completion before reloading, otherwise we might miss the dom-complete event.
+ // This can happen if `ResourceCommand.watchResources` made by `waitForNextResource` is still pending
+ // while the reload already started and finished loading the document early.
+ const { onResource: onReloaded } =
+ await this.commands.resourceCommand.waitForNextResource(
+ this.commands.resourceCommand.TYPES.DOCUMENT_EVENT,
+ {
+ ignoreExistingResources: true,
+ predicate(resource) {
+ return resource.name == "dom-complete";
+ },
+ }
+ );
+
+ await this.descriptorFront.reloadDescriptor({ bypassCache });
+
+ await onReloaded;
+ }
+
+ /**
+ * Called when the top level target is replaced by a new one.
+ * Typically when we navigate to another domain which requires to be loaded in a distinct process.
+ *
+ * @param {TargetFront} newTarget
+ * The new top level target to debug.
+ */
+ async switchToTarget(newTarget) {
+ // Notify about this new target to creation listeners
+ // _onTargetAvailable will also destroy all previous target before notifying about this new one.
+ await this._onTargetAvailable(newTarget);
+ }
+
+ /**
+ * Called when the user selects a frame in the iframe picker.
+ *
+ * @param {WindowGlobalTargetFront} targetFront
+ * The target front we want the toolbox to focus on.
+ */
+ selectTarget(targetFront) {
+ return this._onTargetSelected(targetFront);
+ }
+
+ /**
+ * Returns true if the top-level frame is the selected one
+ *
+ * @returns {Boolean}
+ */
+ isTopLevelTargetSelected() {
+ return this.selectedTargetFront === this.targetFront;
+ }
+
+ /**
+ * Returns true if a non top-level frame is the selected one in the iframe picker.
+ *
+ * @returns {Boolean}
+ */
+ isNonTopLevelTargetSelected() {
+ return this.selectedTargetFront !== this.targetFront;
+ }
+
+ isTargetRegistered(targetFront) {
+ return this._targets.has(targetFront);
+ }
+
+ getParentTarget(targetFront) {
+ // Note that there are edgecases:
+ // * Until bug 1741927 is fixed and we remove non-EFT codepath entirely,
+ // we may receive a `parentInnerWindowId` that doesn't relate to any target.
+ // This happens when the parent document of the targetFront is a document loaded in the
+ // same process as its parent document. In such scenario, and only when EFT is disabled,
+ // we won't instantiate a target for the parent document of the targetFront.
+ // * `parentInnerWindowId` could be null in some case like for tabs in the MBT
+ // we should report the top level target as parent. That's what `getParentWindowGlobalTarget` does.
+ // Once we can stop using getParentWindowGlobalTarget for the other edgecase we will be able to
+ // replace it with such fallback: `return this.targetFront;`.
+ // browser_target_command_frames.js will help you get things right.
+ const { parentInnerWindowId } = targetFront.targetForm;
+ if (parentInnerWindowId) {
+ const targets = this.getAllTargets([TargetCommand.TYPES.FRAME]);
+ const parent = targets.find(
+ target => target.innerWindowId == parentInnerWindowId
+ );
+ // Until EFT is the only codepath supported (bug 1741927), we will fallback to `getParentWindowGlobalTarget`
+ // as we may not have a target if the parent is an iframe running in the same process as its parent.
+ if (parent) {
+ return parent;
+ }
+ }
+
+ // Note that all callsites which care about FRAME additional target
+ // should all have a toolbox using the watcher actor.
+ // It should be: MBT, regular tab toolbox and web extension.
+ // The others which still don't support watcher don't spawn FRAME targets:
+ // browser content toolbox and service workers.
+
+ return this.watcherFront.getParentWindowGlobalTarget(
+ targetFront.browsingContextID
+ );
+ }
+
+ isDestroyed() {
+ return this._isDestroyed;
+ }
+
+ isServerTargetSwitchingEnabled() {
+ if (this.descriptorFront.isServerTargetSwitchingEnabled) {
+ return this.descriptorFront.isServerTargetSwitchingEnabled();
+ }
+ return false;
+ }
+
+ _isValidTargetType(type) {
+ return this.ALL_TYPES.includes(type);
+ }
+
+ destroy() {
+ this.stopListening();
+ this._createListeners.off();
+ this._destroyListeners.off();
+ this._selectListeners.off();
+
+ this.#selectedTargetFront = null;
+ this._isDestroyed = true;
+
+ Services.prefs.removeObserver(
+ BROWSERTOOLBOX_SCOPE_PREF,
+ this._updateBrowserToolboxScope
+ );
+ }
+}
+
+/**
+ * All types of target:
+ */
+TargetCommand.TYPES = TargetCommand.prototype.TYPES = {
+ PROCESS: "process",
+ FRAME: "frame",
+ WORKER: "worker",
+ SHARED_WORKER: "shared_worker",
+ SERVICE_WORKER: "service_worker",
+};
+TargetCommand.ALL_TYPES = TargetCommand.prototype.ALL_TYPES = Object.values(
+ TargetCommand.TYPES
+);
+
+const LegacyTargetWatchers = {};
+loader.lazyRequireGetter(
+ LegacyTargetWatchers,
+ TargetCommand.TYPES.PROCESS,
+ "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js"
+);
+loader.lazyRequireGetter(
+ LegacyTargetWatchers,
+ TargetCommand.TYPES.WORKER,
+ "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js"
+);
+loader.lazyRequireGetter(
+ LegacyTargetWatchers,
+ TargetCommand.TYPES.SHARED_WORKER,
+ "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-sharedworkers-watcher.js"
+);
+loader.lazyRequireGetter(
+ LegacyTargetWatchers,
+ TargetCommand.TYPES.SERVICE_WORKER,
+ "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js"
+);
+
+module.exports = TargetCommand;