1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
7 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
11 XPCOMUtils.defineLazyServiceGetter(
14 "@mozilla.org/dom/workers/workerdebuggermanager;1",
15 "nsIWorkerDebuggerManager"
18 ChromeUtils.defineLazyGetter(lazy, "Loader", () =>
19 ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs")
22 ChromeUtils.defineLazyGetter(lazy, "DevToolsUtils", () =>
23 lazy.Loader.require("resource://devtools/shared/DevToolsUtils.js")
25 XPCOMUtils.defineLazyModuleGetters(lazy, {
27 "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm",
29 ChromeUtils.defineESModuleGetters(lazy, {
30 isWindowGlobalPartOfContext:
31 "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
34 // Name of the attribute into which we save data in `sharedData` object.
35 const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
37 export class DevToolsWorkerChild extends JSWindowActorChild {
41 // The map is indexed by the Watcher Actor ID.
42 // The values are objects containing the following properties:
43 // - connection: the DevToolsServerConnection itself
44 // - workers: An array of object containing the following properties:
45 // - dbg: A WorkerDebuggerInstance
46 // - workerTargetForm: The associated worker target instance form
47 // - workerThreadServerForwardingPrefix: The prefix used to forward events to the
48 // worker target on the worker thread ().
49 // - forwardingPrefix: Prefix used by the JSWindowActorTransport pair to communicate
50 // between content and parent processes.
51 // - sessionData: Data (targets, resources, …) the watcher wants to be notified about.
52 // See WatcherRegistry.getSessionData to see the full list of properties.
53 this._connections = new Map();
55 EventEmitter.decorate(this);
58 _onWorkerRegistered(dbg) {
59 if (!this._shouldHandleWorker(dbg)) {
63 for (const [watcherActorID, { connection, forwardingPrefix }] of this
65 this._createWorkerTargetActor({
74 _onWorkerUnregistered(dbg) {
75 for (const [watcherActorID, { workers, forwardingPrefix }] of this
77 // Check if the worker registration was handled for this watcherActorID.
78 const unregisteredActorIndex = workers.findIndex(worker => {
80 // Accessing the WorkerDebugger id might throw (NS_ERROR_UNEXPECTED).
81 return worker.dbg.id === dbg.id;
86 if (unregisteredActorIndex === -1) {
90 const { workerTargetForm, transport } = workers[unregisteredActorIndex];
94 this.sendAsyncMessage("DevToolsWorkerChild:workerTargetDestroyed", {
103 workers.splice(unregisteredActorIndex, 1);
107 onDOMWindowCreated() {
108 const { sharedData } = Services.cpmm;
109 const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
110 if (!sessionDataByWatcherActor) {
112 "Request to instantiate the target(s) for the Worker, but `sharedData` is empty about watched targets"
116 // Create one Target actor for each prefix/client which listen to workers
117 for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
118 const { targets, connectionPrefix, sessionContext } = sessionData;
120 targets?.includes("worker") &&
121 lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, {
122 acceptInitialDocument: true,
123 forceAcceptTopLevelTarget: true,
124 acceptSameProcessIframes: true,
127 this._watchWorkerTargets({
129 parentConnectionPrefix: connectionPrefix,
137 * Function handling messages sent by DevToolsWorkerParent (part of JSWindowActor API).
139 * @param {Object} message
140 * @param {String} message.name
141 * @param {*} message.data
143 receiveMessage(message) {
144 // All messages pass `sessionContext` (except packet) and are expected
145 // to match isWindowGlobalPartOfContext result.
146 if (message.name != "DevToolsWorkerParent:packet") {
147 const { browserId } = message.data.sessionContext;
148 // Re-check here, just to ensure that both parent and content processes agree
149 // on what should or should not be watched.
151 this.manager.browsingContext.browserId != browserId &&
152 !lazy.isWindowGlobalPartOfContext(
154 message.data.sessionContext,
156 acceptInitialDocument: true,
161 "Mismatch between DevToolsWorkerParent and DevToolsWorkerChild " +
162 (this.manager.browsingContext.browserId == browserId
163 ? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)"
164 : `expected browsing context with ID ${browserId}, but got ${this.manager.browsingContext.browserId}`)
169 switch (message.name) {
170 case "DevToolsWorkerParent:instantiate-already-available": {
171 const { watcherActorID, connectionPrefix, sessionData } = message.data;
173 return this._watchWorkerTargets({
175 parentConnectionPrefix: connectionPrefix,
179 case "DevToolsWorkerParent:destroy": {
180 const { watcherActorID } = message.data;
181 return this._destroyTargetActors(watcherActorID);
183 case "DevToolsWorkerParent:addOrSetSessionDataEntry": {
184 const { watcherActorID, type, entries, updateType } = message.data;
185 return this._addOrSetSessionDataEntry(
192 case "DevToolsWorkerParent:removeSessionDataEntry": {
193 const { watcherActorID, type, entries } = message.data;
194 return this._removeSessionDataEntry(watcherActorID, type, entries);
196 case "DevToolsWorkerParent:packet":
197 return this.emit("packet-received", message);
200 "Unsupported message in DevToolsWorkerParent: " + message.name
206 * Instantiate targets for existing workers, watch for worker registration and listen
207 * for resources on those workers, for given connection and context. Targets are sent
208 * to the DevToolsWorkerParent via the DevToolsWorkerChild:workerTargetAvailable message.
210 * @param {Object} options
211 * @param {Integer} options.watcherActorID: The ID of the WatcherActor who requested to
212 * observe and create these target actors.
213 * @param {String} options.parentConnectionPrefix: The prefix of the DevToolsServerConnection
214 * of the Watcher Actor. This is used to compute a unique ID for the target actor.
215 * @param {Object} options.sessionData: Data (targets, resources, …) the watcher wants
216 * to be notified about. See WatcherRegistry.getSessionData to see the full list
219 async _watchWorkerTargets({
221 parentConnectionPrefix,
224 if (this._connections.has(watcherActorID)) {
226 "DevToolsWorkerChild _watchWorkerTargets was called more than once" +
227 ` for the same Watcher (Actor ID: "${watcherActorID}")`
231 // Listen for new workers that will be spawned.
232 if (!this._workerDebuggerListener) {
233 this._workerDebuggerListener = {
234 onRegister: this._onWorkerRegistered.bind(this),
235 onUnregister: this._onWorkerUnregistered.bind(this),
237 lazy.wdm.addListener(this._workerDebuggerListener);
240 // Compute a unique prefix, just for this WindowGlobal,
241 // which will be used to create a JSWindowActorTransport pair between content and parent processes.
242 // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`,
243 // but here, we can't have access to any DevTools connection as we are really early in the content process startup
244 // WindowGlobalChild's innerWindowId should be unique across processes, so it should be safe?
245 // (this.manager == WindowGlobalChild interface)
246 const forwardingPrefix =
247 parentConnectionPrefix + "workerGlobal" + this.manager.innerWindowId;
249 const connection = this._createConnection(forwardingPrefix);
251 this._connections.set(watcherActorID, {
259 Array.from(lazy.wdm.getWorkerDebuggerEnumerator())
260 .filter(dbg => this._shouldHandleWorker(dbg))
262 this._createWorkerTargetActor({
272 _createConnection(forwardingPrefix) {
273 const { DevToolsServer } = lazy.Loader.require(
274 "resource://devtools/server/devtools-server.js"
277 DevToolsServer.init();
279 // We want a special server without any root actor and only target-scoped actors.
280 // We are going to spawn a WorkerTargetActor instance in the next few lines,
281 // it is going to act like a root actor without being one.
282 DevToolsServer.registerActors({ target: true });
284 const connection = DevToolsServer.connectToParentWindowActor(
293 * Indicates whether or not we should handle the worker debugger
295 * @param {WorkerDebugger} dbg: The worker debugger we want to check.
298 _shouldHandleWorker(dbg) {
299 // We only want to create targets for non-closed dedicated worker, in the same document
301 lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg) &&
302 dbg.type === Ci.nsIWorkerDebugger.TYPE_DEDICATED &&
303 dbg.windowIDs.includes(this.manager.innerWindowId)
307 async _createWorkerTargetActor({
313 // Prevent the debuggee from executing in this worker until the client has
314 // finished attaching to it. This call will throw if the debugger is already "registered"
315 // (i.e. if this is called outside of the register listener)
316 // See https://searchfox.org/mozilla-central/rev/84922363f4014eae684aabc4f1d06380066494c5/dom/workers/nsIWorkerDebugger.idl#55-66
318 dbg.setDebuggerReady(false);
321 const watcherConnectionData = this._connections.get(watcherActorID);
322 const { sessionData } = watcherConnectionData;
323 const workerThreadServerForwardingPrefix =
324 connection.allocID("workerTarget");
326 // Create the actual worker target actor, in the worker thread.
327 const { connectToWorker } = lazy.Loader.require(
328 "resource://devtools/server/connectors/worker-connector.js"
331 const onConnectToWorker = connectToWorker(
334 workerThreadServerForwardingPrefix,
337 sessionContext: sessionData.sessionContext,
342 await onConnectToWorker;
344 // onConnectToWorker can reject if the Worker Debugger is closed; so we only want to
345 // resume the debugger if it is not closed (otherwise it can cause crashes).
347 dbg.setDebuggerReady(true);
352 const { workerTargetForm, transport } = await onConnectToWorker;
355 this.sendAsyncMessage("DevToolsWorkerChild:workerTargetAvailable", {
361 // If there was an error while sending the message, we are not going to use this
362 // connection to communicate with the worker.
367 // Only add data to the connection if we successfully send the
368 // workerTargetAvailable message.
369 watcherConnectionData.workers.push({
373 workerThreadServerForwardingPrefix,
377 _destroyTargetActors(watcherActorID) {
378 const watcherConnectionData = this._connections.get(watcherActorID);
379 this._connections.delete(watcherActorID);
381 // This connection has already been cleaned?
382 if (!watcherConnectionData) {
384 `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
392 workerThreadServerForwardingPrefix,
393 } of watcherConnectionData.workers) {
395 if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
399 forwardingPrefix: workerThreadServerForwardingPrefix,
408 watcherConnectionData.connection.close();
411 async sendPacket(packet, prefix) {
412 return this.sendAsyncMessage("DevToolsWorkerChild:packet", {
418 async _addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) {
419 const watcherConnectionData = this._connections.get(watcherActorID);
420 if (!watcherConnectionData) {
424 lazy.SessionDataHelpers.addOrSetSessionDataEntry(
425 watcherConnectionData.sessionData,
434 workerThreadServerForwardingPrefix,
435 } of watcherConnectionData.workers) {
437 addOrSetSessionDataEntryInWorkerTarget({
439 workerThreadServerForwardingPrefix,
446 await Promise.all(promises);
449 _removeSessionDataEntry(watcherActorID, type, entries) {
450 const watcherConnectionData = this._connections.get(watcherActorID);
452 if (!watcherConnectionData) {
456 lazy.SessionDataHelpers.removeSessionDataEntry(
457 watcherConnectionData.sessionData,
464 workerThreadServerForwardingPrefix,
465 } of watcherConnectionData.workers) {
466 if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
469 type: "remove-session-data-entry",
470 forwardingPrefix: workerThreadServerForwardingPrefix,
479 handleEvent({ type }) {
480 // DOMWindowCreated is registered from the WatcherRegistry via `ActorManagerParent.addJSWindowActors`
481 // as a DOM event to be listened to and so is fired by JSWindowActor platform code.
482 if (type == "DOMWindowCreated") {
483 this.onDOMWindowCreated();
487 _removeExistingWorkerDebuggerListener() {
488 if (this._workerDebuggerListener) {
489 lazy.wdm.removeListener(this._workerDebuggerListener);
490 this._workerDebuggerListener = null;
495 * Part of JSActor API
496 * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
498 * > The didDestroy method, if present, will be called after the actor is no
499 * > longer able to receive any more messages.
502 this._removeExistingWorkerDebuggerListener();
504 for (const [watcherActorID, watcherConnectionData] of this._connections) {
505 const { connection } = watcherConnectionData;
506 this._destroyTargetActors(watcherActorID);
511 this._connections.clear();
516 * Communicate the type and entries to the Worker Target actor, via the WorkerDebugger.
518 * @param {WorkerDebugger} dbg
519 * @param {String} workerThreadServerForwardingPrefix
520 * @param {String} type
521 * Session data type name
522 * @param {Array} entries
523 * Session data entries to add or set.
524 * @param {String} updateType
525 * Either "add" or "set", to control if we should only add some items,
526 * or replace the whole data set with the new entries.
527 * @returns {Promise} Returns a Promise that resolves once the data entry were handled
528 * by the worker target.
530 function addOrSetSessionDataEntryInWorkerTarget({
532 workerThreadServerForwardingPrefix,
537 if (!lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
538 return Promise.resolve();
541 return new Promise(resolve => {
542 // Wait until we're notified by the worker that the resources are watched.
543 // This is important so we know existing resources were handled.
545 onMessage: message => {
546 message = JSON.parse(message);
547 if (message.type === "session-data-entry-added-or-set") {
549 dbg.removeListener(listener);
552 // Resolve if the worker is being destroyed so we don't have a dangling promise.
553 onClose: () => resolve(),
556 dbg.addListener(listener);
560 type: "add-or-set-session-data-entry",
561 forwardingPrefix: workerThreadServerForwardingPrefix,