watcher.js (33818B)
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 "use strict"; 6 const { Actor } = require("resource://devtools/shared/protocol.js"); 7 const { watcherSpec } = require("resource://devtools/shared/specs/watcher.js"); 8 9 const Resources = require("resource://devtools/server/actors/resources/index.js"); 10 const { TargetActorRegistry } = ChromeUtils.importESModule( 11 "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs", 12 { global: "shared" } 13 ); 14 const { ParentProcessWatcherRegistry } = ChromeUtils.importESModule( 15 "resource://devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs", 16 // ParentProcessWatcherRegistry needs to be a true singleton and loads ActorManagerParent 17 // which also has to be a true singleton. 18 { global: "shared" } 19 ); 20 const { getAllBrowsingContextsForContext } = ChromeUtils.importESModule( 21 "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", 22 { global: "contextual" } 23 ); 24 const { 25 SESSION_TYPES, 26 } = require("resource://devtools/server/actors/watcher/session-context.js"); 27 28 loader.lazyRequireGetter( 29 this, 30 "throttle", 31 "resource://devtools/shared/throttle.js", 32 true 33 ); 34 loader.lazyRequireGetter( 35 this, 36 "NetworkParentActor", 37 "resource://devtools/server/actors/network-monitor/network-parent.js", 38 true 39 ); 40 loader.lazyRequireGetter( 41 this, 42 "BlackboxingActor", 43 "resource://devtools/server/actors/blackboxing.js", 44 true 45 ); 46 loader.lazyRequireGetter( 47 this, 48 "BreakpointListActor", 49 "resource://devtools/server/actors/breakpoint-list.js", 50 true 51 ); 52 loader.lazyRequireGetter( 53 this, 54 "TargetConfigurationActor", 55 "resource://devtools/server/actors/target-configuration.js", 56 true 57 ); 58 loader.lazyRequireGetter( 59 this, 60 "ThreadConfigurationActor", 61 "resource://devtools/server/actors/thread-configuration.js", 62 true 63 ); 64 65 const RESOURCES_THROTTLING_DELAY = 100; 66 67 exports.WatcherActor = class WatcherActor extends Actor { 68 /** 69 * Initialize a new WatcherActor which is the main entry point to debug 70 * something. The main features of this actor are to: 71 * - observe targets related to the context we are debugging. 72 * This is done via watchTargets/unwatchTargets methods, and 73 * target-available-form/target-destroyed-form events. 74 * - observe resources related to the observed targets. 75 * This is done via watchResources/unwatchResources methods, and 76 * resources-available-array/resources-updated-array/resources-destroyed-array events. 77 * Note that these events are also emited on both the watcher actor, 78 * for resources observed from the parent process, as well as on the 79 * target actors, when the resources are observed from the target's process or thread. 80 * 81 * @param {DevToolsServerConnection} conn 82 * The connection to use in order to communicate back to the client. 83 * @param {object} sessionContext 84 * The Session Context to help know what is debugged. 85 * See devtools/server/actors/watcher/session-context.js 86 * @param {number} sessionContext.browserId: If this is a "browser-element" context type, 87 * the "browserId" of the <browser> element we would like to debug. 88 * @param {boolean} sessionContext.isServerTargetSwitchingEnabled: Flag to to know if we should 89 * spawn new top level targets for the debugged context. 90 */ 91 constructor(conn, sessionContext) { 92 super(conn, watcherSpec); 93 this._sessionContext = sessionContext; 94 if (sessionContext.type == SESSION_TYPES.BROWSER_ELEMENT) { 95 // Retrieve the <browser> element for the given browser ID 96 const browsingContext = BrowsingContext.getCurrentTopByBrowserId( 97 sessionContext.browserId 98 ); 99 if (!browsingContext) { 100 throw new Error( 101 "Unable to retrieve the <browser> element for browserId=" + 102 sessionContext.browserId 103 ); 104 } 105 this._browserElement = browsingContext.embedderElement; 106 } 107 108 this.watcherConnectionPrefix = conn.allocID("watcher"); 109 110 // Lists of resources available/updated/destroyed RDP packet 111 // currently queued which will be emitted after a throttle delay. 112 this.#throttledResources = { 113 available: [], 114 updated: [], 115 destroyed: [], 116 }; 117 118 this.#throttledEmitResources = throttle( 119 this.emitResources.bind(this), 120 RESOURCES_THROTTLING_DELAY 121 ); 122 123 // Sometimes we get iframe targets before the top-level targets 124 // mostly when doing bfcache navigations, lets cache the early iframes targets and 125 // flush them after the top-level target is available. See Bug 1726568 for details. 126 this._earlyIframeTargets = {}; 127 128 // All currently available WindowGlobal target's form, keyed by `innerWindowId`. 129 // 130 // This helps to: 131 // - determine if the iframe targets are early or not. 132 // i.e. if it is notified before its parent target is available. 133 // - notify the destruction of all children targets when a parent is destroyed. 134 // i.e. have a reliable order of destruction between parent and children. 135 // 136 // Note that there should be just one top-level window target at a time, 137 // but there are certain cases when a new target is available before the 138 // old target is destroyed. 139 this._currentWindowGlobalTargets = new Map(); 140 141 // The Browser Toolbox requires to load modules in a distinct compartment in order 142 // to be able to debug system compartments modules (most of Firefox internal codebase). 143 // This is a requirement coming from SpiderMonkey Debugger API and relates to the thread actor. 144 this._jsActorName = 145 sessionContext.type == SESSION_TYPES.ALL 146 ? "BrowserToolboxDevToolsProcess" 147 : "DevToolsProcess"; 148 } 149 150 #throttledResources; 151 #throttledEmitResources; 152 153 get sessionContext() { 154 return this._sessionContext; 155 } 156 157 /** 158 * If we are debugging only one Tab or Document, returns its BrowserElement. 159 * For Tabs, it will be the <browser> element used to load the web page. 160 * 161 * This is typicaly used to fetch: 162 * - its `browserId` attribute, which uniquely defines it, 163 * - its `browsingContextID` or `browsingContext`, which helps inspecting its content. 164 */ 165 get browserElement() { 166 return this._browserElement; 167 } 168 169 getAllBrowsingContexts() { 170 return getAllBrowsingContextsForContext(this.sessionContext); 171 } 172 173 /** 174 * Helper to know if the context we are debugging has been already destroyed 175 */ 176 isContextDestroyed() { 177 if (this.sessionContext.type == "browser-element") { 178 return !this.browserElement.browsingContext; 179 } else if (this.sessionContext.type == "webextension") { 180 // This is no obvious browsing context to target for extensions, so always consider it running 181 return false; 182 } else if (this.sessionContext.type == "all") { 183 return false; 184 } 185 throw new Error( 186 "Unsupported session context type: " + this.sessionContext.type 187 ); 188 } 189 190 destroy() { 191 // Only try to notify content processes if the watcher was in the registry. 192 // Otherwise it means that it wasn't connected to any process and the JS Process Actor 193 // wouldn't be registered. 194 if (ParentProcessWatcherRegistry.getWatcher(this.actorID)) { 195 // Emit one IPC message on destroy to all the processes 196 const domProcesses = ChromeUtils.getAllDOMProcesses(); 197 for (const domProcess of domProcesses) { 198 domProcess.getActor(this._jsActorName).destroyWatcher({ 199 watcherActorID: this.actorID, 200 }); 201 } 202 } 203 204 // Ensure destroying all Resource Watcher instantiated in the parent process 205 Resources.unwatchResources( 206 this, 207 Resources.getParentProcessResourceTypes(Object.values(Resources.TYPES)) 208 ); 209 210 ParentProcessWatcherRegistry.unregisterWatcher(this.actorID); 211 212 // In case the watcher actor is leaked, prevent leaking the browser window 213 this._browserElement = null; 214 215 // Destroy the actor in order to ensure destroying all its children actors. 216 // As this actor is a pool with children actors, when the transport/connection closes 217 // we expect all actors and its children to be destroyed. 218 super.destroy(); 219 } 220 221 /** 222 * Get the list of the currently watched resources for this watcher. 223 * 224 * @return Array<String> 225 * Returns the list of currently watched resource types. 226 */ 227 get sessionData() { 228 return ParentProcessWatcherRegistry.getSessionData(this); 229 } 230 231 form() { 232 return { 233 actor: this.actorID, 234 // The resources and target traits should be removed all at the same time since the 235 // client has generic ways to deal with all of them (See Bug 1680280). 236 traits: { 237 ...this.sessionContext.supportedTargets, 238 resources: this.sessionContext.supportedResources, 239 }, 240 }; 241 } 242 243 /** 244 * Start watching for a new target type. 245 * 246 * This will instantiate Target Actors for existing debugging context of this type, 247 * but will also create actors as context of this type get created. 248 * The actors are notified to the client via "target-available-form" RDP events. 249 * We also notify about target actors destruction via "target-destroyed-form". 250 * Note that we are guaranteed to receive all existing target actor by the time this method 251 * resolves. 252 * 253 * @param {string} targetType 254 * Type of context to observe. See Targets.TYPES object. 255 */ 256 async watchTargets(targetType) { 257 ParentProcessWatcherRegistry.watchTargets(this, targetType); 258 259 // When debugging a tab, ensure processing the top level target first 260 // (for now, other session context types are instantiating the top level target 261 // from the descriptor's getTarget method instead of the Watcher) 262 let topLevelTargetProcess; 263 if (this.sessionContext.type == SESSION_TYPES.BROWSER_ELEMENT) { 264 topLevelTargetProcess = 265 this.browserElement.browsingContext.currentWindowGlobal?.domProcess; 266 if (topLevelTargetProcess) { 267 await topLevelTargetProcess.getActor(this._jsActorName).watchTargets({ 268 watcherActorID: this.actorID, 269 targetType, 270 }); 271 // Stop execution if we were destroyed in the meantime 272 if (this.isDestroyed()) { 273 return; 274 } 275 } 276 } 277 278 // We have to reach out all the content processes as the page may navigate 279 // to any other content process when navigating to another origin. 280 // It may even run in the parent process when loading about:robots. 281 const domProcesses = ChromeUtils.getAllDOMProcesses(); 282 const promises = []; 283 for (const domProcess of domProcesses) { 284 if (domProcess == topLevelTargetProcess) { 285 continue; 286 } 287 promises.push( 288 domProcess 289 .getActor(this._jsActorName) 290 .watchTargets({ 291 watcherActorID: this.actorID, 292 targetType, 293 }) 294 .catch(e => { 295 // Ignore any process that got destroyed while trying to send the request 296 if (!domProcess.canSend) { 297 console.warn( 298 "Content process closed while requesting targets", 299 domProcess.name, 300 domProcess.remoteType 301 ); 302 return; 303 } 304 throw e; 305 }) 306 ); 307 } 308 await Promise.all(promises); 309 } 310 311 /** 312 * Stop watching for a given target type. 313 * 314 * @param {string} targetType 315 * Type of context to observe. See Targets.TYPES object. 316 * @param {object} options 317 * @param {boolean} options.isModeSwitching 318 * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref 319 */ 320 unwatchTargets(targetType, options = {}) { 321 const isWatchingTargets = ParentProcessWatcherRegistry.unwatchTargets( 322 this, 323 targetType 324 ); 325 if (!isWatchingTargets) { 326 return; 327 } 328 329 const domProcesses = ChromeUtils.getAllDOMProcesses(); 330 for (const domProcess of domProcesses) { 331 domProcess.getActor(this._jsActorName).unwatchTargets({ 332 watcherActorID: this.actorID, 333 targetType, 334 options, 335 }); 336 } 337 } 338 339 /** 340 * Flush any early iframe targets relating to this top level 341 * window target. 342 * 343 * @param {number} topInnerWindowID 344 */ 345 _flushIframeTargets(topInnerWindowID) { 346 while (this._earlyIframeTargets[topInnerWindowID]?.length > 0) { 347 const actor = this._earlyIframeTargets[topInnerWindowID].shift(); 348 this.emit("target-available-form", actor); 349 } 350 } 351 352 /** 353 * Called by a Watcher module, whenever a new target is available 354 */ 355 notifyTargetAvailable(actor) { 356 // Emit immediately for worker, process & extension targets 357 // as they don't have a parent browsing context. 358 if (!actor.traits?.isBrowsingContext) { 359 this.emit("target-available-form", actor); 360 return; 361 } 362 363 // If isBrowsingContext trait is true, we are processing a WindowGlobalTarget. 364 // (this trait should be renamed) 365 this._currentWindowGlobalTargets.set(actor.innerWindowId, actor); 366 367 // The top-level is always the same for the browser-toolbox 368 if (this.sessionContext.type == "all") { 369 this.emit("target-available-form", actor); 370 return; 371 } 372 373 if (actor.isTopLevelTarget) { 374 this.emit("target-available-form", actor); 375 // Flush any existing early iframe targets 376 this._flushIframeTargets(actor.innerWindowId); 377 378 if (this.sessionContext.type == SESSION_TYPES.BROWSER_ELEMENT) { 379 // Ignore any pending exception as this request may be pending 380 // while the toolbox closes. And we don't want to delay target emission 381 // on this as this is a implementation detail. 382 this.updateDomainSessionDataForServiceWorkers(actor.url).catch( 383 () => {} 384 ); 385 } 386 } else if (this._currentWindowGlobalTargets.has(actor.topInnerWindowId)) { 387 // Emit the event immediately if the top-level target is already available 388 this.emit("target-available-form", actor); 389 } else if (this._earlyIframeTargets[actor.topInnerWindowId]) { 390 // Add the early iframe target to the list of other early targets. 391 this._earlyIframeTargets[actor.topInnerWindowId].push(actor); 392 } else { 393 // Set the first early iframe target 394 this._earlyIframeTargets[actor.topInnerWindowId] = [actor]; 395 } 396 } 397 398 /** 399 * Called by a Watcher module, whenever a target has been destroyed 400 * 401 * @param {object} actor 402 * the actor form of the target being destroyed 403 * @param {object} options 404 * @param {boolean} options.isModeSwitching 405 * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref 406 */ 407 async notifyTargetDestroyed(actor, options = {}) { 408 // Emit immediately for worker, process & extension targets 409 // as they don't have a parent browsing context. 410 if (!actor.innerWindowId) { 411 this.emit("target-destroyed-form", actor, options); 412 return; 413 } 414 // Flush all iframe targets if we are destroying a top level target. 415 if (actor.isTopLevelTarget) { 416 // First compute the list of children actors, as notifyTargetDestroy will mutate _currentWindowGlobalTargets 417 const childrenActors = [ 418 ...this._currentWindowGlobalTargets.values(), 419 ].filter( 420 form => 421 form.topInnerWindowId == actor.innerWindowId && 422 // Ignore the top level target itself, because its topInnerWindowId will be its innerWindowId 423 form.innerWindowId != actor.innerWindowId 424 ); 425 childrenActors.map(form => this.notifyTargetDestroyed(form, options)); 426 } 427 if (this._earlyIframeTargets[actor.innerWindowId]) { 428 delete this._earlyIframeTargets[actor.innerWindowId]; 429 } 430 this._currentWindowGlobalTargets.delete(actor.innerWindowId); 431 const documentEventWatcher = Resources.getResourceWatcher( 432 this, 433 Resources.TYPES.DOCUMENT_EVENT 434 ); 435 // If we have a Watcher class instantiated, ensure that target-destroyed is sent 436 // *after* DOCUMENT_EVENT's will-navigate. Otherwise this resource will have an undefined 437 // `targetFront` attribute, as it is associated with the target from which we navigate 438 // and not the one we navigate to. 439 // 440 // About documentEventWatcher check: We won't have any watcher class if we aren't 441 // using server side Watcher classes. 442 // i.e. when we are using the legacy listener for DOCUMENT_EVENT. 443 // This is still the case for all toolboxes but the one for local and remote tabs. 444 // 445 // About isServerTargetSwitchingEnabled check: if we are using the watcher class 446 // we may still use client side target, which will still use legacy listeners for 447 // will-navigate and so will-navigate will be emitted by the target actor itself. 448 // 449 // About isTopLevelTarget check: only top level targets emit will-navigate, 450 // so there is no reason to delay target-destroy for remote iframes. 451 if ( 452 documentEventWatcher && 453 this.sessionContext.isServerTargetSwitchingEnabled && 454 actor.isTopLevelTarget 455 ) { 456 await documentEventWatcher.onceWillNavigateIsEmitted(actor.innerWindowId); 457 } 458 this.emit("target-destroyed-form", actor, options); 459 } 460 461 /** 462 * Given a browsingContextID, returns its parent browsingContextID. Returns null if a 463 * parent browsing context couldn't be found. Throws if the browsing context 464 * corresponding to the passed browsingContextID couldn't be found. 465 * 466 * @param {Integer} browsingContextID 467 * @returns {Integer|null} 468 */ 469 getParentBrowsingContextID(browsingContextID) { 470 const browsingContext = BrowsingContext.get(browsingContextID); 471 if (!browsingContext) { 472 throw new Error( 473 `BrowsingContext with ID=${browsingContextID} doesn't exist.` 474 ); 475 } 476 // Top-level documents of tabs, loaded in a <browser> element expose a null `parent`. 477 // i.e. Their BrowsingContext has no parent and is considered top level. 478 // But... in the context of the Browser Toolbox, we still consider them as child of the browser window. 479 // So, for them, fallback on `embedderWindowGlobal`, which will typically be the WindowGlobal for browser.xhtml. 480 if (browsingContext.parent) { 481 return browsingContext.parent.id; 482 } 483 if (browsingContext.embedderWindowGlobal) { 484 return browsingContext.embedderWindowGlobal.browsingContext.id; 485 } 486 return null; 487 } 488 489 /** 490 * Called by Resource Watchers, when new resources are available, updated or destroyed. 491 * 492 * @param String updateType 493 * Can be "available", "updated" or "destroyed" 494 * @param String resourceType 495 * The type of resources to be notified about. 496 * @param Array<json> resources 497 * List of all resource's form. A resource is a JSON object piped over to the client. 498 * It can contain actor IDs, actor forms, to be manually marshalled by the client. 499 */ 500 notifyResources(updateType, resourceType, resources) { 501 if (resources.length === 0) { 502 // Don't try to emit if the resources array is empty. 503 return; 504 } 505 506 const shouldEmitSynchronously = 507 resourceType == Resources.TYPES.DOCUMENT_EVENT && 508 resources.some(resource => resource.name == "will-navigate"); 509 510 // If the last throttled resources were of the same resource type, 511 // augment the resources array with the new resources 512 const lastResourceInThrottleCache = 513 this.#throttledResources[updateType].at(-1); 514 if ( 515 lastResourceInThrottleCache && 516 lastResourceInThrottleCache[0] === resourceType 517 ) { 518 lastResourceInThrottleCache[1].push.apply( 519 lastResourceInThrottleCache[1], 520 resources 521 ); 522 } else { 523 // Otherwise, add a new item in the throttle queue with the resource type 524 this.#throttledResources[updateType].push([resourceType, resources]); 525 } 526 527 // Force firing resources immediately when the DOCUMENT_EVENT's will-navigate is received 528 // This will force clearing resources on the client side ASAP. 529 // Otherwise we might emit some other RDP event (outside of resources), 530 // which will be cleared by the throttled/delayed will-navigate. 531 if (shouldEmitSynchronously) { 532 this.emitResources(); 533 } else { 534 this.#throttledEmitResources(); 535 } 536 } 537 538 /** 539 * Flush resources to DevTools transport layer, actually sending all resource update packets 540 */ 541 emitResources() { 542 if (this.isDestroyed()) { 543 return; 544 } 545 for (const updateType of ["available", "updated", "destroyed"]) { 546 const resources = this.#throttledResources[updateType]; 547 if (!resources.length) { 548 continue; 549 } 550 this.#throttledResources[updateType] = []; 551 this.emit(`resources-${updateType}-array`, resources); 552 } 553 } 554 555 /** 556 * Try to retrieve Target Actors instantiated in the parent process which aren't 557 * instantiated via the Watcher actor (and its dependencies): 558 * - top level target for the browser toolboxes 559 * - xpcshell targets for xpcshell debugging 560 * 561 * See comment in `watchResources`. 562 * 563 * @return {Set<TargetActor>} Matching target actors. 564 */ 565 getTargetActorsInParentProcess() { 566 if (TargetActorRegistry.xpcShellTargetActors.size) { 567 return TargetActorRegistry.xpcShellTargetActors; 568 } 569 570 // Note: For browser-element debugging, the WindowGlobalTargetActor returned here is created 571 // for a parent process page and lives in the parent process. 572 const actors = TargetActorRegistry.getTargetActors( 573 this.sessionContext, 574 // Note that we aren't using watcherConnectionPrefix as the ParentProcessTargetActor 575 // are registered in `this.conn` (i.e The connection which is bound to the client) 576 // directly and not in the DevToolsServerConnection running in the content process with `watcherConnectionPrefix` 577 this.conn.prefix 578 ); 579 580 switch (this.sessionContext.type) { 581 case "all": { 582 const parentProcessTargetActor = actors.find( 583 actor => actor.typeName === "parentProcessTarget" 584 ); 585 if (parentProcessTargetActor) { 586 return new Set([parentProcessTargetActor]); 587 } 588 return new Set(); 589 } 590 case "browser-element": 591 case "webextension": 592 // All target actors for browser-element and webextension sessions 593 // should be created using the JS Window actors. 594 return new Set(); 595 default: 596 throw new Error( 597 "Unsupported session context type: " + this.sessionContext.type 598 ); 599 } 600 } 601 602 /** 603 * Start watching for a list of resource types. 604 * This should only resolve once all "already existing" resources of these types 605 * are notified to the client via resources-available-array event on related target actors. 606 * 607 * @param {Array<string>} resourceTypes 608 * List of all types to listen to. 609 */ 610 async watchResources(resourceTypes) { 611 // First process resources which have to be listened from the parent process 612 // (the watcher actor always runs in the parent process) 613 await Resources.watchResources( 614 this, 615 Resources.getParentProcessResourceTypes(resourceTypes) 616 ); 617 618 // Bail out early if all resources were watched from parent process. 619 // In this scenario, we do not need to update these resource types in the ParentProcessWatcherRegistry 620 // as targets do not care about them. 621 if (!Resources.hasResourceTypesForTargets(resourceTypes)) { 622 return; 623 } 624 625 ParentProcessWatcherRegistry.watchResources(this, resourceTypes); 626 627 const promises = []; 628 const domProcesses = ChromeUtils.getAllDOMProcesses(); 629 for (const domProcess of domProcesses) { 630 promises.push( 631 domProcess 632 .getActor(this._jsActorName) 633 .addOrSetSessionDataEntry({ 634 watcherActorID: this.actorID, 635 sessionContext: this.sessionContext, 636 type: "resources", 637 entries: resourceTypes, 638 updateType: "add", 639 }) 640 .catch(e => { 641 // Ignore any process that got destroyed while trying to send the request 642 if (!domProcess.canSend) { 643 console.warn( 644 "Content process closed while requesting resources", 645 domProcess.name, 646 domProcess.remoteType 647 ); 648 return; 649 } 650 throw e; 651 }) 652 ); 653 } 654 await Promise.all(promises); 655 656 // Stop execution if we were destroyed in the meantime 657 if (this.isDestroyed()) { 658 return; 659 } 660 661 /* 662 * The Watcher actor doesn't support watching the top level target 663 * (bug 1644397 and possibly some other followup). 664 * 665 * Because of that, we miss reaching these targets in the previous lines of this function. 666 * Since all BrowsingContext target actors register themselves to the TargetActorRegistry, 667 * we use it here in order to reach those missing targets, which are running in the 668 * parent process (where this WatcherActor lives as well): 669 * - the parent process target (which inherits from WindowGlobalTargetActor) 670 * - top level tab target for documents loaded in the parent process (e.g. about:robots). 671 * When the tab loads document in the content process, the FrameTargetHelper will 672 * reach it via the JSWindowActor API. Even if it uses MessageManager for anything 673 * else (RDP packet forwarding, creation and destruction). 674 * 675 * We will eventually get rid of this code once all targets are properly supported by 676 * the Watcher Actor and we have target helpers for all of them. 677 */ 678 const targetActors = this.getTargetActorsInParentProcess(); 679 for (const targetActor of targetActors) { 680 const targetActorResourceTypes = Resources.getResourceTypesForTargetType( 681 resourceTypes, 682 targetActor.targetType 683 ); 684 await targetActor.addOrSetSessionDataEntry( 685 "resources", 686 targetActorResourceTypes, 687 false, 688 "add" 689 ); 690 } 691 } 692 693 /** 694 * Stop watching for a list of resource types. 695 * 696 * @param {Array<string>} resourceTypes 697 * List of all types to listen to. 698 */ 699 unwatchResources(resourceTypes) { 700 // First process resources which are listened from the parent process 701 // (the watcher actor always runs in the parent process) 702 Resources.unwatchResources( 703 this, 704 Resources.getParentProcessResourceTypes(resourceTypes) 705 ); 706 707 // Bail out early if all resources were all watched from parent process. 708 // In this scenario, we do not need to update these resource types in the ParentProcessWatcherRegistry 709 // as targets do not care about them. 710 if (!Resources.hasResourceTypesForTargets(resourceTypes)) { 711 return; 712 } 713 714 const isWatchingResources = ParentProcessWatcherRegistry.unwatchResources( 715 this, 716 resourceTypes 717 ); 718 if (!isWatchingResources) { 719 return; 720 } 721 722 // Prevent trying to unwatch when the related BrowsingContext has already 723 // been destroyed 724 if (!this.isContextDestroyed()) { 725 const domProcesses = ChromeUtils.getAllDOMProcesses(); 726 for (const domProcess of domProcesses) { 727 domProcess.getActor(this._jsActorName).removeSessionDataEntry({ 728 watcherActorID: this.actorID, 729 sessionContext: this.sessionContext, 730 type: "resources", 731 entries: resourceTypes, 732 }); 733 } 734 } 735 736 // See comment in watchResources. 737 const targetActors = this.getTargetActorsInParentProcess(); 738 for (const targetActor of targetActors) { 739 const targetActorResourceTypes = Resources.getResourceTypesForTargetType( 740 resourceTypes, 741 targetActor.targetType 742 ); 743 targetActor.removeSessionDataEntry("resources", targetActorResourceTypes); 744 } 745 } 746 747 clearResources(resourceTypes) { 748 // First process resources which have to be listened from the parent process 749 // (the watcher actor always runs in the parent process) 750 // TODO: content process / worker thread resources are not cleared. See Bug 1774573 751 Resources.clearResources( 752 this, 753 Resources.getParentProcessResourceTypes(resourceTypes) 754 ); 755 } 756 757 /** 758 * Returns the network actor. 759 * 760 * @return {object} actor 761 * The network actor. 762 */ 763 getNetworkParentActor() { 764 if (!this._networkParentActor) { 765 this._networkParentActor = new NetworkParentActor(this); 766 } 767 768 return this._networkParentActor; 769 } 770 771 /** 772 * Returns the blackboxing actor. 773 * 774 * @return {object} actor 775 * The blackboxing actor. 776 */ 777 getBlackboxingActor() { 778 if (!this._blackboxingActor) { 779 this._blackboxingActor = new BlackboxingActor(this); 780 } 781 782 return this._blackboxingActor; 783 } 784 785 /** 786 * Returns the breakpoint list actor. 787 * 788 * @return {object} actor 789 * The breakpoint list actor. 790 */ 791 getBreakpointListActor() { 792 if (!this._breakpointListActor) { 793 this._breakpointListActor = new BreakpointListActor(this); 794 } 795 796 return this._breakpointListActor; 797 } 798 799 /** 800 * Returns the target configuration actor. 801 * 802 * @return {object} actor 803 * The configuration actor. 804 */ 805 getTargetConfigurationActor() { 806 if (!this._targetConfigurationListActor) { 807 this._targetConfigurationListActor = new TargetConfigurationActor(this); 808 } 809 return this._targetConfigurationListActor; 810 } 811 812 /** 813 * Returns the thread configuration actor. 814 * 815 * @return {object} actor 816 * The configuration actor. 817 */ 818 getThreadConfigurationActor() { 819 if (!this._threadConfigurationListActor) { 820 this._threadConfigurationListActor = new ThreadConfigurationActor(this); 821 } 822 return this._threadConfigurationListActor; 823 } 824 825 /** 826 * Server internal API, called by other actors, but not by the client. 827 * Used to agrement some new entries for a given data type (watchers target, resources, 828 * breakpoints,...) 829 * 830 * @param {string} type 831 * Data type to contribute to. 832 * @param {Array<*>} entries 833 * List of values to add or set for this data type. 834 * @param {string} updateType 835 * "add" will only add the new entries in the existing data set. 836 * "set" will update the data set with the new entries. 837 */ 838 async addOrSetDataEntry(type, entries, updateType) { 839 ParentProcessWatcherRegistry.addOrSetSessionDataEntry( 840 this, 841 type, 842 entries, 843 updateType 844 ); 845 846 const promises = []; 847 const domProcesses = ChromeUtils.getAllDOMProcesses(); 848 for (const domProcess of domProcesses) { 849 promises.push( 850 domProcess 851 .getActor(this._jsActorName) 852 .addOrSetSessionDataEntry({ 853 watcherActorID: this.actorID, 854 sessionContext: this.sessionContext, 855 type, 856 entries, 857 updateType, 858 }) 859 .catch(e => { 860 // Ignore any process that got destroyed while trying to send the request 861 if (!domProcess.canSend) { 862 console.warn( 863 "Content process closed while sending session data", 864 domProcess.name, 865 domProcess.remoteType 866 ); 867 return; 868 } 869 throw e; 870 }) 871 ); 872 } 873 await Promise.all(promises); 874 875 // Stop execution if we were destroyed in the meantime 876 if (this.isDestroyed()) { 877 return; 878 } 879 880 // See comment in watchResources 881 const targetActors = this.getTargetActorsInParentProcess(); 882 for (const targetActor of targetActors) { 883 await targetActor.addOrSetSessionDataEntry( 884 type, 885 entries, 886 false, 887 updateType 888 ); 889 } 890 } 891 892 /** 893 * Server internal API, called by other actors, but not by the client. 894 * Used to remve some existing entries for a given data type (watchers target, resources, 895 * breakpoints,...) 896 * 897 * @param {string} type 898 * Data type to modify. 899 * @param {Array<*>} entries 900 * List of values to remove from this data type. 901 */ 902 removeDataEntry(type, entries) { 903 ParentProcessWatcherRegistry.removeSessionDataEntry(this, type, entries); 904 905 const domProcesses = ChromeUtils.getAllDOMProcesses(); 906 for (const domProcess of domProcesses) { 907 domProcess.getActor(this._jsActorName).removeSessionDataEntry({ 908 watcherActorID: this.actorID, 909 sessionContext: this.sessionContext, 910 type, 911 entries, 912 }); 913 } 914 915 // See comment in addOrSetDataEntry 916 const targetActors = this.getTargetActorsInParentProcess(); 917 for (const targetActor of targetActors) { 918 targetActor.removeSessionDataEntry(type, entries); 919 } 920 } 921 922 /** 923 * Retrieve the current watched data for the provided type. 924 * 925 * @param {string} type 926 * Data type to retrieve. 927 */ 928 getSessionDataForType(type) { 929 return this.sessionData?.[type]; 930 } 931 932 /** 933 * Special code dedicated to Service Worker debugging. 934 * This will notify the Service Worker JS Process Actors about the new top level page domain. 935 * So that we start tracking that domain's workers. 936 * 937 * @param {string} newTargetUrl 938 */ 939 async updateDomainSessionDataForServiceWorkers(newTargetUrl) { 940 // If the url could not be parsed the host defaults to an empty string. 941 const host = URL.parse(newTargetUrl)?.host ?? ""; 942 943 ParentProcessWatcherRegistry.addOrSetSessionDataEntry( 944 this, 945 "browser-element-host", 946 [host], 947 "set" 948 ); 949 950 return this.addOrSetDataEntry("browser-element-host", [host], "set"); 951 } 952 };