Source code

Revision control

Copy as Markdown

Other Tools

/* 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);