diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/fronts/descriptors | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
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.js | 60 | ||||
-rw-r--r-- | devtools/client/fronts/descriptors/descriptor-types.js | 17 | ||||
-rw-r--r-- | devtools/client/fronts/descriptors/moz.build | 14 | ||||
-rw-r--r-- | devtools/client/fronts/descriptors/process.js | 142 | ||||
-rw-r--r-- | devtools/client/fronts/descriptors/tab.js | 330 | ||||
-rw-r--r-- | devtools/client/fronts/descriptors/webextension.js | 173 | ||||
-rw-r--r-- | devtools/client/fronts/descriptors/worker.js | 146 |
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); |