summaryrefslogtreecommitdiffstats
path: root/devtools/client/fronts/descriptors
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/client/fronts/descriptors
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/fronts/descriptors')
-rw-r--r--devtools/client/fronts/descriptors/descriptor-mixin.js60
-rw-r--r--devtools/client/fronts/descriptors/descriptor-types.js17
-rw-r--r--devtools/client/fronts/descriptors/moz.build14
-rw-r--r--devtools/client/fronts/descriptors/process.js142
-rw-r--r--devtools/client/fronts/descriptors/tab.js330
-rw-r--r--devtools/client/fronts/descriptors/webextension.js173
-rw-r--r--devtools/client/fronts/descriptors/worker.js146
7 files changed, 882 insertions, 0 deletions
diff --git a/devtools/client/fronts/descriptors/descriptor-mixin.js b/devtools/client/fronts/descriptors/descriptor-mixin.js
new file mode 100644
index 0000000000..ac38e104b5
--- /dev/null
+++ b/devtools/client/fronts/descriptors/descriptor-mixin.js
@@ -0,0 +1,60 @@
+/* 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";
+
+/**
+ * A Descriptor represents a debuggable context. It can be a browser tab, a tab on
+ * a remote device, like a tab on Firefox for Android. But it can also be an add-on,
+ * as well as firefox parent process, or just one of its content process.
+ * It can be very similar to a Target. The key difference is the lifecycle of these two classes.
+ * The descriptor is meant to be always alive and meaningful/usable until the end of the RDP connection.
+ * Typically a Tab Descriptor will describe the tab and not the one document currently loaded in this tab,
+ * while the Target, will describe this one document and a new Target may be created on each navigation.
+ */
+function DescriptorMixin(parentClass) {
+ class Descriptor extends parentClass {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this._client = client;
+
+ // Pass a true value in order to distinguish this event reception
+ // from any manual destroy caused by the frontend
+ this.on(
+ "descriptor-destroyed",
+ this.destroy.bind(this, { isServerDestroyEvent: true })
+ );
+ }
+
+ get client() {
+ return this._client;
+ }
+
+ async destroy({ isServerDestroyEvent } = {}) {
+ if (this.isDestroyed()) {
+ return;
+ }
+ // This workaround is mostly done for Workers, as WorkerDescriptor
+ // extends the Target class, which causes some issue down the road:
+ // In Target.destroy, we call WorkerDescriptorActor.detach *before* calling super.destroy(),
+ // and so hold on before calling Front.destroy() which would reject all pending requests, including detach().
+ // When calling detach, the server will emit "descriptor-destroyed", which will call Target.destroy again,
+ // but will still be blocked on detach resolution and won't call Front.destroy, and won't reject pending requests either.
+ //
+ // So call Front.baseFrontClassDestroyed manually from here, so that we ensure rejecting the pending detach request
+ // and unblock Target.destroy resolution.
+ //
+ // Here is the inheritance chain for WorkerDescriptor:
+ // WorkerDescriptor -> Descriptor (from descriptor-mixin.js) -> Target (from target-mixin.js) -> Front (protocol.js) -> Pool (protocol.js) -> EventEmitter
+ if (isServerDestroyEvent) {
+ this.baseFrontClassDestroy();
+ }
+
+ await super.destroy();
+ }
+ }
+ return Descriptor;
+}
+exports.DescriptorMixin = DescriptorMixin;
diff --git a/devtools/client/fronts/descriptors/descriptor-types.js b/devtools/client/fronts/descriptors/descriptor-types.js
new file mode 100644
index 0000000000..00164509ab
--- /dev/null
+++ b/devtools/client/fronts/descriptors/descriptor-types.js
@@ -0,0 +1,17 @@
+/* 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";
+
+/**
+ * List of all descriptor types.
+ *
+ * This will be expose via `descriptorType` on all descriptor fronts.
+ */
+module.exports = {
+ PROCESS: "process",
+ WORKER: "worker",
+ TAB: "tab",
+ EXTENSION: "extension",
+};
diff --git a/devtools/client/fronts/descriptors/moz.build b/devtools/client/fronts/descriptors/moz.build
new file mode 100644
index 0000000000..665a0f252a
--- /dev/null
+++ b/devtools/client/fronts/descriptors/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "descriptor-mixin.js",
+ "descriptor-types.js",
+ "process.js",
+ "tab.js",
+ "webextension.js",
+ "worker.js",
+)
diff --git a/devtools/client/fronts/descriptors/process.js b/devtools/client/fronts/descriptors/process.js
new file mode 100644
index 0000000000..6efc1bd4d7
--- /dev/null
+++ b/devtools/client/fronts/descriptors/process.js
@@ -0,0 +1,142 @@
+/* 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 {
+ processDescriptorSpec,
+} = require("resource://devtools/shared/specs/descriptors/process.js");
+const {
+ WindowGlobalTargetFront,
+} = require("resource://devtools/client/fronts/targets/window-global.js");
+const {
+ ContentProcessTargetFront,
+} = require("resource://devtools/client/fronts/targets/content-process.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ DescriptorMixin,
+} = require("resource://devtools/client/fronts/descriptors/descriptor-mixin.js");
+const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js");
+
+class ProcessDescriptorFront extends DescriptorMixin(
+ FrontClassWithSpec(processDescriptorSpec)
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._isParent = false;
+ this._processTargetFront = null;
+ this._targetFrontPromise = null;
+ }
+
+ descriptorType = DESCRIPTOR_TYPES.PROCESS;
+
+ form(json) {
+ this.id = json.id;
+ this._isParent = json.isParent;
+ this._isWindowlessParent = json.isWindowlessParent;
+ this.traits = json.traits || {};
+ }
+
+ async _createProcessTargetFront(form) {
+ let front = null;
+ // the request to getTarget may return a ContentProcessTargetActor or a
+ // ParentProcessTargetActor. In most cases getProcess(0) will return the
+ // main process target actor, which is a ParentProcessTargetActor, but
+ // not in xpcshell, which uses a ContentProcessTargetActor. So select
+ // the right front based on the actor ID.
+ if (form.actor.includes("parentProcessTarget")) {
+ // ParentProcessTargetActor doesn't have a specific front, instead it uses
+ // WindowGlobalTargetFront on the client side.
+ front = new WindowGlobalTargetFront(this._client, null, this);
+ } else {
+ front = new ContentProcessTargetFront(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);
+
+ // @backward-compat { version 84 } Older server don't send the processID in the form
+ if (!front.processID) {
+ front.processID = this.id;
+ }
+
+ this.manage(front);
+ return front;
+ }
+
+ /**
+ * This flag should be true for parent process descriptors of a regular
+ * browser instance, where you can expect the target to be associated with a
+ * window global.
+ *
+ * This will typically be true for the descriptor used by the Browser Toolbox
+ * or the Browser Console opened against a regular Firefox instance.
+ *
+ * On the contrary this will be false for parent process descriptors created
+ * for xpcshell debugging or for background task debugging.
+ */
+ get isBrowserProcessDescriptor() {
+ return this._isParent && !this._isWindowlessParent;
+ }
+
+ get isParentProcessDescriptor() {
+ return this._isParent;
+ }
+
+ get isProcessDescriptor() {
+ return true;
+ }
+
+ getCachedTarget() {
+ return this._processTargetFront;
+ }
+
+ async getTarget() {
+ // Only return the cached Target if it is still alive.
+ if (this._processTargetFront && !this._processTargetFront.isDestroyed()) {
+ return this._processTargetFront;
+ }
+ // Otherwise, ensure that we don't try to spawn more than one Target by
+ // returning the pending promise
+ if (this._targetFrontPromise) {
+ return this._targetFrontPromise;
+ }
+ this._targetFrontPromise = (async () => {
+ let targetFront = null;
+ try {
+ const targetForm = await super.getTarget();
+ targetFront = await this._createProcessTargetFront(targetForm);
+ } catch (e) {
+ // This is likely to happen if we get a lot of events which drop previous
+ // processes.
+ console.log(
+ `Request to connect to ProcessDescriptor "${this.id}" failed: ${e}`
+ );
+ }
+ // Save the reference to the target only after the call to attach
+ // so that getTarget always returns the attached target in case of concurrent calls
+ this._processTargetFront = targetFront;
+ // clear the promise if we are finished so that we can re-connect if
+ // necessary
+ this._targetFrontPromise = null;
+ return targetFront;
+ })();
+ return this._targetFrontPromise;
+ }
+
+ destroy() {
+ if (this._processTargetFront) {
+ this._processTargetFront.destroy();
+ this._processTargetFront = null;
+ }
+ this._targetFrontPromise = null;
+ super.destroy();
+ }
+}
+
+exports.ProcessDescriptorFront = ProcessDescriptorFront;
+registerFront(ProcessDescriptorFront);
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);
diff --git a/devtools/client/fronts/descriptors/webextension.js b/devtools/client/fronts/descriptors/webextension.js
new file mode 100644
index 0000000000..5db86ee5f8
--- /dev/null
+++ b/devtools/client/fronts/descriptors/webextension.js
@@ -0,0 +1,173 @@
+/* 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 {
+ webExtensionDescriptorSpec,
+} = require("resource://devtools/shared/specs/descriptors/webextension.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ DescriptorMixin,
+} = require("resource://devtools/client/fronts/descriptors/descriptor-mixin.js");
+const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js");
+loader.lazyRequireGetter(
+ this,
+ "WindowGlobalTargetFront",
+ "resource://devtools/client/fronts/targets/window-global.js",
+ true
+);
+
+class WebExtensionDescriptorFront extends DescriptorMixin(
+ FrontClassWithSpec(webExtensionDescriptorSpec)
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.traits = {};
+ }
+
+ descriptorType = DESCRIPTOR_TYPES.EXTENSION;
+
+ form(json) {
+ this.actorID = json.actor;
+
+ // Do not use `form` name to avoid colliding with protocol.js's `form` method
+ this._form = json;
+ this.traits = json.traits || {};
+ }
+
+ get backgroundScriptStatus() {
+ return this._form.backgroundScriptStatus;
+ }
+
+ get debuggable() {
+ return this._form.debuggable;
+ }
+
+ get hidden() {
+ return this._form.hidden;
+ }
+
+ get iconDataURL() {
+ return this._form.iconDataURL;
+ }
+
+ get iconURL() {
+ return this._form.iconURL;
+ }
+
+ get id() {
+ return this._form.id;
+ }
+
+ get isSystem() {
+ return this._form.isSystem;
+ }
+
+ get isWebExtensionDescriptor() {
+ return true;
+ }
+
+ get isWebExtension() {
+ return this._form.isWebExtension;
+ }
+
+ get manifestURL() {
+ return this._form.manifestURL;
+ }
+
+ get name() {
+ return this._form.name;
+ }
+
+ get persistentBackgroundScript() {
+ return this._form.persistentBackgroundScript;
+ }
+
+ get temporarilyInstalled() {
+ return this._form.temporarilyInstalled;
+ }
+
+ get url() {
+ return this._form.url;
+ }
+
+ get warnings() {
+ return this._form.warnings;
+ }
+
+ isServerTargetSwitchingEnabled() {
+ // For now, we don't expose any target out of the WatcherActor.
+ // And the top level target is still manually instantiated by the descriptor.
+ // We most likely need to wait for full enabling of EFT before being able to spawn
+ // the extension target from the server side as doing this would most likely break
+ // the iframe dropdown. It would break it as spawning the targets from the server
+ // would probably mean getting rid of the usage of WindowGlobalTargetActor._setWindow
+ // and instead spawn one target per extension document.
+ // That, instead of having a unique target for all the documents.
+ return false;
+ }
+
+ getWatcher() {
+ return super.getWatcher({
+ isServerTargetSwitchingEnabled: this.isServerTargetSwitchingEnabled(),
+ });
+ }
+
+ _createWebExtensionTarget(form) {
+ const front = new WindowGlobalTargetFront(this.conn, null, this);
+ front.form(form);
+ this.manage(front);
+ return front;
+ }
+
+ /**
+ * Retrieve the WindowGlobalTargetFront representing a
+ * WebExtensionTargetActor if this addon is a webextension.
+ *
+ * WebExtensionDescriptors will be created for any type of addon type
+ * (webextension, search plugin, themes). Only webextensions can be targets.
+ * This method will throw for other addon types.
+ *
+ * TODO: We should filter out non-webextension & non-debuggable addons on the
+ * server to avoid the isWebExtension check here. See Bug 1644355.
+ */
+ async getTarget() {
+ if (!this.isWebExtension) {
+ throw new Error(
+ "Tried to create a target for an addon which is not a webextension: " +
+ this.actorID
+ );
+ }
+
+ if (this._targetFront && !this._targetFront.isDestroyed()) {
+ return this._targetFront;
+ }
+
+ if (this._targetFrontPromise) {
+ return this._targetFrontPromise;
+ }
+
+ this._targetFrontPromise = (async () => {
+ let targetFront = null;
+ try {
+ const targetForm = await super.getTarget();
+ targetFront = this._createWebExtensionTarget(targetForm);
+ } catch (e) {
+ console.log(
+ `Request to connect to WebExtensionDescriptor "${this.id}" failed: ${e}`
+ );
+ }
+ this._targetFront = targetFront;
+ this._targetFrontPromise = null;
+ return targetFront;
+ })();
+ return this._targetFrontPromise;
+ }
+}
+
+exports.WebExtensionDescriptorFront = WebExtensionDescriptorFront;
+registerFront(WebExtensionDescriptorFront);
diff --git a/devtools/client/fronts/descriptors/worker.js b/devtools/client/fronts/descriptors/worker.js
new file mode 100644
index 0000000000..47f016f15f
--- /dev/null
+++ b/devtools/client/fronts/descriptors/worker.js
@@ -0,0 +1,146 @@
+/* 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 {
+ workerDescriptorSpec,
+} = require("resource://devtools/shared/specs/descriptors/worker.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ TargetMixin,
+} = require("resource://devtools/client/fronts/targets/target-mixin.js");
+const {
+ DescriptorMixin,
+} = require("resource://devtools/client/fronts/descriptors/descriptor-mixin.js");
+const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js");
+
+class WorkerDescriptorFront extends DescriptorMixin(
+ TargetMixin(FrontClassWithSpec(workerDescriptorSpec))
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.traits = {};
+ }
+
+ descriptorType = DESCRIPTOR_TYPES.WORKER;
+
+ form(json) {
+ this.actorID = json.actor;
+ this.id = json.id;
+
+ // Save the full form for Target class usage.
+ // Do not use `form` name to avoid colliding with protocol.js's `form` method
+ this.targetForm = json;
+ this._url = json.url;
+ this.type = json.type;
+ this.scope = json.scope;
+ this.fetch = json.fetch;
+ this.traits = json.traits;
+ }
+
+ get name() {
+ // this._url is nullified in TargetMixin#destroy.
+ if (!this.url) {
+ return null;
+ }
+
+ return this.url.split("/").pop();
+ }
+
+ get isWorkerDescriptor() {
+ return true;
+ }
+
+ get isDedicatedWorker() {
+ return this.type === Ci.nsIWorkerDebugger.TYPE_DEDICATED;
+ }
+
+ get isSharedWorker() {
+ return this.type === Ci.nsIWorkerDebugger.TYPE_SHARED;
+ }
+
+ get isServiceWorker() {
+ return this.type === Ci.nsIWorkerDebugger.TYPE_SERVICE;
+ }
+
+ // For now, WorkerDescriptor is morphed into a WorkerTarget when calling this method.
+ // Ideally, we would split this into two distinct classes.
+ async morphWorkerDescriptorIntoWorkerTarget() {
+ // temporary, will be moved once we have a target actor
+ return this.getTarget();
+ }
+
+ async getTarget() {
+ if (this._attach) {
+ return this._attach;
+ }
+
+ this._attach = (async () => {
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return this;
+ }
+
+ if (this.isServiceWorker) {
+ this.registration = await this._getRegistrationIfActive();
+ if (this.registration) {
+ await this.registration.preventShutdown();
+ }
+ }
+
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return this;
+ }
+
+ const workerTargetForm = await super.getTarget();
+
+ // Set the console and thread actor IDs on the form so it is accessible by TargetMixin.getFront
+ this.targetForm.consoleActor = workerTargetForm.consoleActor;
+ this.targetForm.threadActor = workerTargetForm.threadActor;
+ this.targetForm.tracerActor = workerTargetForm.tracerActor;
+
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return this;
+ }
+
+ return this;
+ })();
+ return this._attach;
+ }
+
+ async detach() {
+ try {
+ await super.detach();
+
+ if (this.registration) {
+ // Bug 1644772 - Sometimes, the Browser Toolbox fails opening with a connection timeout
+ // with an exception related to this call to allowShutdown and its usage of detachDebugger API.
+ await this.registration.allowShutdown();
+ this.registration = null;
+ }
+ } catch (e) {
+ this.logDetachError(e, "worker");
+ }
+ }
+
+ async _getRegistrationIfActive() {
+ const { registrations } =
+ await this.client.mainRoot.listServiceWorkerRegistrations();
+ return registrations.find(({ activeWorker }) => {
+ return activeWorker && this.id === activeWorker.id;
+ });
+ }
+
+ reconfigure() {
+ // Toolbox and options panel are calling this method but Worker Target can't be
+ // reconfigured. So we ignore this call here.
+ return Promise.resolve();
+ }
+}
+
+exports.WorkerDescriptorFront = WorkerDescriptorFront;
+registerFront(exports.WorkerDescriptorFront);