diff options
Diffstat (limited to 'devtools/client/fronts/descriptors/tab.js')
-rw-r--r-- | devtools/client/fronts/descriptors/tab.js | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/devtools/client/fronts/descriptors/tab.js b/devtools/client/fronts/descriptors/tab.js new file mode 100644 index 0000000000..097aed7674 --- /dev/null +++ b/devtools/client/fronts/descriptors/tab.js @@ -0,0 +1,330 @@ +/* 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 { + tabDescriptorSpec, +} = require("resource://devtools/shared/specs/descriptors/tab.js"); +const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js"); + +loader.lazyRequireGetter( + this, + "gDevTools", + "resource://devtools/client/framework/devtools.js", + true +); +loader.lazyRequireGetter( + this, + "WindowGlobalTargetFront", + "resource://devtools/client/fronts/targets/window-global.js", + true +); +const { + FrontClassWithSpec, + registerFront, +} = require("resource://devtools/shared/protocol.js"); +const { + DescriptorMixin, +} = require("resource://devtools/client/fronts/descriptors/descriptor-mixin.js"); + +const POPUP_DEBUG_PREF = "devtools.popups.debug"; + +/** + * DescriptorFront for tab targets. + * + * @fires remoteness-change + * Fired only for target switching, when the debugged tab is a local tab. + * TODO: This event could move to the server in order to support + * remoteness change for remote debugging. + */ +class TabDescriptorFront extends DescriptorMixin( + FrontClassWithSpec(tabDescriptorSpec) +) { + constructor(client, targetFront, parentFront) { + super(client, targetFront, parentFront); + + // The tab descriptor can be configured to create either local tab targets + // (eg, regular tab toolbox) or browsing context targets (eg tab remote + // debugging). + this._localTab = null; + + // Flag to prevent the server from trying to spawn targets by the watcher actor. + this._disableTargetSwitching = false; + + this._onTargetDestroyed = this._onTargetDestroyed.bind(this); + this._handleTabEvent = this._handleTabEvent.bind(this); + + // When the target is created from the server side, + // it is not created via TabDescriptor.getTarget. + // Instead, it is retrieved by the TargetCommand which + // will call TabDescriptor.setTarget from TargetCommand.onTargetAvailable + if (this.isServerTargetSwitchingEnabled()) { + this._targetFrontPromise = new Promise( + r => (this._resolveTargetFrontPromise = r) + ); + } + } + + descriptorType = DESCRIPTOR_TYPES.TAB; + + form(json) { + this.actorID = json.actor; + this._form = json; + this.traits = json.traits || {}; + } + + /** + * Destroy the front. + * + * @param Boolean If true, it means that we destroy the front when receiving the descriptor-destroyed + * event from the server. + */ + destroy({ isServerDestroyEvent = false } = {}) { + if (this.isDestroyed()) { + return; + } + + // The descriptor may be destroyed first by the frontend. + // When closing the tab, the toolbox document is almost immediately removed from the DOM. + // The `unload` event fires and toolbox destroys itself, as well as its related client. + // + // In such case, we emit the descriptor-destroyed event + if (!isServerDestroyEvent) { + this.emit("descriptor-destroyed"); + } + if (this.isLocalTab) { + this._teardownLocalTabListeners(); + } + super.destroy(); + } + + getWatcher() { + const isPopupDebuggingEnabled = Services.prefs.getBoolPref( + POPUP_DEBUG_PREF, + false + ); + return super.getWatcher({ + isServerTargetSwitchingEnabled: this.isServerTargetSwitchingEnabled(), + isPopupDebuggingEnabled, + }); + } + + setLocalTab(localTab) { + this._localTab = localTab; + this._setupLocalTabListeners(); + } + + get isTabDescriptor() { + return true; + } + + get isLocalTab() { + return !!this._localTab; + } + + get localTab() { + return this._localTab; + } + + _setupLocalTabListeners() { + this.localTab.addEventListener("TabClose", this._handleTabEvent); + this.localTab.addEventListener("TabRemotenessChange", this._handleTabEvent); + } + + _teardownLocalTabListeners() { + this.localTab.removeEventListener("TabClose", this._handleTabEvent); + this.localTab.removeEventListener( + "TabRemotenessChange", + this._handleTabEvent + ); + } + + isServerTargetSwitchingEnabled() { + return !this._disableTargetSwitching; + } + + /** + * Called by CommandsFactory, when the WebExtension codebase instantiates + * a commands. We have to flag the TabDescriptor for them as they don't support + * target switching and gets severely broken when enabling server target which + * introduce target switching for all navigations and reloads + */ + setIsForWebExtension() { + this.disableTargetSwitching(); + } + + /** + * Method used by the WebExtension which still need to disable server side targets, + * and also a few xpcshell tests which are using legacy API and don't support watcher actor. + */ + disableTargetSwitching() { + this._disableTargetSwitching = true; + // Delete these two attributes which have to be set early from the constructor, + // but we don't know yet if target switch should be disabled. + delete this._targetFrontPromise; + delete this._resolveTargetFrontPromise; + } + + get isZombieTab() { + return this._form.isZombieTab; + } + + get browserId() { + return this._form.browserId; + } + + get selected() { + return this._form.selected; + } + + get title() { + return this._form.title; + } + + get url() { + return this._form.url; + } + + get favicon() { + // Note: the favicon is not part of the default form() payload, it will be + // added in `retrieveFavicon`. + return this._form.favicon; + } + + _createTabTarget(form) { + const front = new WindowGlobalTargetFront(this._client, null, this); + + // As these fronts aren't instantiated by protocol.js, we have to set their actor ID + // manually like that: + front.actorID = form.actor; + front.form(form); + this.manage(front); + return front; + } + + _onTargetDestroyed() { + // Clear the cached targetFront when the target is destroyed. + // Note that we are also checking that _targetFront has a valid actorID + // in getTarget, this acts as an additional security to avoid races. + this._targetFront = null; + } + + /** + * Safely retrieves the favicon via getFavicon() and populates this._form.favicon. + * + * We could let callers explicitly retrieve the favicon instead of inserting it in the + * form dynamically. + */ + async retrieveFavicon() { + try { + this._form.favicon = await this.getFavicon(); + } catch (e) { + // We might request the data for a tab which is going to be destroyed. + // In this case the TargetFront will be destroyed. Otherwise log an error. + if (!this.isDestroyed()) { + console.error("Failed to retrieve the favicon for " + this.url, e); + } + } + } + + /** + * Top-level targets created on the server will not be created and managed + * by a descriptor front. Instead they are created by the Watcher actor. + * On the client side we manually re-establish a link between the descriptor + * and the new top-level target. + */ + setTarget(targetFront) { + // Completely ignore the previous target. + // We might nullify the _targetFront unexpectely due to previous target + // being destroyed after the new is created + if (this._targetFront) { + this._targetFront.off("target-destroyed", this._onTargetDestroyed); + } + this._targetFront = targetFront; + + targetFront.on("target-destroyed", this._onTargetDestroyed); + + if (this.isServerTargetSwitchingEnabled()) { + this._resolveTargetFrontPromise(targetFront); + + // Set a new promise in order to: + // 1) Avoid leaking the targetFront we just resolved into the previous promise. + // 2) Never return an empty target from `getTarget` + // + // About the second point: + // There is a race condition where we call `onTargetDestroyed` (which clears `this.targetFront`) + // a bit before calling `setTarget`. So that `this.targetFront` could be null, + // while we now a new target will eventually come when calling `setTarget`. + // Setting a new promise will help wait for the next target while `_targetFront` is null. + // Note that `getTarget` first look into `_targetFront` before checking for `_targetFrontPromise`. + this._targetFrontPromise = new Promise( + r => (this._resolveTargetFrontPromise = r) + ); + } + } + getCachedTarget() { + return this._targetFront; + } + async getTarget() { + if (this._targetFront && !this._targetFront.isDestroyed()) { + return this._targetFront; + } + + if (this._targetFrontPromise) { + return this._targetFrontPromise; + } + + this._targetFrontPromise = (async () => { + let newTargetFront = null; + try { + const targetForm = await super.getTarget(); + newTargetFront = this._createTabTarget(targetForm); + this.setTarget(newTargetFront); + } catch (e) { + console.log( + `Request to connect to TabDescriptor "${this.id}" failed: ${e}` + ); + } + + this._targetFrontPromise = null; + return newTargetFront; + })(); + return this._targetFrontPromise; + } + + /** + * Handle tabs events. + */ + async _handleTabEvent(event) { + switch (event.type) { + case "TabClose": + // Always destroy the toolbox opened for this local tab descriptor. + // When the toolbox is in a Window Host, it won't be removed from the + // DOM when the tab is closed. + const toolbox = gDevTools.getToolboxForDescriptorFront(this); + if (toolbox) { + // Toolbox.destroy will call target.destroy eventually. + await toolbox.destroy(); + } + break; + case "TabRemotenessChange": + this._onRemotenessChange(); + break; + } + } + + /** + * Automatically respawn the toolbox when the tab changes between being + * loaded within the parent process and loaded from a content process. + * Process change can go in both ways. + */ + async _onRemotenessChange() { + // In a near future, this client side code should be replaced by actor code, + // notifying about new tab targets. + this.emit("remoteness-change", this._targetFront); + } +} + +exports.TabDescriptorFront = TabDescriptorFront; +registerFront(TabDescriptorFront); |