summaryrefslogtreecommitdiffstats
path: root/devtools/server/startup
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/server/startup
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--devtools/server/startup/content-process-script.js280
-rw-r--r--devtools/server/startup/content-process.js35
-rw-r--r--devtools/server/startup/content-process.jsm110
-rw-r--r--devtools/server/startup/frame.js141
-rw-r--r--devtools/server/startup/moz.build13
-rw-r--r--devtools/server/startup/worker.js146
6 files changed, 725 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..361333cfe8
--- /dev/null
+++ b/devtools/server/startup/content-process-script.js
@@ -0,0 +1,280 @@
+/* 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";
+
+/**
+ * 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+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() {
+ this.removeListeners();
+
+ for (const [, connectionInfo] of this._connections) {
+ connectionInfo.connection.close();
+ }
+ 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-watcher-data-entry",
+ this.receiveMessage
+ );
+ Services.cpmm.addMessageListener(
+ "debug:remove-watcher-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-watcher-data-entry",
+ this.receiveMessage
+ );
+ Services.cpmm.removeMessageListener(
+ "debug:remove-watcher-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.watchedData,
+ true
+ );
+ break;
+ case "debug:destroy-target":
+ this.destroyTarget(msg.data.watcherActorID);
+ break;
+ case "debug:add-watcher-data-entry":
+ this.addWatcherDataEntry(
+ msg.data.watcherActorID,
+ msg.data.type,
+ msg.data.entries
+ );
+ break;
+ case "debug:remove-watcher-data-entry":
+ this.addWatcherDataEntry(
+ msg.data.watcherActorID,
+ msg.data.type,
+ msg.data.entries
+ );
+ break;
+ case "debug:destroy-process-script":
+ this.destroy();
+ 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 watchedDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
+ if (!watchedDataByWatcherActor) {
+ return;
+ }
+
+ // Create one Target actor for each prefix/client which listen to process
+ for (const [watcherActorID, watchedData] of watchedDataByWatcherActor) {
+ const { connectionPrefix, targets } = watchedData;
+ // 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, watchedData);
+ }
+ }
+ }
+
+ /**
+ * 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 initialData
+ * 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,
+ initialData,
+ 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.import(
+ "resource://devtools/server/startup/content-process.jsm"
+ );
+ const { actor, connection } = initContentProcessTarget({
+ target: Services.cpmm,
+ data: {
+ watcherActorID,
+ parentConnectionPrefix,
+ prefix,
+ },
+ });
+ this._connections.set(watcherActorID, {
+ actor,
+ connection,
+ });
+
+ // Pass initialization data to the target actor
+ for (const type in initialData) {
+ actor.addWatcherDataEntry(type, initialData[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 addWatcherDataEntry(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.addWatcherDataEntry(type, entries);
+ Services.cpmm.sendAsyncMessage("debug:add-watcher-data-entry-done", {
+ watcherActorID,
+ });
+ }
+
+ removeWatcherDataEntry(watcherActorID, type, entries) {
+ const connectionInfo = this._connections.get(watcherActorID);
+ if (!connectionInfo) {
+ return;
+ }
+ const { actor } = connectionInfo;
+ actor.removeWatcherDataEntry(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..859c7be310
--- /dev/null
+++ b/devtools/server/startup/content-process.js
@@ -0,0 +1,35 @@
+/* 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/. */
+
+/* global addMessageListener, removeMessageListener */
+
+"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.
+ */
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+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.import(
+ "resource://devtools/server/startup/content-process.jsm"
+ );
+ 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.jsm b/devtools/server/startup/content-process.jsm
new file mode 100644
index 0000000000..c54e8d2048
--- /dev/null
+++ b/devtools/server/startup/content-process.jsm
@@ -0,0 +1,110 @@
+/* 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";
+
+/*
+ * 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.
+ */
+
+/* exported initContentProcessTarget */
+const EXPORTED_SYMBOLS = ["initContentProcessTarget"];
+
+let gLoader;
+
+function setupServer(mm) {
+ // Prevent spawning multiple server per process, even if the caller call us
+ // multiple times
+ if (gLoader) {
+ return gLoader;
+ }
+
+ // Lazy load Loader.jsm to prevent loading any devtools dependency too early.
+ const { DevToolsLoader } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+ );
+
+ // 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.
+ gLoader = new DevToolsLoader({
+ invisibleToDebugger: true,
+ });
+ const { DevToolsServer } = gLoader.require("devtools/server/devtools-server");
+
+ 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 });
+
+ // Destroy the server once its last connection closes. Note that multiple frame
+ // scripts may be running in parallel and reuse the same server.
+ function destroyServer() {
+ // Only destroy the server if there is no more connections to it. It may be used
+ // to debug the same process from another client.
+ if (DevToolsServer.hasConnection()) {
+ return;
+ }
+ DevToolsServer.off("connectionchange", destroyServer);
+
+ DevToolsServer.destroy();
+ gLoader.destroy();
+ gLoader = null;
+ }
+ DevToolsServer.on("connectionchange", destroyServer);
+
+ return gLoader;
+}
+
+function initContentProcessTarget(msg) {
+ const mm = msg.target;
+ const prefix = msg.data.prefix;
+ const watcherActorID = msg.data.watcherActorID;
+
+ // Setup a server if none started yet
+ const loader = setupServer(mm);
+
+ // Connect both parent/child processes devtools servers RDP via message
+ // managers
+ const { DevToolsServer } = loader.require("devtools/server/devtools-server");
+ const conn = DevToolsServer.connectToParent(prefix, mm);
+ conn.parentMessageManager = mm;
+
+ const { ContentProcessTargetActor } = loader.require(
+ "devtools/server/actors/targets/content-process"
+ );
+ const actor = new ContentProcessTargetActor(conn);
+ actor.manage(actor);
+
+ const response = { watcherActorID, prefix, actor: actor.form() };
+ mm.sendAsyncMessage("debug:content-process-actor", response);
+
+ // Clean up things when the client disconnects
+ mm.addMessageListener("debug:content-process-disconnect", function onDestroy(
+ 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;
+ }
+ mm.removeMessageListener("debug:content-process-disconnect", onDestroy);
+
+ // Call DevToolsServerConnection.close to destroy all child actors. It should end up
+ // calling DevToolsServerConnection.onClosed that would actually cleanup all actor
+ // pools.
+ conn.close();
+ });
+ return {
+ actor,
+ connection: conn,
+ };
+}
diff --git a/devtools/server/startup/frame.js b/devtools/server/startup/frame.js
new file mode 100644
index 0000000000..ced47824bd
--- /dev/null
+++ b/devtools/server/startup/frame.js
@@ -0,0 +1,141 @@
+/* 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";
+
+/* global content, addEventListener, addMessageListener, removeMessageListener,
+ sendAsyncMessage */
+
+/*
+ * 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 { DevToolsLoader } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+ );
+ loader = new DevToolsLoader({
+ invisibleToDebugger: true,
+ });
+ customLoader = true;
+ } else {
+ // Otherwise, use the shared loader.
+ loader = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+ }
+ const { require } = loader;
+
+ const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+ const { DevToolsServer } = require("devtools/server/devtools-server");
+
+ DevToolsServer.init();
+ // We want a special server without any root actor and only target-scoped actors.
+ // We are going to spawn a FrameTargetActor 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) {
+ removeMessageListener("debug:connect", onConnect);
+
+ const mm = msg.target;
+ const prefix = msg.data.prefix;
+ const addonId = msg.data.addonId;
+
+ const conn = DevToolsServer.connectToParent(prefix, mm);
+ conn.parentMessageManager = mm;
+ connections.set(prefix, conn);
+
+ let actor;
+
+ if (addonId) {
+ const {
+ WebExtensionTargetActor,
+ } = require("devtools/server/actors/targets/webextension");
+ actor = new WebExtensionTargetActor(
+ conn,
+ chromeGlobal,
+ prefix,
+ addonId
+ );
+ } else {
+ const {
+ FrameTargetActor,
+ } = require("devtools/server/actors/targets/frame");
+ const { docShell } = chromeGlobal;
+ // For a script loaded via loadFrameScript, the global is the content
+ // message manager.
+ actor = new FrameTargetActor(conn, docShell);
+ }
+ actor.manage(actor);
+
+ sendAsyncMessage("debug:actor", { actor: actor.form(), prefix: 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.onClosed 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 destroyServer() {
+ // 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", destroyServer);
+ DevToolsServer.destroy();
+
+ // When debugging chrome pages, we initialized a dedicated loader, also destroy it
+ if (customLoader) {
+ loader.destroy();
+ }
+ }
+ DevToolsServer.on("connectionchange", destroyServer);
+ })();
+} 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..9f1389a33d
--- /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.jsm",
+ "frame.js",
+ "worker.js",
+)
diff --git a/devtools/server/startup/worker.js b/devtools/server/startup/worker.js
new file mode 100644
index 0000000000..dea34bafe4
--- /dev/null
+++ b/devtools/server/startup/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";
+
+/* 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,
+ })
+ );
+ });
+};
+
+loadSubScript("resource://devtools/shared/worker/loader.js");
+
+const { WorkerTargetActor } = worker.require(
+ "devtools/server/actors/targets/worker"
+);
+const { DevToolsServer } = worker.require("devtools/server/devtools-server");
+
+DevToolsServer.init();
+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;
+
+ // 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
+ );
+ // 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" }));
+ }
+ );
+ workerTargetActor.attach();
+
+ // 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?.watchedData) {
+ const promises = [];
+ for (const [type, entries] of Object.entries(
+ packet.options.watchedData
+ )) {
+ promises.push(workerTargetActor.addWatcherDataEntry(type, entries));
+ }
+ await Promise.all(promises);
+ }
+
+ break;
+
+ case "add-watcher-data-entry":
+ await connections
+ .get(packet.forwardingPrefix)
+ .workerTargetActor.addWatcherDataEntry(
+ packet.dataEntryType,
+ packet.entries
+ );
+ postMessage(JSON.stringify({ type: "watcher-data-entry-added" }));
+ break;
+
+ case "remove-watcher-data-entry":
+ await connections
+ .get(packet.forwardingPrefix)
+ .workerTargetActor.removeWatcherDataEntry(
+ 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;
+ }
+});