summaryrefslogtreecommitdiffstats
path: root/devtools/client/fronts/descriptors/tab.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/fronts/descriptors/tab.js')
-rw-r--r--devtools/client/fronts/descriptors/tab.js330
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);