diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/fronts/descriptors | |
parent | Initial commit. (diff) | |
download | firefox-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.build | 12 | ||||
-rw-r--r-- | devtools/client/fronts/descriptors/process.js | 112 | ||||
-rw-r--r-- | devtools/client/fronts/descriptors/tab.js | 263 | ||||
-rw-r--r-- | devtools/client/fronts/descriptors/webextension.js | 139 | ||||
-rw-r--r-- | devtools/client/fronts/descriptors/worker.js | 142 |
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); |