330 lines
10 KiB
JavaScript
330 lines
10 KiB
JavaScript
/* 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);
|