diff options
Diffstat (limited to 'devtools/server/actors/descriptors')
-rw-r--r-- | devtools/server/actors/descriptors/moz.build | 12 | ||||
-rw-r--r-- | devtools/server/actors/descriptors/process.js | 246 | ||||
-rw-r--r-- | devtools/server/actors/descriptors/tab.js | 253 | ||||
-rw-r--r-- | devtools/server/actors/descriptors/webextension.js | 336 | ||||
-rw-r--r-- | devtools/server/actors/descriptors/worker.js | 182 |
5 files changed, 1029 insertions, 0 deletions
diff --git a/devtools/server/actors/descriptors/moz.build b/devtools/server/actors/descriptors/moz.build new file mode 100644 index 0000000000..bf297b3dcb --- /dev/null +++ b/devtools/server/actors/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/server/actors/descriptors/process.js b/devtools/server/actors/descriptors/process.js new file mode 100644 index 0000000000..19944c7d03 --- /dev/null +++ b/devtools/server/actors/descriptors/process.js @@ -0,0 +1,246 @@ +/* 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"; + +/* + * Represents any process running in Firefox. + * This can be: + * - the parent process, where all top level chrome window runs: + * like browser.xhtml, sidebars, devtools iframes, the browser console, ... + * - any content process + * + * There is some special cases in the class around: + * - xpcshell, where there is only one process which doesn't expose any DOM document + * And instead of exposing a ParentProcessTargetActor, getTarget will return + * a ContentProcessTargetActor. + * - background task, similarly to xpcshell, they don't expose any DOM document + * and this also works with a ContentProcessTargetActor. + * + * See devtools/docs/backend/actor-hierarchy.md for more details. + */ + +const { Actor } = require("resource://devtools/shared/protocol.js"); +const { + processDescriptorSpec, +} = require("resource://devtools/shared/specs/descriptors/process.js"); + +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); + +const { + createBrowserSessionContext, + createContentProcessSessionContext, +} = require("resource://devtools/server/actors/watcher/session-context.js"); + +loader.lazyRequireGetter( + this, + "ContentProcessTargetActor", + "resource://devtools/server/actors/targets/content-process.js", + true +); +loader.lazyRequireGetter( + this, + "ParentProcessTargetActor", + "resource://devtools/server/actors/targets/parent-process.js", + true +); +loader.lazyRequireGetter( + this, + "connectToContentProcess", + "resource://devtools/server/connectors/content-process-connector.js", + true +); +loader.lazyRequireGetter( + this, + "WatcherActor", + "resource://devtools/server/actors/watcher.js", + true +); + +class ProcessDescriptorActor extends Actor { + constructor(connection, options = {}) { + super(connection, processDescriptorSpec); + + if ("id" in options && typeof options.id != "number") { + throw Error("process connect requires a valid `id` attribute."); + } + + this.id = options.id; + this._windowGlobalTargetActor = null; + this.isParent = options.parent; + this.destroy = this.destroy.bind(this); + } + + get browsingContextID() { + if (this._windowGlobalTargetActor) { + return this._windowGlobalTargetActor.docShell.browsingContext.id; + } + return null; + } + + get isWindowlessParent() { + return this.isParent && (this.isXpcshell || this.isBackgroundTaskMode); + } + + get isXpcshell() { + return Services.env.exists("XPCSHELL_TEST_PROFILE_DIR"); + } + + get isBackgroundTaskMode() { + const bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( + Ci.nsIBackgroundTasks + ); + return bts && bts.isBackgroundTaskMode; + } + + _parentProcessConnect() { + let targetActor; + if (this.isWindowlessParent) { + // Check if we are running on xpcshell or in background task mode. + // In these modes, there is no valid browsing context to attach to + // and so ParentProcessTargetActor doesn't make sense as it inherits from + // WindowGlobalTargetActor. So instead use ContentProcessTargetActor, which + // matches the needs of these modes. + targetActor = new ContentProcessTargetActor(this.conn, { + isXpcShellTarget: true, + sessionContext: createContentProcessSessionContext(), + }); + } else { + // Create the target actor for the parent process, which is in the same process + // as this target. Because we are in the same process, we have a true actor that + // should be managed by the ProcessDescriptorActor. + targetActor = new ParentProcessTargetActor(this.conn, { + // This target actor is special and will stay alive as long + // as the toolbox/client is alive. It is the original top level target for + // the BrowserToolbox and isTopLevelTarget should always be true here. + // (It isn't the typical behavior of WindowGlobalTargetActor's base class) + isTopLevelTarget: true, + sessionContext: createBrowserSessionContext(), + }); + // this is a special field that only parent process with a browsing context + // have, as they are the only processes at the moment that have child + // browsing contexts + this._windowGlobalTargetActor = targetActor; + } + this.manage(targetActor); + // to be consistent with the return value of the _childProcessConnect, we are returning + // the form here. This might be memoized in the future + return targetActor.form(); + } + + /** + * Connect to a remote process actor, always a ContentProcess target. + */ + async _childProcessConnect() { + const { id } = this; + const mm = this._lookupMessageManager(id); + if (!mm) { + return { + error: "noProcess", + message: "There is no process with id '" + id + "'.", + }; + } + const childTargetForm = await connectToContentProcess( + this.conn, + mm, + this.destroy + ); + return childTargetForm; + } + + _lookupMessageManager(id) { + for (let i = 0; i < Services.ppmm.childCount; i++) { + const mm = Services.ppmm.getChildAt(i); + + // A zero id is used for the parent process, instead of its actual pid. + if (id ? mm.osPid == id : mm.isInProcess) { + return mm; + } + } + return null; + } + + /** + * Connect the a process actor. + */ + async getTarget() { + if (!DevToolsServer.allowChromeProcess) { + return { + error: "forbidden", + message: "You are not allowed to debug processes.", + }; + } + if (this.isParent) { + return this._parentProcessConnect(); + } + // This is a remote process we are connecting to + return this._childProcessConnect(); + } + + /** + * Return a Watcher actor, allowing to keep track of targets which + * already exists or will be created. It also helps knowing when they + * are destroyed. + */ + getWatcher() { + if (!this.watcher) { + this.watcher = new WatcherActor(this.conn, createBrowserSessionContext()); + this.manage(this.watcher); + } + return this.watcher; + } + + form() { + return { + actor: this.actorID, + id: this.id, + isParent: this.isParent, + isWindowlessParent: this.isWindowlessParent, + traits: { + // Supports the Watcher actor. Can be removed as part of Bug 1680280. + // Bug 1687461: WatcherActor only supports the parent process, where we debug everything. + // For the "Browser Content Toolbox", where we debug only one content process, + // we will still be using legacy listeners. + watcher: this.isParent, + // ParentProcessTargetActor can be reloaded. + supportsReloadDescriptor: this.isParent && !this.isWindowlessParent, + }, + }; + } + + async reloadDescriptor() { + if (!this.isParent || this.isWindowlessParent) { + throw new Error( + "reloadDescriptor is only available for parent process descriptors" + ); + } + + // Reload for the parent process will restart the whole browser + // + // This aims at replicate `DevelopmentHelpers.quickRestart` + // This allows a user to do a full firefox restart + session restore + // Via Ctrl+Alt+R on the Browser Console/Toolbox + + // Maximize the chance of fetching new source content by clearing the cache + Services.obs.notifyObservers(null, "startupcache-invalidate"); + + // Avoid safemode popup from appearing on restart + Services.env.set("MOZ_DISABLE_SAFE_MODE_KEY", "1"); + + Services.startup.quit( + Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart + ); + } + + destroy() { + this.emit("descriptor-destroyed"); + + this._windowGlobalTargetActor = null; + super.destroy(); + } +} + +exports.ProcessDescriptorActor = ProcessDescriptorActor; diff --git a/devtools/server/actors/descriptors/tab.js b/devtools/server/actors/descriptors/tab.js new file mode 100644 index 0000000000..ea20d3fb36 --- /dev/null +++ b/devtools/server/actors/descriptors/tab.js @@ -0,0 +1,253 @@ +/* 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"; + +/* + * Descriptor Actor that represents a Tab in the parent process. It + * launches a WindowGlobalTargetActor in the content process to do the real work and tunnels the + * data. + * + * See devtools/docs/backend/actor-hierarchy.md for more details. + */ + +const { Actor } = require("resource://devtools/shared/protocol.js"); +const { + tabDescriptorSpec, +} = require("resource://devtools/shared/specs/descriptors/tab.js"); + +const { + connectToFrame, +} = require("resource://devtools/server/connectors/frame-connector.js"); +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { + createBrowserElementSessionContext, +} = require("resource://devtools/server/actors/watcher/session-context.js"); + +loader.lazyRequireGetter( + this, + "WatcherActor", + "resource://devtools/server/actors/watcher.js", + true +); + +/** + * Creates a target actor proxy for handling requests to a single browser frame. + * Both <xul:browser> and <iframe mozbrowser> are supported. + * This actor is a shim that connects to a WindowGlobalTargetActor in a remote browser process. + * All RDP packets get forwarded using the message manager. + * + * @param connection The main RDP connection. + * @param browser <xul:browser> or <iframe mozbrowser> element to connect to. + */ +class TabDescriptorActor extends Actor { + constructor(connection, browser) { + super(connection, tabDescriptorSpec); + this._browser = browser; + } + + form() { + const form = { + actor: this.actorID, + browserId: this._browser.browserId, + browsingContextID: + this._browser && this._browser.browsingContext + ? this._browser.browsingContext.id + : null, + isZombieTab: this._isZombieTab(), + outerWindowID: this._getOuterWindowId(), + selected: this.selected, + title: this._getTitle(), + traits: { + // Supports the Watcher actor. Can be removed as part of Bug 1680280. + watcher: true, + supportsReloadDescriptor: true, + }, + url: this._getUrl(), + }; + + return form; + } + + _getTitle() { + // If the content already provides a title, use it. + if (this._browser.contentTitle) { + return this._browser.contentTitle; + } + + // For zombie or lazy tabs (tab created, but content has not been loaded), + // try to retrieve the title from the XUL Tab itself. + // Note: this only works on Firefox desktop. + if (this._tabbrowser) { + const tab = this._tabbrowser.getTabForBrowser(this._browser); + if (tab) { + return tab.label; + } + } + + // No title available. + return null; + } + + _getUrl() { + if (!this._browser || !this._browser.browsingContext) { + return ""; + } + + const { browsingContext } = this._browser; + return browsingContext.currentWindowGlobal.documentURI.spec; + } + + _getOuterWindowId() { + if (!this._browser || !this._browser.browsingContext) { + return ""; + } + + const { browsingContext } = this._browser; + return browsingContext.currentWindowGlobal.outerWindowId; + } + + get selected() { + // getMostRecentBrowserWindow will find the appropriate window on Firefox + // Desktop and on GeckoView. + const topAppWindow = Services.wm.getMostRecentBrowserWindow(); + + const selectedBrowser = topAppWindow?.gBrowser?.selectedBrowser; + if (!selectedBrowser) { + // Note: gBrowser is not available on GeckoView. + // We should find another way to know if this browser is the selected + // browser. See Bug 1631020. + return false; + } + + return this._browser === selectedBrowser; + } + + async getTarget() { + if (!this.conn) { + return { + error: "tabDestroyed", + message: "Tab destroyed while performing a TabDescriptorActor update", + }; + } + + /* eslint-disable-next-line no-async-promise-executor */ + return new Promise(async (resolve, reject) => { + const onDestroy = () => { + // Reject the update promise if the tab was destroyed while requesting an update + reject({ + error: "tabDestroyed", + message: "Tab destroyed while performing a TabDescriptorActor update", + }); + + // Targets created from the TabDescriptor are not created via JSWindowActors and + // we need to notify the watcher manually about their destruction. + // TabDescriptor's targets are created via TabDescriptor.getTarget and are still using + // message manager instead of JSWindowActors. + if (this.watcher && this.targetActorForm) { + this.watcher.notifyTargetDestroyed(this.targetActorForm); + } + }; + + try { + // Check if the browser is still connected before calling connectToFrame + if (!this._browser.isConnected) { + onDestroy(); + return; + } + + const connectForm = await connectToFrame( + this.conn, + this._browser, + onDestroy + ); + this.targetActorForm = connectForm; + resolve(connectForm); + } catch (e) { + reject({ + error: "tabDestroyed", + message: "Tab destroyed while connecting to the frame", + }); + } + }); + } + + /** + * Return a Watcher actor, allowing to keep track of targets which + * already exists or will be created. It also helps knowing when they + * are destroyed. + */ + getWatcher(config) { + if (!this.watcher) { + this.watcher = new WatcherActor( + this.conn, + createBrowserElementSessionContext(this._browser, { + isServerTargetSwitchingEnabled: config.isServerTargetSwitchingEnabled, + isPopupDebuggingEnabled: config.isPopupDebuggingEnabled, + }) + ); + this.manage(this.watcher); + } + return this.watcher; + } + + get _tabbrowser() { + if (this._browser && typeof this._browser.getTabBrowser == "function") { + return this._browser.getTabBrowser(); + } + return null; + } + + async getFavicon() { + if (!AppConstants.MOZ_PLACES) { + // PlacesUtils is not supported + return null; + } + + try { + const { data } = await lazy.PlacesUtils.promiseFaviconData( + this._getUrl() + ); + return data; + } catch (e) { + // Favicon unavailable for this url. + return null; + } + } + + _isZombieTab() { + // Note: GeckoView doesn't support zombie tabs + const tabbrowser = this._tabbrowser; + const tab = tabbrowser ? tabbrowser.getTabForBrowser(this._browser) : null; + return tab?.hasAttribute && tab.hasAttribute("pending"); + } + + reloadDescriptor({ bypassCache }) { + if (!this._browser || !this._browser.browsingContext) { + return; + } + + this._browser.browsingContext.reload( + bypassCache + ? Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE + : Ci.nsIWebNavigation.LOAD_FLAGS_NONE + ); + } + + destroy() { + this.emit("descriptor-destroyed"); + this._browser = null; + + super.destroy(); + } +} + +exports.TabDescriptorActor = TabDescriptorActor; diff --git a/devtools/server/actors/descriptors/webextension.js b/devtools/server/actors/descriptors/webextension.js new file mode 100644 index 0000000000..56e4abfc41 --- /dev/null +++ b/devtools/server/actors/descriptors/webextension.js @@ -0,0 +1,336 @@ +/* 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"; + +/* + * Represents a WebExtension add-on in the parent process. This gives some metadata about + * the add-on and watches for uninstall events. This uses a proxy to access the + * WebExtension in the WebExtension process via the message manager. + * + * See devtools/docs/backend/actor-hierarchy.md for more details. + */ + +const { Actor } = require("resource://devtools/shared/protocol.js"); +const { + webExtensionDescriptorSpec, +} = require("resource://devtools/shared/specs/descriptors/webextension.js"); + +const { + connectToFrame, +} = require("resource://devtools/server/connectors/frame-connector.js"); +const { + createWebExtensionSessionContext, +} = require("resource://devtools/server/actors/watcher/session-context.js"); + +const lazy = {}; +loader.lazyGetter(lazy, "AddonManager", () => { + return ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs", + { loadInDevToolsLoader: false } + ).AddonManager; +}); +loader.lazyGetter(lazy, "ExtensionParent", () => { + return ChromeUtils.importESModule( + "resource://gre/modules/ExtensionParent.sys.mjs", + { loadInDevToolsLoader: false } + ).ExtensionParent; +}); +loader.lazyRequireGetter( + this, + "WatcherActor", + "resource://devtools/server/actors/watcher.js", + true +); + +const BGSCRIPT_STATUSES = { + RUNNING: "RUNNING", + STOPPED: "STOPPED", +}; + +/** + * Creates the actor that represents the addon in the parent process, which connects + * itself to a WebExtensionTargetActor counterpart which is created in the extension + * process (or in the main process if the WebExtensions OOP mode is disabled). + * + * The WebExtensionDescriptorActor subscribes itself as an AddonListener on the AddonManager + * and forwards this events to child actor (e.g. on addon reload or when the addon is + * uninstalled completely) and connects to the child extension process using a `browser` + * element provided by the extension internals (it is not related to any single extension, + * but it will be created automatically to the currently selected "WebExtensions OOP mode" + * and it persist across the extension reloads (it is destroyed once the actor exits). + * WebExtensionDescriptorActor is a child of RootActor, it can be retrieved via + * RootActor.listAddons request. + * + * @param {DevToolsServerConnection} conn + * The connection to the client. + * @param {AddonWrapper} addon + * The target addon. + */ +class WebExtensionDescriptorActor extends Actor { + constructor(conn, addon) { + super(conn, webExtensionDescriptorSpec); + this.addon = addon; + this.addonId = addon.id; + this._childFormPromise = null; + + this._onChildExit = this._onChildExit.bind(this); + this.destroy = this.destroy.bind(this); + lazy.AddonManager.addAddonListener(this); + } + + form() { + const { addonId } = this; + const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID(addonId); + const persistentBackgroundScript = + lazy.ExtensionParent.DebugUtils.hasPersistentBackgroundScript(addonId); + const backgroundScriptStatus = this._getBackgroundScriptStatus(); + + return { + actor: this.actorID, + backgroundScriptStatus, + // Note that until the policy becomes active, + // getTarget/connectToFrame will fail attaching to the web extension: + // https://searchfox.org/mozilla-central/rev/526a5089c61db85d4d43eb0e46edaf1f632e853a/toolkit/components/extensions/WebExtensionPolicy.cpp#551-553 + debuggable: policy?.active && this.addon.isDebuggable, + hidden: this.addon.hidden, + // iconDataURL is available after calling loadIconDataURL + iconDataURL: this._iconDataURL, + iconURL: this.addon.iconURL, + id: addonId, + isSystem: this.addon.isSystem, + isWebExtension: this.addon.isWebExtension, + manifestURL: policy && policy.getURL("manifest.json"), + name: this.addon.name, + persistentBackgroundScript, + temporarilyInstalled: this.addon.temporarilyInstalled, + traits: { + supportsReloadDescriptor: true, + // Supports the Watcher actor. Can be removed as part of Bug 1680280. + watcher: true, + }, + url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined, + warnings: lazy.ExtensionParent.DebugUtils.getExtensionManifestWarnings( + this.addonId + ), + }; + } + + /** + * Return a Watcher actor, allowing to keep track of targets which + * already exists or will be created. It also helps knowing when they + * are destroyed. + */ + async getWatcher(config = {}) { + if (!this.watcher) { + // Ensure connecting to the webextension frame in order to populate this._form + await this._extensionFrameConnect(); + this.watcher = new WatcherActor( + this.conn, + createWebExtensionSessionContext( + { + addonId: this.addonId, + browsingContextID: this._form.browsingContextID, + innerWindowId: this._form.innerWindowId, + }, + config + ) + ); + this.manage(this.watcher); + } + return this.watcher; + } + + async getTarget() { + const form = await this._extensionFrameConnect(); + // Merge into the child actor form, some addon metadata + // (e.g. the addon name shown in the addon debugger window title). + return Object.assign(form, { + iconURL: this.addon.iconURL, + id: this.addon.id, + name: this.addon.name, + }); + } + + getChildren() { + return []; + } + + async _extensionFrameConnect() { + if (this._form) { + return this._form; + } + + this._browser = + await lazy.ExtensionParent.DebugUtils.getExtensionProcessBrowser(this); + + const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID( + this.addonId + ); + this._form = await connectToFrame(this.conn, this._browser, this.destroy, { + addonId: this.addonId, + addonBrowsingContextGroupId: policy.browsingContextGroupId, + // Bug 1754452: This flag is passed by the client to getWatcher(), but the server + // doesn't support this anyway. So always pass false here and keep things simple. + // Once we enable this flag, we will stop using connectToFrame and instantiate + // the WebExtensionTargetActor from watcher code instead, so that shouldn't + // introduce an issue for the future. + isServerTargetSwitchingEnabled: false, + }); + + // connectToFrame may resolve to a null form, + // in case the browser element is destroyed before it is fully connected to it. + if (!this._form) { + throw new Error( + "browser element destroyed while connecting to it: " + this.addon.name + ); + } + + this._childActorID = this._form.actor; + + // Exit the proxy child actor if the child actor has been destroyed. + this._mm.addMessageListener("debug:webext_child_exit", this._onChildExit); + + return this._form; + } + + /** + * Note that reloadDescriptor is the common API name for descriptors + * which support to be reloaded, while WebExtensionDescriptorActor::reload + * is a legacy API which is for instance used from web-ext. + * + * bypassCache has no impact for addon reloads. + */ + reloadDescriptor({ bypassCache }) { + return this.reload(); + } + + async reload() { + await this.addon.reload(); + return {}; + } + + async terminateBackgroundScript() { + await lazy.ExtensionParent.DebugUtils.terminateBackgroundScript( + this.addonId + ); + } + + // This function will be called from RootActor in case that the devtools client + // retrieves list of addons with `iconDataURL` option. + async loadIconDataURL() { + this._iconDataURL = await this.getIconDataURL(); + } + + async getIconDataURL() { + if (!this.addon.iconURL) { + return null; + } + + const xhr = new XMLHttpRequest(); + xhr.responseType = "blob"; + xhr.open("GET", this.addon.iconURL, true); + + if (this.addon.iconURL.toLowerCase().endsWith(".svg")) { + // Maybe SVG, thus force to change mime type. + xhr.overrideMimeType("image/svg+xml"); + } + + try { + const blob = await new Promise((resolve, reject) => { + xhr.onload = () => resolve(xhr.response); + xhr.onerror = reject; + xhr.send(); + }); + + const reader = new FileReader(); + return await new Promise((resolve, reject) => { + reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } catch (_) { + console.warn(`Failed to create data url from [${this.addon.iconURL}]`); + return null; + } + } + + // Private Methods + _getBackgroundScriptStatus() { + const isRunning = lazy.ExtensionParent.DebugUtils.isBackgroundScriptRunning( + this.addonId + ); + // The background script status doesn't apply to this addon (e.g. the addon + // type doesn't have any code, like staticthemes/langpacks/dictionaries, or + // the extension does not have a background script at all). + if (isRunning === undefined) { + return undefined; + } + + return isRunning ? BGSCRIPT_STATUSES.RUNNING : BGSCRIPT_STATUSES.STOPPED; + } + + get _mm() { + return ( + this._browser && + (this._browser.messageManager || this._browser.frameLoader.messageManager) + ); + } + + /** + * Handle the child actor exit. + */ + _onChildExit(msg) { + if (msg.json.actor !== this._childActorID) { + return; + } + + this.destroy(); + } + + // AddonManagerListener callbacks. + onInstalled(addon) { + if (addon.id != this.addonId) { + return; + } + + // Update the AddonManager's addon object on reload/update. + this.addon = addon; + } + + onUninstalled(addon) { + if (addon != this.addon) { + return; + } + + this.destroy(); + } + + destroy() { + lazy.AddonManager.removeAddonListener(this); + + this.addon = null; + if (this._mm) { + this._mm.removeMessageListener( + "debug:webext_child_exit", + this._onChildExit + ); + + this._mm.sendAsyncMessage("debug:webext_parent_exit", { + actor: this._childActorID, + }); + + lazy.ExtensionParent.DebugUtils.releaseExtensionProcessBrowser(this); + } + + this._browser = null; + this._childActorID = null; + + this.emit("descriptor-destroyed"); + + super.destroy(); + } +} + +exports.WebExtensionDescriptorActor = WebExtensionDescriptorActor; diff --git a/devtools/server/actors/descriptors/worker.js b/devtools/server/actors/descriptors/worker.js new file mode 100644 index 0000000000..89ca918e05 --- /dev/null +++ b/devtools/server/actors/descriptors/worker.js @@ -0,0 +1,182 @@ +/* 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"; + +/* + * Target actor for any of the various kinds of workers. + * + * See devtools/docs/backend/actor-hierarchy.md for more details. + */ + +// protocol.js uses objects as exceptions in order to define +// error packets. +/* eslint-disable no-throw-literal */ + +const { Actor } = require("resource://devtools/shared/protocol.js"); +const { + workerDescriptorSpec, +} = require("resource://devtools/shared/specs/descriptors/worker.js"); + +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { + createWorkerSessionContext, +} = require("resource://devtools/server/actors/watcher/session-context.js"); + +loader.lazyRequireGetter( + this, + "connectToWorker", + "resource://devtools/server/connectors/worker-connector.js", + true +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "swm", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +class WorkerDescriptorActor extends Actor { + constructor(conn, dbg) { + super(conn, workerDescriptorSpec); + this._dbg = dbg; + + this._threadActor = null; + this._transport = null; + + this._dbgListener = { + onClose: this._onWorkerClose.bind(this), + onError: this._onWorkerError.bind(this), + }; + + this._dbg.addListener(this._dbgListener); + this._attached = true; + } + + form() { + const form = { + actor: this.actorID, + + consoleActor: this._consoleActor, + threadActor: this._threadActor, + tracerActor: this._tracerActor, + + id: this._dbg.id, + url: this._dbg.url, + traits: {}, + type: this._dbg.type, + }; + if (this._dbg.type === Ci.nsIWorkerDebugger.TYPE_SERVICE) { + /** + * The ServiceWorkerManager in content processes don't maintain + * ServiceWorkerRegistrations; record the ServiceWorker's ID, and + * this data will be merged with the corresponding registration in + * the parent process. + */ + if (!DevToolsServer.isInChildProcess) { + const registration = this._getServiceWorkerRegistrationInfo(); + form.scope = registration.scope; + const newestWorker = + registration.activeWorker || + registration.waitingWorker || + registration.installingWorker; + form.fetch = newestWorker?.handlesFetchEvents; + } + } + return form; + } + + detach() { + if (!this._attached) { + throw { error: "wrongState" }; + } + + this.destroy(); + } + + destroy() { + if (this._attached) { + this._detach(); + } + + this.emit("descriptor-destroyed"); + super.destroy(); + } + + async getTarget() { + if (!this._attached) { + return { error: "wrongState" }; + } + + if (this._threadActor !== null) { + return { + type: "connected", + + consoleActor: this._consoleActor, + threadActor: this._threadActor, + tracerActor: this._tracerActor, + }; + } + + try { + const { transport, workerTargetForm } = await connectToWorker( + this.conn, + this._dbg, + this.actorID, + { + sessionContext: createWorkerSessionContext(), + } + ); + + this._consoleActor = workerTargetForm.consoleActor; + this._threadActor = workerTargetForm.threadActor; + this._tracerActor = workerTargetForm.tracerActor; + + this._transport = transport; + + return { + type: "connected", + + consoleActor: this._consoleActor, + threadActor: this._threadActor, + tracerActor: this._tracerActor, + + url: this._dbg.url, + }; + } catch (error) { + return { error: error.toString() }; + } + } + + _onWorkerClose() { + this.destroy(); + } + + _onWorkerError(filename, lineno, message) { + console.error("ERROR:", filename, ":", lineno, ":", message); + } + + _getServiceWorkerRegistrationInfo() { + return swm.getRegistrationByPrincipal(this._dbg.principal, this._dbg.url); + } + + _detach() { + if (this._threadActor !== null) { + this._transport.close(); + this._transport = null; + this._threadActor = null; + } + + this._dbg.removeListener(this._dbgListener); + this._attached = false; + } +} + +exports.WorkerDescriptorActor = WorkerDescriptorActor; |