summaryrefslogtreecommitdiffstats
path: root/devtools/client/fronts/descriptors
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/fronts/descriptors
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/fronts/descriptors')
-rw-r--r--devtools/client/fronts/descriptors/moz.build12
-rw-r--r--devtools/client/fronts/descriptors/process.js112
-rw-r--r--devtools/client/fronts/descriptors/tab.js263
-rw-r--r--devtools/client/fronts/descriptors/webextension.js139
-rw-r--r--devtools/client/fronts/descriptors/worker.js142
5 files changed, 668 insertions, 0 deletions
diff --git a/devtools/client/fronts/descriptors/moz.build b/devtools/client/fronts/descriptors/moz.build
new file mode 100644
index 0000000000..bf297b3dcb
--- /dev/null
+++ b/devtools/client/fronts/descriptors/moz.build
@@ -0,0 +1,12 @@
+# -*- 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(
+ "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..0d0602920b
--- /dev/null
+++ b/devtools/client/fronts/descriptors/process.js
@@ -0,0 +1,112 @@
+/* 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("devtools/shared/specs/descriptors/process");
+const {
+ BrowsingContextTargetFront,
+} = require("devtools/client/fronts/targets/browsing-context");
+const {
+ ContentProcessTargetFront,
+} = require("devtools/client/fronts/targets/content-process");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+class ProcessDescriptorFront extends FrontClassWithSpec(processDescriptorSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.isParent = false;
+ this._processTargetFront = null;
+ this._targetFrontPromise = null;
+ this._client = client;
+ }
+
+ form(json) {
+ this.id = json.id;
+ this.isParent = json.isParent;
+ 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
+ // BrowsingContextTargetFront on the client side.
+ front = new BrowsingContextTargetFront(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;
+ }
+
+ 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);
+ await targetFront.attach();
+ } 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..a3e649bfd2
--- /dev/null
+++ b/devtools/client/fronts/descriptors/tab.js
@@ -0,0 +1,263 @@
+/* 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 Services = require("Services");
+const { tabDescriptorSpec } = require("devtools/shared/specs/descriptors/tab");
+
+loader.lazyRequireGetter(
+ this,
+ "gDevTools",
+ "devtools/client/framework/devtools",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "BrowsingContextTargetFront",
+ "devtools/client/fronts/targets/browsing-context",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "TargetFactory",
+ "devtools/client/framework/target",
+ true
+);
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+/**
+ * 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 FrontClassWithSpec(tabDescriptorSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._client = client;
+
+ // 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;
+
+ // Flipped when creating dedicated tab targets for DevTools WebExtensions.
+ // See toolkit/components/extensions/ExtensionParent.jsm .
+ this.isDevToolsExtensionContext = false;
+
+ this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
+ this._handleTabEvent = this._handleTabEvent.bind(this);
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+ this._form = json;
+ this.traits = json.traits || {};
+ }
+
+ destroy() {
+ if (this.isLocalTab) {
+ this._teardownLocalTabListeners();
+ }
+ super.destroy();
+ }
+
+ setLocalTab(localTab) {
+ this._localTab = localTab;
+ this._setupLocalTabListeners();
+ }
+
+ 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
+ );
+ }
+
+ get isZombieTab() {
+ return this._form.isZombieTab;
+ }
+
+ get outerWindowID() {
+ return this._form.outerWindowID;
+ }
+
+ 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 BrowsingContextTargetFront(this._client, null, this);
+
+ if (this.isLocalTab) {
+ front.shouldCloseClient = true;
+ }
+
+ // 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);
+ front.on("target-destroyed", this._onTargetDestroyed);
+ 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);
+ }
+ }
+ }
+
+ async getTarget() {
+ 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._createTabTarget(targetForm);
+ await targetFront.attach();
+ } catch (e) {
+ console.log(
+ `Request to connect to TabDescriptor "${this.id}" failed: ${e}`
+ );
+ }
+ this._targetFront = targetFront;
+ this._targetFrontPromise = null;
+ return targetFront;
+ })();
+ return this._targetFrontPromise;
+ }
+
+ /**
+ * Handle tabs events.
+ */
+ async _handleTabEvent(event) {
+ switch (event.type) {
+ case "TabClose":
+ // Always destroy the toolbox opened for this local tab target.
+ // Toolboxes are no longer destroyed on target destruction.
+ // When the toolbox is in a Window Host, it won't be removed from the
+ // DOM when the tab is closed.
+ const toolbox = gDevTools.getToolbox(this._targetFront);
+ // A few tests are using TargetFactory.forTab, but aren't spawning any
+ // toolbox. In this case, the toobox won't destroy the target, so we
+ // do it from here. But ultimately, the target should destroy itself
+ // from the actor side anyway.
+ 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() {
+ // Responsive design does a crazy dance around tabs and triggers
+ // remotenesschange events. But we should ignore them as at the end
+ // the content doesn't change its remoteness.
+ if (this.localTab.isResponsiveDesignMode) {
+ return;
+ }
+
+ // The front that was created for DevTools page extension does not have corresponding toolbox.
+ if (this.isDevToolsExtensionContext) {
+ return;
+ }
+
+ const toolbox = gDevTools.getToolbox(this._targetFront);
+
+ const targetSwitchingEnabled = Services.prefs.getBoolPref(
+ "devtools.target-switching.enabled",
+ false
+ );
+
+ // When target switching is enabled, everything is handled by the TargetList
+ // In a near future, this client side code should be replaced by actor code,
+ // notifying about new tab targets.
+ if (targetSwitchingEnabled) {
+ this.emit("remoteness-change", this._targetFront);
+ return;
+ }
+
+ // Otherwise, if we don't support target switching, ensure the toolbox is destroyed.
+ // We need to wait for the toolbox destruction because the TargetFactory memoized the targets,
+ // and only cleans up the cache after the target is destroyed via toolbox destruction.
+ await toolbox.destroy();
+
+ // Fetch the new target for this tab
+ const newTarget = await TargetFactory.forTab(this.localTab, null);
+
+ gDevTools.showToolbox(newTarget);
+ }
+}
+
+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..4ed8cc53c5
--- /dev/null
+++ b/devtools/client/fronts/descriptors/webextension.js
@@ -0,0 +1,139 @@
+/* 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("devtools/shared/specs/descriptors/webextension");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+loader.lazyRequireGetter(
+ this,
+ "BrowsingContextTargetFront",
+ "devtools/client/fronts/targets/browsing-context",
+ true
+);
+
+class WebExtensionDescriptorFront extends FrontClassWithSpec(
+ webExtensionDescriptorSpec
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.client = client;
+ this.traits = {};
+ }
+
+ 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 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 isWebExtension() {
+ return this._form.isWebExtension;
+ }
+
+ get manifestURL() {
+ return this._form.manifestURL;
+ }
+
+ get name() {
+ return this._form.name;
+ }
+
+ get temporarilyInstalled() {
+ return this._form.temporarilyInstalled;
+ }
+
+ get url() {
+ return this._form.url;
+ }
+
+ get warnings() {
+ return this._form.warnings;
+ }
+
+ _createWebExtensionTarget(form) {
+ const front = new BrowsingContextTargetFront(this.conn, null, this);
+ front.form(form);
+ this.manage(front);
+ return front;
+ }
+
+ /**
+ * Retrieve the BrowsingContextTargetFront 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);
+ await targetFront.attach();
+ } 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..3e0ed5ed30
--- /dev/null
+++ b/devtools/client/fronts/descriptors/worker.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 { Ci } = require("chrome");
+const {
+ workerDescriptorSpec,
+} = require("devtools/shared/specs/descriptors/worker");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { TargetMixin } = require("devtools/client/fronts/targets/target-mixin");
+
+class WorkerDescriptorFront extends TargetMixin(
+ FrontClassWithSpec(workerDescriptorSpec)
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.traits = {};
+
+ // The actor sends a "close" event, which is translated to "worker-close" by
+ // the specification in order to not conflict with Target's "close" event.
+ // This event is similar to tabDetached and means that the worker is destroyed.
+ // So that we should destroy the target in order to significate that the target
+ // is no longer debuggable.
+ this.once("worker-close", this.destroy.bind(this));
+ }
+
+ 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;
+ }
+
+ get name() {
+ return this.url.split("/").pop();
+ }
+
+ 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;
+ }
+
+ async attach() {
+ // 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;
+ }
+
+ const response = await super.attach();
+
+ if (this.isServiceWorker) {
+ this.registration = await this._getRegistrationIfActive();
+ if (this.registration) {
+ await this.registration.preventShutdown();
+ }
+ }
+
+ this._url = response.url;
+
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ const workerTargetForm = await super.getTarget();
+
+ // Set the console actor ID on the form to expose it to Target.attachConsole
+ // Set the ThreadActor on the target form so it is accessible by getFront
+ this.targetForm.consoleActor = workerTargetForm.consoleActor;
+ this.targetForm.threadActor = workerTargetForm.threadActor;
+
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ await this.attachConsole();
+ })();
+ return this._attach;
+ }
+
+ async detach() {
+ let response;
+ try {
+ response = 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");
+ }
+
+ return response;
+ }
+
+ 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);