summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/descriptors
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/descriptors')
-rw-r--r--devtools/server/actors/descriptors/moz.build12
-rw-r--r--devtools/server/actors/descriptors/process.js246
-rw-r--r--devtools/server/actors/descriptors/tab.js253
-rw-r--r--devtools/server/actors/descriptors/webextension.js336
-rw-r--r--devtools/server/actors/descriptors/worker.js182
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;