DevToolsProcessChild.sys.mjs (19014B)
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/. */ 4 5 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; 6 import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs"; 7 8 const lazy = {}; 9 ChromeUtils.defineESModuleGetters( 10 lazy, 11 { 12 ContentScriptTargetWatcher: 13 "resource://devtools/server/connectors/js-process-actor/target-watchers/content_script.sys.mjs", 14 ProcessTargetWatcher: 15 "resource://devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs", 16 SessionDataHelpers: 17 "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", 18 ServiceWorkerTargetWatcher: 19 "resource://devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs", 20 SharedWorkerTargetWatcher: 21 "resource://devtools/server/connectors/js-process-actor/target-watchers/shared_worker.sys.mjs", 22 WorkerTargetWatcher: 23 "resource://devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs", 24 WindowGlobalTargetWatcher: 25 "resource://devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs", 26 }, 27 { global: "contextual" } 28 ); 29 30 // TargetActorRegistery has to be shared between all devtools instances 31 // and so is loaded into the shared global. 32 ChromeUtils.defineESModuleGetters( 33 lazy, 34 { 35 TargetActorRegistry: 36 "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs", 37 }, 38 { global: "shared" } 39 ); 40 41 export class DevToolsProcessChild extends JSProcessActorChild { 42 constructor() { 43 super(); 44 45 // The EventEmitter interface is used for DevToolsTransport's packet-received event. 46 EventEmitter.decorate(this); 47 } 48 49 #watchers = { 50 // Keys are target types, which are defined in this CommonJS Module: 51 // https://searchfox.org/mozilla-central/rev/0e9ea50a999420d93df0e4e27094952af48dd3b8/devtools/server/actors/targets/index.js#7-14 52 // We avoid loading it as this ESM should be lightweight and avoid spawning DevTools CommonJS Loader until 53 // whe know we have to instantiate a Target Actor. 54 frame: { 55 // Number of active watcher actors currently watching for the given target type 56 activeListener: 0, 57 58 // Instance of a target watcher class whose task is to observe new target instances 59 get watcher() { 60 return lazy.WindowGlobalTargetWatcher; 61 }, 62 }, 63 64 process: { 65 activeListener: 0, 66 get watcher() { 67 return lazy.ProcessTargetWatcher; 68 }, 69 }, 70 71 worker: { 72 activeListener: 0, 73 get watcher() { 74 return lazy.WorkerTargetWatcher; 75 }, 76 }, 77 78 service_worker: { 79 activeListener: 0, 80 get watcher() { 81 return lazy.ServiceWorkerTargetWatcher; 82 }, 83 }, 84 85 shared_worker: { 86 activeListener: 0, 87 get watcher() { 88 return lazy.SharedWorkerTargetWatcher; 89 }, 90 }, 91 92 content_script: { 93 activeListener: 0, 94 get watcher() { 95 return lazy.ContentScriptTargetWatcher; 96 }, 97 }, 98 }; 99 100 #initialized = false; 101 102 /** 103 * Called when this JSProcess Actor instantiate either when we start observing for first target types, 104 * or when the process just started. 105 */ 106 instantiate() { 107 if (this.#initialized) { 108 return; 109 } 110 this.#initialized = true; 111 // Create and watch for future target actors for each watcher currently watching some target types 112 for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) { 113 this.#watchInitialTargetsForWatcher(watcherDataObject); 114 } 115 } 116 117 /** 118 * Instantiate and watch future target actors based on the already watched targets. 119 * 120 * @param Object watcherDataObject 121 * See ContentProcessWatcherRegistry. 122 */ 123 #watchInitialTargetsForWatcher(watcherDataObject) { 124 const { sessionData, sessionContext } = watcherDataObject; 125 126 // About WebExtension, see note in addOrSetSessionDataEntry. 127 // Their target actor aren't created by this class, but session data is still managed by it 128 // and we need to pass the initial session data coming to already instantiated target actor. 129 if (sessionContext.type == "webextension") { 130 const { watcherActorID } = watcherDataObject; 131 const connectionPrefix = watcherActorID.replace(/watcher\d+$/, ""); 132 const targetActors = lazy.TargetActorRegistry.getTargetActors( 133 sessionContext, 134 connectionPrefix 135 ); 136 if (targetActors.length) { 137 // Pass initialization data to the target actor 138 for (const type in sessionData) { 139 // `sessionData` will also contain `browserId` as well as entries with empty arrays, 140 // which shouldn't be processed. 141 const entries = sessionData[type]; 142 if (!Array.isArray(entries) || !entries.length) { 143 continue; 144 } 145 targetActors[0].addOrSetSessionDataEntry(type, entries, false, "set"); 146 } 147 } 148 } 149 150 // Ignore the call if the watched targets property isn't populated yet. 151 // This typically happens when instantiating the JS Process Actor on toolbox opening, 152 // where the actor is spawn early and a watchTarget message comes later with the `targets` array set. 153 if (!sessionData.targets) { 154 return; 155 } 156 157 for (const targetType of sessionData.targets) { 158 this.#watchNewTargetTypeForWatcher(watcherDataObject, targetType, true); 159 } 160 } 161 162 /** 163 * Instantiate and watch future target actors based on the already watched targets. 164 * 165 * @param Object watcherDataObject 166 * See ContentProcessWatcherRegistry. 167 * @param String targetType 168 * New typeof target to start watching. 169 * @param Boolean isProcessActorStartup 170 * True when we are watching for targets during this JS Process actor instantiation. 171 * It shouldn't be the case on toolbox opening, but only when a new process starts. 172 * On toolbox opening, the Actor will receive an explicit watchTargets query. 173 */ 174 #watchNewTargetTypeForWatcher( 175 watcherDataObject, 176 targetType, 177 isProcessActorStartup 178 ) { 179 const { watchingTargetTypes } = watcherDataObject; 180 // Ensure creating and watching only once per target type and watcher actor. 181 if (watchingTargetTypes.includes(targetType)) { 182 return; 183 } 184 watchingTargetTypes.push(targetType); 185 186 // Update sessionData as watched target types are a Session Data 187 // used later for example by worker target watcher 188 lazy.SessionDataHelpers.addOrSetSessionDataEntry( 189 watcherDataObject.sessionData, 190 "targets", 191 [targetType], 192 "add" 193 ); 194 195 this.#watchers[targetType].activeListener++; 196 197 // Start listening for platform events when we are observing this type for the first time 198 if (this.#watchers[targetType].activeListener === 1) { 199 this.#watchers[targetType].watcher.watch(); 200 } 201 202 // And instantiate targets for the already existing instances 203 this.#watchers[targetType].watcher.createTargetsForWatcher( 204 watcherDataObject, 205 isProcessActorStartup 206 ); 207 } 208 209 /** 210 * Stop watching for all target types and destroy all existing targets actor 211 * related to a given watcher actor. 212 * 213 * @param {object} watcherDataObject 214 * @param {string} targetType 215 * @param {object} options 216 */ 217 #unwatchTargetsForWatcher(watcherDataObject, targetType, options) { 218 const { watchingTargetTypes } = watcherDataObject; 219 const targetTypeIndex = watchingTargetTypes.indexOf(targetType); 220 // Ignore targetTypes which were not observed 221 if (targetTypeIndex === -1) { 222 return; 223 } 224 // Update to the new list of currently watched target types 225 watchingTargetTypes.splice(targetTypeIndex, 1); 226 227 // Update sessionData as watched target types are a Session Data 228 // used later for example by worker target watcher 229 lazy.SessionDataHelpers.removeSessionDataEntry( 230 watcherDataObject.sessionData, 231 "targets", 232 [targetType] 233 ); 234 235 this.#watchers[targetType].activeListener--; 236 237 // Stop observing for platform events 238 if (this.#watchers[targetType].activeListener === 0) { 239 this.#watchers[targetType].watcher.unwatch(); 240 } 241 242 // Destroy all targets which are still instantiated for this type 243 this.#watchers[targetType].watcher.destroyTargetsForWatcher( 244 watcherDataObject, 245 options 246 ); 247 248 // Unregister the watcher if we stopped watching for all target types 249 if (!watchingTargetTypes.length) { 250 ContentProcessWatcherRegistry.remove(watcherDataObject); 251 } 252 253 // If we removed the last watcher, clean the internal state of this class. 254 if (ContentProcessWatcherRegistry.isEmpty()) { 255 this.didDestroy(options); 256 } 257 } 258 259 /** 260 * Cleanup everything around a given watcher actor 261 * 262 * @param {object} watcherDataObject 263 */ 264 #destroyWatcher(watcherDataObject) { 265 const { watchingTargetTypes } = watcherDataObject; 266 // Clone the array as it will be modified during the loop execution 267 for (const targetType of [...watchingTargetTypes]) { 268 this.#unwatchTargetsForWatcher(watcherDataObject, targetType); 269 } 270 } 271 272 /** 273 * Used by DevTools Transport to send packets to the content process. 274 * 275 * @param {JSON} packet 276 * @param {string} prefix 277 */ 278 sendPacket(packet, prefix) { 279 this.sendAsyncMessage("DevToolsProcessChild:packet", { packet, prefix }); 280 } 281 282 /** 283 * JsWindowActor API 284 */ 285 286 async sendQuery(msg, args) { 287 try { 288 const res = await super.sendQuery(msg, args); 289 return res; 290 } catch (e) { 291 console.error("Failed to sendQuery in DevToolsProcessChild", msg); 292 console.error(e.toString()); 293 throw e; 294 } 295 } 296 297 /** 298 * Called by the JSProcessActor API when the process process sent us a message. 299 */ 300 receiveMessage(message) { 301 switch (message.name) { 302 case "DevToolsProcessParent:watchTargets": { 303 const { watcherActorID, targetType } = message.data; 304 const watcherDataObject = 305 ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); 306 return this.#watchNewTargetTypeForWatcher( 307 watcherDataObject, 308 targetType 309 ); 310 } 311 case "DevToolsProcessParent:unwatchTargets": { 312 const { watcherActorID, targetType, options } = message.data; 313 const watcherDataObject = 314 ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); 315 return this.#unwatchTargetsForWatcher( 316 watcherDataObject, 317 targetType, 318 options 319 ); 320 } 321 case "DevToolsProcessParent:addOrSetSessionDataEntry": { 322 const { watcherActorID, type, entries, updateType } = message.data; 323 return this.#addOrSetSessionDataEntry( 324 watcherActorID, 325 type, 326 entries, 327 updateType 328 ); 329 } 330 case "DevToolsProcessParent:removeSessionDataEntry": { 331 const { watcherActorID, type, entries } = message.data; 332 return this.#removeSessionDataEntry(watcherActorID, type, entries); 333 } 334 case "DevToolsProcessParent:destroyWatcher": { 335 const { watcherActorID } = message.data; 336 const watcherDataObject = 337 ContentProcessWatcherRegistry.getWatcherDataObject( 338 watcherActorID, 339 true 340 ); 341 // The watcher may already be destroyed if the client unwatched for all target types. 342 if (watcherDataObject) { 343 return this.#destroyWatcher(watcherDataObject); 344 } 345 return null; 346 } 347 case "DevToolsProcessParent:packet": 348 return this.emit("packet-received", message); 349 default: 350 throw new Error( 351 "Unsupported message in DevToolsProcessParent: " + message.name 352 ); 353 } 354 } 355 356 /** 357 * The parent process requested that some session data have been added or set. 358 * 359 * @param {string} watcherActorID 360 * The Watcher Actor ID requesting to add new session data 361 * @param {string} type 362 * The type of data to be added 363 * @param {Array<object>} entries 364 * The values to be added to this type of data 365 * @param {string} updateType 366 * "add" will only add the new entries in the existing data set. 367 * "set" will update the data set with the new entries. 368 */ 369 async #addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) { 370 const watcherDataObject = 371 ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); 372 373 // Maintain the copy of `sessionData` so that it is up-to-date when 374 // a new worker target needs to be instantiated 375 const { sessionData } = watcherDataObject; 376 lazy.SessionDataHelpers.addOrSetSessionDataEntry( 377 sessionData, 378 type, 379 entries, 380 updateType 381 ); 382 383 // This type is really specific to Service Workers and doesn't need to be transferred to any target. 384 // We only need to instantiate and destroy the target actors based on this new host. 385 const { watchingTargetTypes } = watcherDataObject; 386 if (type == "browser-element-host") { 387 if (watchingTargetTypes.includes("service_worker")) { 388 this.#watchers.service_worker.watcher.updateBrowserElementHost( 389 watcherDataObject 390 ); 391 } 392 return; 393 } 394 395 const promises = []; 396 for (const targetActor of watcherDataObject.actors) { 397 promises.push( 398 targetActor.addOrSetSessionDataEntry(type, entries, false, updateType) 399 ); 400 } 401 402 // Very special codepath for Web Extensions. 403 // Their WebExtension Target Actor is still created manually by WebExtensionDescritpor.getTarget, 404 // via a message manager. That, instead of being instantiated via the WatcherActor.watchTargets and this JSProcess actor. 405 // The Watcher Actor will still instantiate a JS Actor for the WebExt DOM Content Process 406 // and send the addOrSetSessionDataEntry query. But as the target actor isn't managed by the JS Actor, 407 // we have to manually retrieve it via the TargetActorRegistry. 408 if (sessionData.sessionContext.type == "webextension") { 409 const connectionPrefix = watcherActorID.replace(/watcher\d+$/, ""); 410 const targetActors = lazy.TargetActorRegistry.getTargetActors( 411 sessionData.sessionContext, 412 connectionPrefix 413 ); 414 // We will have a single match only in the DOM Process where the add-on runs 415 if (targetActors.length) { 416 promises.push( 417 targetActors[0].addOrSetSessionDataEntry( 418 type, 419 entries, 420 false, 421 updateType 422 ) 423 ); 424 } 425 } 426 await Promise.all(promises); 427 428 if (watchingTargetTypes.includes("worker")) { 429 await this.#watchers.worker.watcher.addOrSetSessionDataEntry( 430 watcherDataObject, 431 type, 432 entries, 433 updateType 434 ); 435 } 436 if (watchingTargetTypes.includes("service_worker")) { 437 await this.#watchers.service_worker.watcher.addOrSetSessionDataEntry( 438 watcherDataObject, 439 type, 440 entries, 441 updateType 442 ); 443 } 444 if (watchingTargetTypes.includes("shared_worker")) { 445 await this.#watchers.shared_worker.watcher.addOrSetSessionDataEntry( 446 watcherDataObject, 447 type, 448 entries, 449 updateType 450 ); 451 } 452 } 453 454 /** 455 * The parent process requested that some session data have been removed. 456 * 457 * @param {string} watcherActorID 458 * The Watcher Actor ID requesting to remove session data 459 * @param {string}} type 460 * The type of data to be removed 461 * @param {Array<object>} entries 462 * The values to be removed to this type of data 463 */ 464 #removeSessionDataEntry(watcherActorID, type, entries) { 465 const watcherDataObject = 466 ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID, true); 467 468 // When we unwatch resources after targets during the devtools shutdown, 469 // the watcher will be removed on last target type unwatch. 470 if (!watcherDataObject) { 471 return; 472 } 473 const { actors, sessionData, watchingTargetTypes } = watcherDataObject; 474 475 // Maintain the copy of `sessionData` so that it is up-to-date when 476 // a new worker target needs to be instantiated 477 lazy.SessionDataHelpers.removeSessionDataEntry(sessionData, type, entries); 478 479 for (const targetActor of actors) { 480 targetActor.removeSessionDataEntry(type, entries); 481 } 482 483 // Special code paths for webextension toolboxes and worker targets 484 // See addOrSetSessionDataEntry for more details. 485 486 if (sessionData.sessionContext.type == "webextension") { 487 const connectionPrefix = watcherActorID.replace(/watcher\d+$/, ""); 488 const targetActors = lazy.TargetActorRegistry.getTargetActors( 489 sessionData.sessionContext, 490 connectionPrefix 491 ); 492 if (targetActors.length) { 493 targetActors[0].removeSessionDataEntry(type, entries); 494 } 495 } 496 497 if (watchingTargetTypes.includes("worker")) { 498 this.#watchers.worker.watcher.removeSessionDataEntry( 499 watcherDataObject, 500 type, 501 entries 502 ); 503 } 504 if (watchingTargetTypes.includes("service_worker")) { 505 this.#watchers.service_worker.watcher.removeSessionDataEntry( 506 watcherDataObject, 507 type, 508 entries 509 ); 510 } 511 if (watchingTargetTypes.includes("shared_worker")) { 512 this.#watchers.shared_worker.watcher.removeSessionDataEntry( 513 watcherDataObject, 514 type, 515 entries 516 ); 517 } 518 } 519 520 /** 521 * Observer service notification handler. 522 * 523 * @param {DOMWindow|Document} subject 524 * A window for *-document-global-created 525 * A document for *-page-{shown|hide} 526 * @param {string} topic 527 */ 528 observe = (subject, topic) => { 529 if (topic === "init-devtools-content-process-actor") { 530 // This is triggered by the process actor registration and some code in process-helper.js 531 // which defines a unique topic to be observed 532 this.instantiate(); 533 } 534 }; 535 536 /** 537 * Called by JS Process Actor API when the current process is destroyed, 538 * but also within this class when the last watcher stopped watching for targets. 539 */ 540 didDestroy() { 541 // Unregister all the active watchers. 542 // This will destroy all the active target actors and unregister the target observers. 543 for (const watcherDataObject of ContentProcessWatcherRegistry.getAllExistingWatchersDataObjects()) { 544 this.#destroyWatcher(watcherDataObject); 545 } 546 547 // The previous for loop should have removed all the elements, 548 // but just to be safe, wipe all stored data to avoid any possible leak. 549 ContentProcessWatcherRegistry.clear(); 550 } 551 } 552 553 export class BrowserToolboxDevToolsProcessChild extends DevToolsProcessChild {}