Backed out 4 changesets (bug 1651522) for causing dt failures on devtools/shared...
[gecko.git] / devtools / server / connectors / js-window-actor / DevToolsWorkerChild.sys.mjs
blob142e9b1d6a4532b0c421808ac87561aa118bdcaa
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";
9 const lazy = {};
11 XPCOMUtils.defineLazyServiceGetter(
12   lazy,
13   "wdm",
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, {
26   SessionDataHelpers:
27     "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm",
28 });
29 ChromeUtils.defineESModuleGetters(lazy, {
30   isWindowGlobalPartOfContext:
31     "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
32 });
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 {
38   constructor() {
39     super();
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);
56   }
58   _onWorkerRegistered(dbg) {
59     if (!this._shouldHandleWorker(dbg)) {
60       return;
61     }
63     for (const [watcherActorID, { connection, forwardingPrefix }] of this
64       ._connections) {
65       this._createWorkerTargetActor({
66         dbg,
67         connection,
68         forwardingPrefix,
69         watcherActorID,
70       });
71     }
72   }
74   _onWorkerUnregistered(dbg) {
75     for (const [watcherActorID, { workers, forwardingPrefix }] of this
76       ._connections) {
77       // Check if the worker registration was handled for this watcherActorID.
78       const unregisteredActorIndex = workers.findIndex(worker => {
79         try {
80           // Accessing the WorkerDebugger id might throw (NS_ERROR_UNEXPECTED).
81           return worker.dbg.id === dbg.id;
82         } catch (e) {
83           return false;
84         }
85       });
86       if (unregisteredActorIndex === -1) {
87         continue;
88       }
90       const { workerTargetForm, transport } = workers[unregisteredActorIndex];
91       transport.close();
93       try {
94         this.sendAsyncMessage("DevToolsWorkerChild:workerTargetDestroyed", {
95           watcherActorID,
96           forwardingPrefix,
97           workerTargetForm,
98         });
99       } catch (e) {
100         return;
101       }
103       workers.splice(unregisteredActorIndex, 1);
104     }
105   }
107   onDOMWindowCreated() {
108     const { sharedData } = Services.cpmm;
109     const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
110     if (!sessionDataByWatcherActor) {
111       throw new Error(
112         "Request to instantiate the target(s) for the Worker, but `sharedData` is empty about watched targets"
113       );
114     }
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;
119       if (
120         targets?.includes("worker") &&
121         lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, {
122           acceptInitialDocument: true,
123           forceAcceptTopLevelTarget: true,
124           acceptSameProcessIframes: true,
125         })
126       ) {
127         this._watchWorkerTargets({
128           watcherActorID,
129           parentConnectionPrefix: connectionPrefix,
130           sessionData,
131         });
132       }
133     }
134   }
136   /**
137    * Function handling messages sent by DevToolsWorkerParent (part of JSWindowActor API).
138    *
139    * @param {Object} message
140    * @param {String} message.name
141    * @param {*} message.data
142    */
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.
150       if (
151         this.manager.browsingContext.browserId != browserId &&
152         !lazy.isWindowGlobalPartOfContext(
153           this.manager,
154           message.data.sessionContext,
155           {
156             acceptInitialDocument: true,
157           }
158         )
159       ) {
160         throw new Error(
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}`)
165         );
166       }
167     }
169     switch (message.name) {
170       case "DevToolsWorkerParent:instantiate-already-available": {
171         const { watcherActorID, connectionPrefix, sessionData } = message.data;
173         return this._watchWorkerTargets({
174           watcherActorID,
175           parentConnectionPrefix: connectionPrefix,
176           sessionData,
177         });
178       }
179       case "DevToolsWorkerParent:destroy": {
180         const { watcherActorID } = message.data;
181         return this._destroyTargetActors(watcherActorID);
182       }
183       case "DevToolsWorkerParent:addOrSetSessionDataEntry": {
184         const { watcherActorID, type, entries, updateType } = message.data;
185         return this._addOrSetSessionDataEntry(
186           watcherActorID,
187           type,
188           entries,
189           updateType
190         );
191       }
192       case "DevToolsWorkerParent:removeSessionDataEntry": {
193         const { watcherActorID, type, entries } = message.data;
194         return this._removeSessionDataEntry(watcherActorID, type, entries);
195       }
196       case "DevToolsWorkerParent:packet":
197         return this.emit("packet-received", message);
198       default:
199         throw new Error(
200           "Unsupported message in DevToolsWorkerParent: " + message.name
201         );
202     }
203   }
205   /**
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.
209    *
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
217    *        of properties.
218    */
219   async _watchWorkerTargets({
220     watcherActorID,
221     parentConnectionPrefix,
222     sessionData,
223   }) {
224     if (this._connections.has(watcherActorID)) {
225       throw new Error(
226         "DevToolsWorkerChild _watchWorkerTargets was called more than once" +
227           ` for the same Watcher (Actor ID: "${watcherActorID}")`
228       );
229     }
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),
236       };
237       lazy.wdm.addListener(this._workerDebuggerListener);
238     }
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, {
252       connection,
253       workers: [],
254       forwardingPrefix,
255       sessionData,
256     });
258     await Promise.all(
259       Array.from(lazy.wdm.getWorkerDebuggerEnumerator())
260         .filter(dbg => this._shouldHandleWorker(dbg))
261         .map(dbg =>
262           this._createWorkerTargetActor({
263             dbg,
264             connection,
265             forwardingPrefix,
266             watcherActorID,
267           })
268         )
269     );
270   }
272   _createConnection(forwardingPrefix) {
273     const { DevToolsServer } = lazy.Loader.require(
274       "resource://devtools/server/devtools-server.js"
275     );
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(
285       this,
286       forwardingPrefix
287     );
289     return connection;
290   }
292   /**
293    * Indicates whether or not we should handle the worker debugger
294    *
295    * @param {WorkerDebugger} dbg: The worker debugger we want to check.
296    * @returns {Boolean}
297    */
298   _shouldHandleWorker(dbg) {
299     // We only want to create targets for non-closed dedicated worker, in the same document
300     return (
301       lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg) &&
302       dbg.type === Ci.nsIWorkerDebugger.TYPE_DEDICATED &&
303       dbg.windowIDs.includes(this.manager.innerWindowId)
304     );
305   }
307   async _createWorkerTargetActor({
308     dbg,
309     connection,
310     forwardingPrefix,
311     watcherActorID,
312   }) {
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
317     try {
318       dbg.setDebuggerReady(false);
319     } catch (e) {}
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"
329     );
331     const onConnectToWorker = connectToWorker(
332       connection,
333       dbg,
334       workerThreadServerForwardingPrefix,
335       {
336         sessionData,
337         sessionContext: sessionData.sessionContext,
338       }
339     );
341     try {
342       await onConnectToWorker;
343     } catch (e) {
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).
346       if (!dbg.isClosed) {
347         dbg.setDebuggerReady(true);
348       }
349       return;
350     }
352     const { workerTargetForm, transport } = await onConnectToWorker;
354     try {
355       this.sendAsyncMessage("DevToolsWorkerChild:workerTargetAvailable", {
356         watcherActorID,
357         forwardingPrefix,
358         workerTargetForm,
359       });
360     } catch (e) {
361       // If there was an error while sending the message, we are not going to use this
362       // connection to communicate with the worker.
363       transport.close();
364       return;
365     }
367     // Only add data to the connection if we successfully send the
368     // workerTargetAvailable message.
369     watcherConnectionData.workers.push({
370       dbg,
371       transport,
372       workerTargetForm,
373       workerThreadServerForwardingPrefix,
374     });
375   }
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) {
383       console.error(
384         `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
385       );
386       return;
387     }
389     for (const {
390       dbg,
391       transport,
392       workerThreadServerForwardingPrefix,
393     } of watcherConnectionData.workers) {
394       try {
395         if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
396           dbg.postMessage(
397             JSON.stringify({
398               type: "disconnect",
399               forwardingPrefix: workerThreadServerForwardingPrefix,
400             })
401           );
402         }
403       } catch (e) {}
405       transport.close();
406     }
408     watcherConnectionData.connection.close();
409   }
411   async sendPacket(packet, prefix) {
412     return this.sendAsyncMessage("DevToolsWorkerChild:packet", {
413       packet,
414       prefix,
415     });
416   }
418   async _addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) {
419     const watcherConnectionData = this._connections.get(watcherActorID);
420     if (!watcherConnectionData) {
421       return;
422     }
424     lazy.SessionDataHelpers.addOrSetSessionDataEntry(
425       watcherConnectionData.sessionData,
426       type,
427       entries,
428       updateType
429     );
431     const promises = [];
432     for (const {
433       dbg,
434       workerThreadServerForwardingPrefix,
435     } of watcherConnectionData.workers) {
436       promises.push(
437         addOrSetSessionDataEntryInWorkerTarget({
438           dbg,
439           workerThreadServerForwardingPrefix,
440           type,
441           entries,
442           updateType,
443         })
444       );
445     }
446     await Promise.all(promises);
447   }
449   _removeSessionDataEntry(watcherActorID, type, entries) {
450     const watcherConnectionData = this._connections.get(watcherActorID);
452     if (!watcherConnectionData) {
453       return;
454     }
456     lazy.SessionDataHelpers.removeSessionDataEntry(
457       watcherConnectionData.sessionData,
458       type,
459       entries
460     );
462     for (const {
463       dbg,
464       workerThreadServerForwardingPrefix,
465     } of watcherConnectionData.workers) {
466       if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
467         dbg.postMessage(
468           JSON.stringify({
469             type: "remove-session-data-entry",
470             forwardingPrefix: workerThreadServerForwardingPrefix,
471             dataEntryType: type,
472             entries,
473           })
474         );
475       }
476     }
477   }
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();
484     }
485   }
487   _removeExistingWorkerDebuggerListener() {
488     if (this._workerDebuggerListener) {
489       lazy.wdm.removeListener(this._workerDebuggerListener);
490       this._workerDebuggerListener = null;
491     }
492   }
494   /**
495    * Part of JSActor API
496    * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
497    *
498    * > The didDestroy method, if present, will be called after the actor is no
499    * > longer able to receive any more messages.
500    */
501   didDestroy() {
502     this._removeExistingWorkerDebuggerListener();
504     for (const [watcherActorID, watcherConnectionData] of this._connections) {
505       const { connection } = watcherConnectionData;
506       this._destroyTargetActors(watcherActorID);
508       connection.close();
509     }
511     this._connections.clear();
512   }
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.
529  */
530 function addOrSetSessionDataEntryInWorkerTarget({
531   dbg,
532   workerThreadServerForwardingPrefix,
533   type,
534   entries,
535   updateType,
536 }) {
537   if (!lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
538     return Promise.resolve();
539   }
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.
544     const listener = {
545       onMessage: message => {
546         message = JSON.parse(message);
547         if (message.type === "session-data-entry-added-or-set") {
548           resolve();
549           dbg.removeListener(listener);
550         }
551       },
552       // Resolve if the worker is being destroyed so we don't have a dangling promise.
553       onClose: () => resolve(),
554     };
556     dbg.addListener(listener);
558     dbg.postMessage(
559       JSON.stringify({
560         type: "add-or-set-session-data-entry",
561         forwardingPrefix: workerThreadServerForwardingPrefix,
562         dataEntryType: type,
563         entries,
564         updateType,
565       })
566     );
567   });