summaryrefslogtreecommitdiffstats
path: root/devtools/server/startup
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/server/startup
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/startup')
-rw-r--r--devtools/server/startup/content-process-script.js281
-rw-r--r--devtools/server/startup/content-process.js33
-rw-r--r--devtools/server/startup/content-process.sys.mjs104
-rw-r--r--devtools/server/startup/frame.js193
-rw-r--r--devtools/server/startup/moz.build13
-rw-r--r--devtools/server/startup/worker.js152
6 files changed, 776 insertions, 0 deletions
diff --git a/devtools/server/startup/content-process-script.js b/devtools/server/startup/content-process-script.js
new file mode 100644
index 0000000000..628c481685
--- /dev/null
+++ b/devtools/server/startup/content-process-script.js
@@ -0,0 +1,281 @@
+/* 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/. */
+
+/* eslint-env mozilla/process-script */
+
+"use strict";
+
+/**
+ * Main entry point for DevTools in content processes.
+ *
+ * This module is loaded early when a content process is started.
+ * Note that (at least) JS XPCOM registered at app-startup, will be running before.
+ * It is used by the multiprocess browser toolbox in order to debug privileged resources.
+ * When debugging a Web page loaded in a Tab, DevToolsFrame JS Window Actor is used instead
+ * (DevToolsFrameParent.jsm and DevToolsFrameChild.jsm).
+ *
+ * This module won't do anything unless DevTools codebase starts adding some data
+ * in `Services.cpmm.sharedData` object or send a message manager message via `Services.cpmm`.
+ * Also, this module is only loaded, on-demand from process-helper if devtools are watching for process targets.
+ */
+
+const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
+
+class ContentProcessStartup {
+ constructor() {
+ // The map is indexed by the Watcher Actor ID.
+ // The values are objects containing the following properties:
+ // - connection: the DevToolsServerConnection itself
+ // - actor: the ContentProcessTargetActor instance
+ this._connections = new Map();
+
+ this.observe = this.observe.bind(this);
+ this.receiveMessage = this.receiveMessage.bind(this);
+
+ this.addListeners();
+ this.maybeCreateExistingTargetActors();
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "xpcom-shutdown": {
+ this.destroy();
+ break;
+ }
+ }
+ }
+
+ destroy(options) {
+ this.removeListeners();
+
+ for (const [, connectionInfo] of this._connections) {
+ connectionInfo.connection.close(options);
+ }
+ this._connections.clear();
+ }
+
+ addListeners() {
+ Services.obs.addObserver(this.observe, "xpcom-shutdown");
+
+ Services.cpmm.addMessageListener(
+ "debug:instantiate-already-available",
+ this.receiveMessage
+ );
+ Services.cpmm.addMessageListener(
+ "debug:destroy-target",
+ this.receiveMessage
+ );
+ Services.cpmm.addMessageListener(
+ "debug:add-session-data-entry",
+ this.receiveMessage
+ );
+ Services.cpmm.addMessageListener(
+ "debug:remove-session-data-entry",
+ this.receiveMessage
+ );
+ Services.cpmm.addMessageListener(
+ "debug:destroy-process-script",
+ this.receiveMessage
+ );
+ }
+
+ removeListeners() {
+ Services.obs.removeObserver(this.observe, "xpcom-shutdown");
+
+ Services.cpmm.removeMessageListener(
+ "debug:instantiate-already-available",
+ this.receiveMessage
+ );
+ Services.cpmm.removeMessageListener(
+ "debug:destroy-target",
+ this.receiveMessage
+ );
+ Services.cpmm.removeMessageListener(
+ "debug:add-session-data-entry",
+ this.receiveMessage
+ );
+ Services.cpmm.removeMessageListener(
+ "debug:remove-session-data-entry",
+ this.receiveMessage
+ );
+ Services.cpmm.removeMessageListener(
+ "debug:destroy-process-script",
+ this.receiveMessage
+ );
+ }
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "debug:instantiate-already-available":
+ this.createTargetActor(
+ msg.data.watcherActorID,
+ msg.data.connectionPrefix,
+ msg.data.sessionData,
+ true
+ );
+ break;
+ case "debug:destroy-target":
+ this.destroyTarget(msg.data.watcherActorID);
+ break;
+ case "debug:add-session-data-entry":
+ this.addSessionDataEntry(
+ msg.data.watcherActorID,
+ msg.data.type,
+ msg.data.entries
+ );
+ break;
+ case "debug:remove-session-data-entry":
+ this.removeSessionDataEntry(
+ msg.data.watcherActorID,
+ msg.data.type,
+ msg.data.entries
+ );
+ break;
+ case "debug:destroy-process-script":
+ this.destroy(msg.data.options);
+ break;
+ default:
+ throw new Error(`Unsupported message name ${msg.name}`);
+ }
+ }
+
+ /**
+ * Called when the content process just started.
+ * This will start creating ContentProcessTarget actors, but only if DevTools code (WatcherActor / WatcherRegistry.jsm)
+ * put some data in `sharedData` telling us to do so.
+ */
+ maybeCreateExistingTargetActors() {
+ const { sharedData } = Services.cpmm;
+
+ // Accessing `sharedData` right off the app-startup returns null.
+ // Spinning the event loop with dispatchToMainThread seems enough,
+ // but it means that we let some more Javascript code run before
+ // instantiating the target actor.
+ // So we may miss a few resources and will register the breakpoints late.
+ if (!sharedData) {
+ Services.tm.dispatchToMainThread(
+ this.maybeCreateExistingTargetActors.bind(this)
+ );
+ return;
+ }
+
+ const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
+ if (!sessionDataByWatcherActor) {
+ return;
+ }
+
+ // Create one Target actor for each prefix/client which listen to process
+ for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
+ const { connectionPrefix, targets } = sessionData;
+ // This is where we only do something significant only if DevTools are opened
+ // and requesting to create target actor for content processes
+ if (targets?.includes("process")) {
+ this.createTargetActor(watcherActorID, connectionPrefix, sessionData);
+ }
+ }
+ }
+
+ /**
+ * Instantiate a new ContentProcessTarget for the given connection.
+ * This is where we start doing some significant computation that only occurs when DevTools are opened.
+ *
+ * @param String watcherActorID
+ * The ID of the WatcherActor who requested to observe and create these target actors.
+ * @param String parentConnectionPrefix
+ * The prefix of the DevToolsServerConnection of the Watcher Actor.
+ * This is used to compute a unique ID for the target actor.
+ * @param Object sessionData
+ * All data managed by the Watcher Actor and WatcherRegistry.jsm, containing
+ * target types, resources types to be listened as well as breakpoints and any
+ * other data meant to be shared across processes and threads.
+ * @param Object options Dictionary with optional values:
+ * @param Boolean options.ignoreAlreadyCreated
+ * If true, do not throw if the target actor has already been created.
+ */
+ createTargetActor(
+ watcherActorID,
+ parentConnectionPrefix,
+ sessionData,
+ ignoreAlreadyCreated = false
+ ) {
+ if (this._connections.get(watcherActorID)) {
+ if (ignoreAlreadyCreated) {
+ return;
+ }
+ throw new Error(
+ "ContentProcessStartup createTargetActor was called more than once" +
+ ` for the Watcher Actor (ID: "${watcherActorID}")`
+ );
+ }
+ // Compute a unique prefix, just for this content process,
+ // which will be used to create a ChildDebuggerTransport pair between content and parent processes.
+ // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`,
+ // but here, we can't have access to any DevTools connection as we are really early in the content process startup
+ const prefix =
+ parentConnectionPrefix + "contentProcess" + Services.appinfo.processID;
+ //TODO: probably merge content-process.jsm with this module
+ const { initContentProcessTarget } = ChromeUtils.importESModule(
+ "resource://devtools/server/startup/content-process.sys.mjs"
+ );
+ const { actor, connection } = initContentProcessTarget({
+ target: Services.cpmm,
+ data: {
+ watcherActorID,
+ parentConnectionPrefix,
+ prefix,
+ sessionContext: sessionData.sessionContext,
+ },
+ });
+ this._connections.set(watcherActorID, {
+ actor,
+ connection,
+ });
+
+ // Pass initialization data to the target actor
+ for (const type in sessionData) {
+ actor.addSessionDataEntry(type, sessionData[type]);
+ }
+ }
+
+ destroyTarget(watcherActorID) {
+ const connectionInfo = this._connections.get(watcherActorID);
+ // This connection has already been cleaned?
+ if (!connectionInfo) {
+ throw new Error(
+ `Trying to destroy a content process target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
+ );
+ }
+ connectionInfo.connection.close();
+ this._connections.delete(watcherActorID);
+ }
+
+ async addSessionDataEntry(watcherActorID, type, entries) {
+ const connectionInfo = this._connections.get(watcherActorID);
+ if (!connectionInfo) {
+ throw new Error(
+ `No content process target actor for this Watcher Actor ID:"${watcherActorID}"`
+ );
+ }
+ const { actor } = connectionInfo;
+ await actor.addSessionDataEntry(type, entries);
+ Services.cpmm.sendAsyncMessage("debug:add-session-data-entry-done", {
+ watcherActorID,
+ });
+ }
+
+ removeSessionDataEntry(watcherActorID, type, entries) {
+ const connectionInfo = this._connections.get(watcherActorID);
+ if (!connectionInfo) {
+ return;
+ }
+ const { actor } = connectionInfo;
+ actor.removeSessionDataEntry(type, entries);
+ }
+}
+
+// Only start this component for content processes.
+// i.e. explicitely avoid running it for the parent process
+if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ new ContentProcessStartup();
+}
diff --git a/devtools/server/startup/content-process.js b/devtools/server/startup/content-process.js
new file mode 100644
index 0000000000..5710220e44
--- /dev/null
+++ b/devtools/server/startup/content-process.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+/* eslint-env mozilla/process-script */
+
+"use strict";
+
+/*
+ * Process script that listens for requests to start a `DevToolsServer` for an entire
+ * content process. Loaded into content processes by the main process during
+ * content-process-connector.js' `connectToContentProcess`.
+ *
+ * The actual server startup itself is in a JSM so that code can be cached.
+ */
+
+function onInit(message) {
+ // Only reply if we are in a real content process
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ const { initContentProcessTarget } = ChromeUtils.importESModule(
+ "resource://devtools/server/startup/content-process.sys.mjs"
+ );
+ initContentProcessTarget(message);
+ }
+}
+
+function onClose() {
+ removeMessageListener("debug:init-content-server", onInit);
+ removeMessageListener("debug:close-content-server", onClose);
+}
+
+addMessageListener("debug:init-content-server", onInit);
+addMessageListener("debug:close-content-server", onClose);
diff --git a/devtools/server/startup/content-process.sys.mjs b/devtools/server/startup/content-process.sys.mjs
new file mode 100644
index 0000000000..fd974e8c4a
--- /dev/null
+++ b/devtools/server/startup/content-process.sys.mjs
@@ -0,0 +1,104 @@
+/* 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/. */
+
+/*
+ * Module that listens for requests to start a `DevToolsServer` for an entire content
+ * process. Loaded into content processes by the main process during
+ * content-process-connector.js' `connectToContentProcess` via the process
+ * script `content-process.js`.
+ *
+ * The actual server startup itself is in this JSM so that code can be cached.
+ */
+
+export function initContentProcessTarget(msg) {
+ const mm = msg.target;
+ const prefix = msg.data.prefix;
+ const watcherActorID = msg.data.watcherActorID;
+
+ // Lazy load Loader.sys.mjs to prevent loading any devtools dependency too early.
+ const {
+ useDistinctSystemPrincipalLoader,
+ releaseDistinctSystemPrincipalLoader,
+ } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
+ );
+
+ // Use a unique object to identify this one usage of the loader
+ const loaderRequester = {};
+
+ // Init a custom, invisible DevToolsServer, in order to not pollute the
+ // debugger with all devtools modules, nor break the debugger itself with
+ // using it in the same process.
+ const loader = useDistinctSystemPrincipalLoader(loaderRequester);
+
+ const { DevToolsServer } = loader.require(
+ "resource://devtools/server/devtools-server.js"
+ );
+
+ DevToolsServer.init();
+ // For browser content toolbox, we do need a regular root actor and all tab
+ // actors, but don't need all the "browser actors" that are only useful when
+ // debugging the parent process via the browser toolbox.
+ DevToolsServer.registerActors({ root: true, target: true });
+
+ // Connect both parent/child processes devtools servers RDP via message
+ // managers
+ const conn = DevToolsServer.connectToParent(prefix, mm);
+
+ const { ContentProcessTargetActor } = loader.require(
+ "resource://devtools/server/actors/targets/content-process.js"
+ );
+
+ const actor = new ContentProcessTargetActor(conn, {
+ sessionContext: msg.data.sessionContext,
+ });
+ actor.manage(actor);
+
+ const response = { watcherActorID, prefix, actor: actor.form() };
+ mm.sendAsyncMessage("debug:content-process-actor", response);
+
+ function onDestroy(options) {
+ mm.removeMessageListener(
+ "debug:content-process-disconnect",
+ onContentProcessDisconnect
+ );
+ actor.off("destroyed", onDestroy);
+
+ // Notify the parent process that the actor is being destroyed
+ mm.sendAsyncMessage("debug:content-process-actor-destroyed", {
+ watcherActorID,
+ });
+
+ // Call DevToolsServerConnection.close to destroy all child actors. It should end up
+ // calling DevToolsServerConnection.onTransportClosed that would actually cleanup all actor
+ // pools.
+ conn.close(options);
+
+ // Destroy the related loader when the target is destroyed
+ // and we were the last user of the special loader
+ releaseDistinctSystemPrincipalLoader(loaderRequester);
+ }
+ function onContentProcessDisconnect(message) {
+ if (message.data.prefix != prefix) {
+ // Several copies of this process script can be running for a single process if
+ // we are debugging the same process from multiple clients.
+ // If this disconnect request doesn't match a connection known here, ignore it.
+ return;
+ }
+ onDestroy();
+ }
+
+ // Clean up things when the client disconnects
+ mm.addMessageListener(
+ "debug:content-process-disconnect",
+ onContentProcessDisconnect
+ );
+ // And also when the target actor is destroyed
+ actor.on("destroyed", onDestroy);
+
+ return {
+ actor,
+ connection: conn,
+ };
+}
diff --git a/devtools/server/startup/frame.js b/devtools/server/startup/frame.js
new file mode 100644
index 0000000000..db14f03c15
--- /dev/null
+++ b/devtools/server/startup/frame.js
@@ -0,0 +1,193 @@
+/* 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/. */
+
+/* eslint-env mozilla/frame-script */
+
+"use strict";
+
+/* global addEventListener */
+
+/*
+ * Frame script that listens for requests to start a `DevToolsServer` for a frame in a
+ * content process. Loaded into content process frames by the main process during
+ * frame-connector.js' connectToFrame.
+ */
+
+try {
+ var chromeGlobal = this;
+
+ // Encapsulate in its own scope to allows loading this frame script more than once.
+ (function () {
+ // In most cases, we are debugging a tab in content process, without chrome
+ // privileges. But in some tests, we are attaching to privileged document.
+ // Because the debugger can't be running in the same compartment than its debuggee,
+ // we have to load the server in a dedicated Loader, flagged with
+ // invisibleToDebugger, which will force it to be loaded in another compartment.
+ let loader,
+ customLoader = false;
+ if (content.document.nodePrincipal.isSystemPrincipal) {
+ const { useDistinctSystemPrincipalLoader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
+ );
+ loader = useDistinctSystemPrincipalLoader(chromeGlobal);
+ customLoader = true;
+ } else {
+ // Otherwise, use the shared loader.
+ loader = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ }
+ const { require } = loader;
+
+ const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+
+ DevToolsServer.init();
+ // We want a special server without any root actor and only target-scoped actors.
+ // We are going to spawn a WindowGlobalTargetActor instance in the next few lines,
+ // it is going to act like a root actor without being one.
+ DevToolsServer.registerActors({ target: true });
+
+ const connections = new Map();
+
+ const onConnect = DevToolsUtils.makeInfallible(function (msg) {
+ const mm = msg.target;
+ const prefix = msg.data.prefix;
+ const addonId = msg.data.addonId;
+ const addonBrowsingContextGroupId = msg.data.addonBrowsingContextGroupId;
+
+ // If we try to create several frame targets simultaneously, the frame script will be loaded several times.
+ // In this case a single "debug:connect" message might be received by all the already loaded frame scripts.
+ // Check if the DevToolsServer already knows the provided connection prefix,
+ // because it means that another framescript instance already handled this message.
+ // Another "debug:connect" message is guaranteed to be emitted for another prefix,
+ // so we keep the message listener and wait for this next message.
+ if (DevToolsServer.hasConnectionForPrefix(prefix)) {
+ return;
+ }
+ removeMessageListener("debug:connect", onConnect);
+
+ const conn = DevToolsServer.connectToParent(prefix, mm);
+ connections.set(prefix, conn);
+
+ let actor;
+
+ if (addonId) {
+ const {
+ WebExtensionTargetActor,
+ } = require("resource://devtools/server/actors/targets/webextension.js");
+ const {
+ createWebExtensionSessionContext,
+ } = require("resource://devtools/server/actors/watcher/session-context.js");
+ const { browsingContext } = docShell;
+ actor = new WebExtensionTargetActor(conn, {
+ addonId,
+ addonBrowsingContextGroupId,
+ chromeGlobal,
+ isTopLevelTarget: true,
+ prefix,
+ sessionContext: createWebExtensionSessionContext(
+ {
+ addonId,
+ browsingContextID: browsingContext.id,
+ innerWindowId: browsingContext.currentWindowContext.innerWindowId,
+ },
+ {
+ isServerTargetSwitchingEnabled:
+ msg.data.isServerTargetSwitchingEnabled,
+ }
+ ),
+ });
+ } else {
+ const {
+ WindowGlobalTargetActor,
+ } = require("resource://devtools/server/actors/targets/window-global.js");
+ const {
+ createBrowserElementSessionContext,
+ } = require("resource://devtools/server/actors/watcher/session-context.js");
+
+ const { docShell } = chromeGlobal;
+ // For a script loaded via loadFrameScript, the global is the content
+ // message manager.
+ // All WindowGlobalTarget actors created via the framescript are top-level
+ // targets. Non top-level WindowGlobalTarget actors are all created by the
+ // DevToolsFrameChild actor.
+ //
+ // createBrowserElementSessionContext only reads browserId attribute
+ const fakeBrowserElement = {
+ browserId: docShell.browsingContext.browserId,
+ };
+ actor = new WindowGlobalTargetActor(conn, {
+ docShell,
+ isTopLevelTarget: true,
+ // This is only used when server target switching is off and we create
+ // the target from TabDescriptor. So all config attributes are false.
+ sessionContext: createBrowserElementSessionContext(
+ fakeBrowserElement,
+ {}
+ ),
+ });
+ }
+ actor.manage(actor);
+
+ sendAsyncMessage("debug:actor", { actor: actor.form(), prefix });
+ });
+
+ addMessageListener("debug:connect", onConnect);
+
+ const onDisconnect = DevToolsUtils.makeInfallible(function (msg) {
+ const prefix = msg.data.prefix;
+ const conn = connections.get(prefix);
+ if (!conn) {
+ // Several copies of this frame script can be running for a single frame since it
+ // is loaded once for each DevTools connection to the frame. If this disconnect
+ // request doesn't match a connection known here, ignore it.
+ return;
+ }
+
+ removeMessageListener("debug:disconnect", onDisconnect);
+ // Call DevToolsServerConnection.close to destroy all child actors. It should end up
+ // calling DevToolsServerConnection.onTransportClosed that would actually cleanup all actor
+ // pools.
+ conn.close();
+ connections.delete(prefix);
+ });
+ addMessageListener("debug:disconnect", onDisconnect);
+
+ // In non-e10s mode, the "debug:disconnect" message isn't always received before the
+ // messageManager connection goes away. Watching for "unload" here ensures we close
+ // any connections when the frame is unloaded.
+ addEventListener("unload", () => {
+ for (const conn of connections.values()) {
+ conn.close();
+ }
+ connections.clear();
+ });
+
+ // Destroy the server once its last connection closes. Note that multiple frame
+ // scripts may be running in parallel and reuse the same server.
+ function destroyLoader() {
+ // Only destroy the server if there is no more connections to it. It may be used
+ // to debug another tab running in the same process.
+ if (DevToolsServer.hasConnection() || DevToolsServer.keepAlive) {
+ return;
+ }
+ DevToolsServer.off("connectionchange", destroyLoader);
+
+ // When debugging chrome pages, we initialized a dedicated loader, also destroy it
+ if (customLoader) {
+ const { releaseDistinctSystemPrincipalLoader } =
+ ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
+ );
+ releaseDistinctSystemPrincipalLoader(chromeGlobal);
+ }
+ }
+ DevToolsServer.on("connectionchange", destroyLoader);
+ })();
+} catch (e) {
+ dump(`Exception in DevTools frame startup: ${e}\n`);
+}
diff --git a/devtools/server/startup/moz.build b/devtools/server/startup/moz.build
new file mode 100644
index 0000000000..4237d88599
--- /dev/null
+++ b/devtools/server/startup/moz.build
@@ -0,0 +1,13 @@
+# -*- 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(
+ "content-process-script.js",
+ "content-process.js",
+ "content-process.sys.mjs",
+ "frame.js",
+ "worker.js",
+)
diff --git a/devtools/server/startup/worker.js b/devtools/server/startup/worker.js
new file mode 100644
index 0000000000..0171cc7ae0
--- /dev/null
+++ b/devtools/server/startup/worker.js
@@ -0,0 +1,152 @@
+/* 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";
+
+/* eslint-env mozilla/chrome-worker */
+/* global worker, loadSubScript, global */
+
+/*
+ * Worker debugger script that listens for requests to start a `DevToolsServer` for a
+ * worker in a process. Loaded into a specific worker during worker-connector.js'
+ * `connectToWorker` which is called from the same process as the worker.
+ */
+
+// This function is used to do remote procedure calls from the worker to the
+// main thread. It is exposed as a built-in global to every module by the
+// worker loader. To make sure the worker loader can access it, it needs to be
+// defined before loading the worker loader script below.
+let nextId = 0;
+this.rpc = function (method, ...params) {
+ return new Promise((resolve, reject) => {
+ const id = nextId++;
+ this.addEventListener("message", function onMessageForRpc(event) {
+ const packet = JSON.parse(event.data);
+ if (packet.type !== "rpc" || packet.id !== id) {
+ return;
+ }
+ if (packet.error) {
+ reject(packet.error);
+ } else {
+ resolve(packet.result);
+ }
+ this.removeEventListener("message", onMessageForRpc);
+ });
+
+ postMessage(
+ JSON.stringify({
+ type: "rpc",
+ method,
+ params,
+ id,
+ })
+ );
+ });
+}.bind(this);
+
+loadSubScript("resource://devtools/shared/loader/worker-loader.js");
+
+const { WorkerTargetActor } = worker.require(
+ "resource://devtools/server/actors/targets/worker.js"
+);
+const { DevToolsServer } = worker.require(
+ "resource://devtools/server/devtools-server.js"
+);
+
+DevToolsServer.createRootActor = function () {
+ throw new Error("Should never get here!");
+};
+
+// This file is only instanciated once for a given WorkerDebugger, which means that
+// multiple toolbox could end up using the same instance of this script. In order to handle
+// that, we handle a Map of the different connections, keyed by forwarding prefix.
+const connections = new Map();
+
+this.addEventListener("message", async function (event) {
+ const packet = JSON.parse(event.data);
+ switch (packet.type) {
+ case "connect":
+ const { forwardingPrefix } = packet;
+
+ // Force initializing the server each time on connect
+ // as it may have been destroyed by a previous, now closed toolbox.
+ // Once the last connection drops, the server auto destroy itself.
+ DevToolsServer.init();
+
+ // Step 3: Create a connection to the parent.
+ const connection = DevToolsServer.connectToParent(forwardingPrefix, this);
+
+ // Step 4: Create a WorkerTarget actor.
+ const workerTargetActor = new WorkerTargetActor(
+ connection,
+ global,
+ packet.workerDebuggerData,
+ packet.options.sessionContext
+ );
+ // Make the worker manage itself so it is put in a Pool and assigned an actorID.
+ workerTargetActor.manage(workerTargetActor);
+
+ workerTargetActor.on(
+ "worker-thread-attached",
+ function onThreadAttached() {
+ postMessage(JSON.stringify({ type: "worker-thread-attached" }));
+ }
+ );
+
+ // Step 5: Send a response packet to the parent to notify
+ // it that a connection has been established.
+ connections.set(forwardingPrefix, {
+ connection,
+ workerTargetActor,
+ });
+
+ postMessage(
+ JSON.stringify({
+ type: "connected",
+ forwardingPrefix,
+ workerTargetForm: workerTargetActor.form(),
+ })
+ );
+
+ // We might receive data to watch.
+ if (packet.options.sessionData) {
+ const promises = [];
+ for (const [type, entries] of Object.entries(
+ packet.options.sessionData
+ )) {
+ promises.push(workerTargetActor.addSessionDataEntry(type, entries));
+ }
+ await Promise.all(promises);
+ }
+
+ break;
+
+ case "add-session-data-entry":
+ await connections
+ .get(packet.forwardingPrefix)
+ .workerTargetActor.addSessionDataEntry(
+ packet.dataEntryType,
+ packet.entries
+ );
+ postMessage(JSON.stringify({ type: "session-data-entry-added" }));
+ break;
+
+ case "remove-session-data-entry":
+ await connections
+ .get(packet.forwardingPrefix)
+ .workerTargetActor.removeSessionDataEntry(
+ packet.dataEntryType,
+ packet.entries
+ );
+ break;
+
+ case "disconnect":
+ // This will destroy the associate WorkerTargetActor (and the actors it manages).
+ if (connections.has(packet.forwardingPrefix)) {
+ connections.get(packet.forwardingPrefix).connection.close();
+ connections.delete(packet.forwardingPrefix);
+ }
+ break;
+ }
+});