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/. */
6 const { Actor } = require("resource://devtools/shared/protocol.js");
7 const { watcherSpec } = require("resource://devtools/shared/specs/watcher.js");
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",
13 loadInDevToolsLoader: false,
16 const { WatcherRegistry } = ChromeUtils.importESModule(
17 "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs",
19 // WatcherRegistry needs to be a true singleton and loads ActorManagerParent
20 // which also has to be a true singleton.
21 loadInDevToolsLoader: false,
24 const Targets = require("resource://devtools/server/actors/targets/index.js");
25 const { getAllBrowsingContextsForContext } = ChromeUtils.importESModule(
26 "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs"
29 const TARGET_HELPERS = {};
30 loader.lazyRequireGetter(
33 "resource://devtools/server/actors/watcher/target-helpers/frame-helper.js"
35 loader.lazyRequireGetter(
37 Targets.TYPES.PROCESS,
38 "resource://devtools/server/actors/watcher/target-helpers/process-helper.js"
40 loader.lazyRequireGetter(
43 "resource://devtools/server/actors/watcher/target-helpers/worker-helper.js"
46 loader.lazyRequireGetter(
49 "resource://devtools/server/actors/network-monitor/network-parent.js",
52 loader.lazyRequireGetter(
55 "resource://devtools/server/actors/blackboxing.js",
58 loader.lazyRequireGetter(
60 "BreakpointListActor",
61 "resource://devtools/server/actors/breakpoint-list.js",
64 loader.lazyRequireGetter(
66 "TargetConfigurationActor",
67 "resource://devtools/server/actors/target-configuration.js",
70 loader.lazyRequireGetter(
72 "ThreadConfigurationActor",
73 "resource://devtools/server/actors/thread-configuration.js",
77 exports.WatcherActor = class WatcherActor extends Actor {
79 * Initialize a new WatcherActor which is the main entry point to debug
80 * something. The main features of this actor are to:
81 * - observe targets related to the context we are debugging.
82 * This is done via watchTargets/unwatchTargets methods, and
83 * target-available-form/target-destroyed-form events.
84 * - observe resources related to the observed targets.
85 * This is done via watchResources/unwatchResources methods, and
86 * resource-available-form/resource-updated-form/resource-destroyed-form events.
87 * Note that these events are also emited on both the watcher actor,
88 * for resources observed from the parent process, as well as on the
89 * target actors, when the resources are observed from the target's process or thread.
91 * @param {DevToolsServerConnection} conn
92 * The connection to use in order to communicate back to the client.
93 * @param {object} sessionContext
94 * The Session Context to help know what is debugged.
95 * See devtools/server/actors/watcher/session-context.js
96 * @param {Number} sessionContext.browserId: If this is a "browser-element" context type,
97 * the "browserId" of the <browser> element we would like to debug.
98 * @param {Boolean} sessionContext.isServerTargetSwitchingEnabled: Flag to to know if we should
99 * spawn new top level targets for the debugged context.
101 constructor(conn, sessionContext) {
102 super(conn, watcherSpec);
103 this._sessionContext = sessionContext;
104 if (sessionContext.type == "browser-element") {
105 // Retrieve the <browser> element for the given browser ID
106 const browsingContext = BrowsingContext.getCurrentTopByBrowserId(
107 sessionContext.browserId
109 if (!browsingContext) {
111 "Unable to retrieve the <browser> element for browserId=" +
112 sessionContext.browserId
115 this._browserElement = browsingContext.embedderElement;
118 // Sometimes we get iframe targets before the top-level targets
119 // mostly when doing bfcache navigations, lets cache the early iframes targets and
120 // flush them after the top-level target is available. See Bug 1726568 for details.
121 this._earlyIframeTargets = {};
123 // All currently available WindowGlobal target's form, keyed by `innerWindowId`.
126 // - determine if the iframe targets are early or not.
127 // i.e. if it is notified before its parent target is available.
128 // - notify the destruction of all children targets when a parent is destroyed.
129 // i.e. have a reliable order of destruction between parent and children.
131 // Note that there should be just one top-level window target at a time,
132 // but there are certain cases when a new target is available before the
133 // old target is destroyed.
134 this._currentWindowGlobalTargets = new Map();
137 get sessionContext() {
138 return this._sessionContext;
142 * If we are debugging only one Tab or Document, returns its BrowserElement.
143 * For Tabs, it will be the <browser> element used to load the web page.
145 * This is typicaly used to fetch:
146 * - its `browserId` attribute, which uniquely defines it,
147 * - its `browsingContextID` or `browsingContext`, which helps inspecting its content.
149 get browserElement() {
150 return this._browserElement;
153 getAllBrowsingContexts(options) {
154 return getAllBrowsingContextsForContext(this.sessionContext, options);
158 * Helper to know if the context we are debugging has been already destroyed
160 isContextDestroyed() {
161 if (this.sessionContext.type == "browser-element") {
162 return !this.browserElement.browsingContext;
163 } else if (this.sessionContext.type == "webextension") {
164 return !BrowsingContext.get(this.sessionContext.addonBrowsingContextID);
165 } else if (this.sessionContext.type == "all") {
169 "Unsupported session context type: " + this.sessionContext.type
174 // Force unwatching for all types, even if we weren't watching.
175 // This is fine as unwatchTarget is NOOP if we weren't already watching for this target type.
176 for (const targetType of Object.values(Targets.TYPES)) {
177 this.unwatchTargets(targetType);
179 this.unwatchResources(Object.values(Resources.TYPES));
181 WatcherRegistry.unregisterWatcher(this);
183 // Destroy the actor at the end so that its actorID keeps being defined.
188 * Get the list of the currently watched resources for this watcher.
190 * @return Array<String>
191 * Returns the list of currently watched resource types.
194 return WatcherRegistry.getSessionData(this);
200 // The resources and target traits should be removed all at the same time since the
201 // client has generic ways to deal with all of them (See Bug 1680280).
203 ...this.sessionContext.supportedTargets,
204 resources: this.sessionContext.supportedResources,
210 * Start watching for a new target type.
212 * This will instantiate Target Actors for existing debugging context of this type,
213 * but will also create actors as context of this type get created.
214 * The actors are notified to the client via "target-available-form" RDP events.
215 * We also notify about target actors destruction via "target-destroyed-form".
216 * Note that we are guaranteed to receive all existing target actor by the time this method
219 * @param {string} targetType
220 * Type of context to observe. See Targets.TYPES object.
222 async watchTargets(targetType) {
223 WatcherRegistry.watchTargets(this, targetType);
225 const targetHelperModule = TARGET_HELPERS[targetType];
226 // Await the registration in order to ensure receiving the already existing targets
227 await targetHelperModule.createTargets(this);
231 * Stop watching for a given target type.
233 * @param {string} targetType
234 * Type of context to observe. See Targets.TYPES object.
235 * @param {object} options
236 * @param {boolean} options.isModeSwitching
237 * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
239 unwatchTargets(targetType, options = {}) {
240 const isWatchingTargets = WatcherRegistry.unwatchTargets(
245 if (!isWatchingTargets) {
249 const targetHelperModule = TARGET_HELPERS[targetType];
250 targetHelperModule.destroyTargets(this, options);
252 // Unregister the JS Window Actor if there is no more DevTools code observing any target/resource,
253 // unless we're switching mode (having both condition at the same time should only
255 if (!options.isModeSwitching) {
256 WatcherRegistry.maybeUnregisteringJSWindowActor();
261 * Flush any early iframe targets relating to this top level
263 * @param {number} topInnerWindowID
265 _flushIframeTargets(topInnerWindowID) {
266 while (this._earlyIframeTargets[topInnerWindowID]?.length > 0) {
267 const actor = this._earlyIframeTargets[topInnerWindowID].shift();
268 this.emit("target-available-form", actor);
273 * Called by a Watcher module, whenever a new target is available
275 notifyTargetAvailable(actor) {
276 // Emit immediately for worker, process & extension targets
277 // as they don't have a parent browsing context.
278 if (!actor.traits?.isBrowsingContext) {
279 this.emit("target-available-form", actor);
283 // If isBrowsingContext trait is true, we are processing a WindowGlobalTarget.
284 // (this trait should be renamed)
285 this._currentWindowGlobalTargets.set(actor.innerWindowId, actor);
287 // The top-level is always the same for the browser-toolbox
288 if (this.sessionContext.type == "all") {
289 this.emit("target-available-form", actor);
293 if (actor.isTopLevelTarget) {
294 this.emit("target-available-form", actor);
295 // Flush any existing early iframe targets
296 this._flushIframeTargets(actor.innerWindowId);
297 } else if (this._currentWindowGlobalTargets.has(actor.topInnerWindowId)) {
298 // Emit the event immediately if the top-level target is already available
299 this.emit("target-available-form", actor);
300 } else if (this._earlyIframeTargets[actor.topInnerWindowId]) {
301 // Add the early iframe target to the list of other early targets.
302 this._earlyIframeTargets[actor.topInnerWindowId].push(actor);
304 // Set the first early iframe target
305 this._earlyIframeTargets[actor.topInnerWindowId] = [actor];
310 * Called by a Watcher module, whenever a target has been destroyed
312 * @param {object} actor
313 * the actor form of the target being destroyed
314 * @param {object} options
315 * @param {boolean} options.isModeSwitching
316 * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
318 async notifyTargetDestroyed(actor, options = {}) {
319 // Emit immediately for worker, process & extension targets
320 // as they don't have a parent browsing context.
321 if (!actor.innerWindowId) {
322 this.emit("target-destroyed-form", actor, options);
325 // Flush all iframe targets if we are destroying a top level target.
326 if (actor.isTopLevelTarget) {
327 // First compute the list of children actors, as notifyTargetDestroy will mutate _currentWindowGlobalTargets
328 const childrenActors = [
329 ...this._currentWindowGlobalTargets.values(),
332 form.topInnerWindowId == actor.innerWindowId &&
333 // Ignore the top level target itself, because its topInnerWindowId will be its innerWindowId
334 form.innerWindowId != actor.innerWindowId
336 childrenActors.map(form => this.notifyTargetDestroyed(form, options));
338 if (this._earlyIframeTargets[actor.innerWindowId]) {
339 delete this._earlyIframeTargets[actor.innerWindowId];
341 this._currentWindowGlobalTargets.delete(actor.innerWindowId);
342 const documentEventWatcher = Resources.getResourceWatcher(
344 Resources.TYPES.DOCUMENT_EVENT
346 // If we have a Watcher class instantiated, ensure that target-destroyed is sent
347 // *after* DOCUMENT_EVENT's will-navigate. Otherwise this resource will have an undefined
348 // `targetFront` attribute, as it is associated with the target from which we navigate
349 // and not the one we navigate to.
351 // About documentEventWatcher check: We won't have any watcher class if we aren't
352 // using server side Watcher classes.
353 // i.e. when we are using the legacy listener for DOCUMENT_EVENT.
354 // This is still the case for all toolboxes but the one for local and remote tabs.
356 // About isServerTargetSwitchingEnabled check: if we are using the watcher class
357 // we may still use client side target, which will still use legacy listeners for
358 // will-navigate and so will-navigate will be emitted by the target actor itself.
360 // About isTopLevelTarget check: only top level targets emit will-navigate,
361 // so there is no reason to delay target-destroy for remote iframes.
363 documentEventWatcher &&
364 this.sessionContext.isServerTargetSwitchingEnabled &&
365 actor.isTopLevelTarget
367 await documentEventWatcher.onceWillNavigateIsEmitted(actor.innerWindowId);
369 this.emit("target-destroyed-form", actor, options);
373 * Given a browsingContextID, returns its parent browsingContextID. Returns null if a
374 * parent browsing context couldn't be found. Throws if the browsing context
375 * corresponding to the passed browsingContextID couldn't be found.
377 * @param {Integer} browsingContextID
378 * @returns {Integer|null}
380 getParentBrowsingContextID(browsingContextID) {
381 const browsingContext = BrowsingContext.get(browsingContextID);
382 if (!browsingContext) {
384 `BrowsingContext with ID=${browsingContextID} doesn't exist.`
387 // Top-level documents of tabs, loaded in a <browser> element expose a null `parent`.
388 // i.e. Their BrowsingContext has no parent and is considered top level.
389 // But... in the context of the Browser Toolbox, we still consider them as child of the browser window.
390 // So, for them, fallback on `embedderWindowGlobal`, which will typically be the WindowGlobal for browser.xhtml.
391 if (browsingContext.parent) {
392 return browsingContext.parent.id;
394 if (browsingContext.embedderWindowGlobal) {
395 return browsingContext.embedderWindowGlobal.browsingContext.id;
401 * Called by Resource Watchers, when new resources are available, updated or destroyed.
403 * @param String updateType
404 * Can be "available", "updated" or "destroyed"
405 * @param Array<json> resources
406 * List of all resource's form. A resource is a JSON object piped over to the client.
407 * It can contain actor IDs, actor forms, to be manually marshalled by the client.
409 notifyResources(updateType, resources) {
410 if (resources.length === 0) {
411 // Don't try to emit if the resources array is empty.
415 if (this.sessionContext.type == "webextension") {
416 this._overrideResourceBrowsingContextForWebExtension(resources);
419 this.emit(`resource-${updateType}-form`, resources);
423 * For WebExtension, we have to hack all resource's browsingContextID
424 * in order to ensure emitting them with the fixed, original browsingContextID
425 * related to the fallback document created by devtools which always exists.
426 * The target's form will always be relating to that BrowsingContext IDs (browsing context ID and inner window id).
427 * Even if the target switches internally to another document via WindowGlobalTargetActor._setWindow.
429 * @param {Array<Objects>} List of resources
431 _overrideResourceBrowsingContextForWebExtension(resources) {
432 resources.forEach(resource => {
433 resource.browsingContextID = this.sessionContext.addonBrowsingContextID;
438 * Try to retrieve a parent process TargetActor which is ignored by the
439 * TARGET_HELPERS. Examples:
440 * - top level target for the browser toolbox
441 * - xpcshell target for xpcshell debugging
443 * See comment in `watchResources`.
445 * @return {TargetActor|null} Matching target actor if any, null otherwise.
447 getTargetActorInParentProcess() {
448 if (TargetActorRegistry.xpcShellTargetActor) {
449 return TargetActorRegistry.xpcShellTargetActor;
452 // Note: For browser-element debugging, the WindowGlobalTargetActor returned here is created
453 // for a parent process page and lives in the parent process.
454 const actors = TargetActorRegistry.getTargetActors(
459 switch (this.sessionContext.type) {
461 return actors.find(actor => actor.typeName === "parentProcessTarget");
462 case "browser-element":
464 // All target actors for browser-element and webextension sessions
465 // should be created using the JS Window actors.
469 "Unsupported session context type: " + this.sessionContext.type
475 * Start watching for a list of resource types.
476 * This should only resolve once all "already existing" resources of these types
477 * are notified to the client via resource-available-form event on related target actors.
479 * @param {Array<string>} resourceTypes
480 * List of all types to listen to.
482 async watchResources(resourceTypes) {
483 // First process resources which have to be listened from the parent process
484 // (the watcher actor always runs in the parent process)
485 await Resources.watchResources(
487 Resources.getParentProcessResourceTypes(resourceTypes)
490 // Bail out early if all resources were watched from parent process.
491 // In this scenario, we do not need to update these resource types in the WatcherRegistry
492 // as targets do not care about them.
493 if (!Resources.hasResourceTypesForTargets(resourceTypes)) {
497 WatcherRegistry.watchResources(this, resourceTypes);
499 // Fetch resources from all existing targets
500 for (const targetType in TARGET_HELPERS) {
501 // We process frame targets even if we aren't watching them,
502 // because frame target helper codepath handles the top level target, if it runs in the *content* process.
503 // It will do another check to `isWatchingTargets(FRAME)` internally.
504 // Note that the workaround at the end of this method, using TargetActorRegistry
505 // is specific to top level target running in the *parent* process.
507 !WatcherRegistry.isWatchingTargets(this, targetType) &&
508 targetType != Targets.TYPES.FRAME
512 const targetResourceTypes = Resources.getResourceTypesForTargetType(
516 if (!targetResourceTypes.length) {
519 const targetHelperModule = TARGET_HELPERS[targetType];
520 await targetHelperModule.addOrSetSessionDataEntry({
523 entries: targetResourceTypes,
529 * The Watcher actor doesn't support watching the top level target
530 * (bug 1644397 and possibly some other followup).
532 * Because of that, we miss reaching these targets in the previous lines of this function.
533 * Since all BrowsingContext target actors register themselves to the TargetActorRegistry,
534 * we use it here in order to reach those missing targets, which are running in the
535 * parent process (where this WatcherActor lives as well):
536 * - the parent process target (which inherits from WindowGlobalTargetActor)
537 * - top level tab target for documents loaded in the parent process (e.g. about:robots).
538 * When the tab loads document in the content process, the FrameTargetHelper will
539 * reach it via the JSWindowActor API. Even if it uses MessageManager for anything
540 * else (RDP packet forwarding, creation and destruction).
542 * We will eventually get rid of this code once all targets are properly supported by
543 * the Watcher Actor and we have target helpers for all of them.
545 const targetActor = this.getTargetActorInParentProcess();
547 const targetActorResourceTypes = Resources.getResourceTypesForTargetType(
549 targetActor.targetType
551 await targetActor.addOrSetSessionDataEntry(
553 targetActorResourceTypes,
561 * Stop watching for a list of resource types.
563 * @param {Array<string>} resourceTypes
564 * List of all types to listen to.
566 unwatchResources(resourceTypes) {
567 // First process resources which are listened from the parent process
568 // (the watcher actor always runs in the parent process)
569 Resources.unwatchResources(
571 Resources.getParentProcessResourceTypes(resourceTypes)
574 // Bail out early if all resources were all watched from parent process.
575 // In this scenario, we do not need to update these resource types in the WatcherRegistry
576 // as targets do not care about them.
577 if (!Resources.hasResourceTypesForTargets(resourceTypes)) {
581 const isWatchingResources = WatcherRegistry.unwatchResources(
585 if (!isWatchingResources) {
589 // Prevent trying to unwatch when the related BrowsingContext has already
591 if (!this.isContextDestroyed()) {
592 for (const targetType in TARGET_HELPERS) {
593 // Frame target helper handles the top level target, if it runs in the content process
594 // so we should always process it. It does a second check to isWatchingTargets.
596 !WatcherRegistry.isWatchingTargets(this, targetType) &&
597 targetType != Targets.TYPES.FRAME
601 const targetResourceTypes = Resources.getResourceTypesForTargetType(
605 if (!targetResourceTypes.length) {
608 const targetHelperModule = TARGET_HELPERS[targetType];
609 targetHelperModule.removeSessionDataEntry({
612 entries: targetResourceTypes,
617 // See comment in watchResources.
618 const targetActor = this.getTargetActorInParentProcess();
620 const targetActorResourceTypes = Resources.getResourceTypesForTargetType(
622 targetActor.targetType
624 targetActor.removeSessionDataEntry("resources", targetActorResourceTypes);
627 // Unregister the JS Window Actor if there is no more DevTools code observing any target/resource
628 WatcherRegistry.maybeUnregisteringJSWindowActor();
631 clearResources(resourceTypes) {
632 // First process resources which have to be listened from the parent process
633 // (the watcher actor always runs in the parent process)
634 // TODO: content process / worker thread resources are not cleared. See Bug 1774573
635 Resources.clearResources(
637 Resources.getParentProcessResourceTypes(resourceTypes)
642 * Returns the network actor.
644 * @return {Object} actor
647 getNetworkParentActor() {
648 if (!this._networkParentActor) {
649 this._networkParentActor = new NetworkParentActor(this);
652 return this._networkParentActor;
656 * Returns the blackboxing actor.
658 * @return {Object} actor
659 * The blackboxing actor.
661 getBlackboxingActor() {
662 if (!this._blackboxingActor) {
663 this._blackboxingActor = new BlackboxingActor(this);
666 return this._blackboxingActor;
670 * Returns the breakpoint list actor.
672 * @return {Object} actor
673 * The breakpoint list actor.
675 getBreakpointListActor() {
676 if (!this._breakpointListActor) {
677 this._breakpointListActor = new BreakpointListActor(this);
680 return this._breakpointListActor;
684 * Returns the target configuration actor.
686 * @return {Object} actor
687 * The configuration actor.
689 getTargetConfigurationActor() {
690 if (!this._targetConfigurationListActor) {
691 this._targetConfigurationListActor = new TargetConfigurationActor(this);
693 return this._targetConfigurationListActor;
697 * Returns the thread configuration actor.
699 * @return {Object} actor
700 * The configuration actor.
702 getThreadConfigurationActor() {
703 if (!this._threadConfigurationListActor) {
704 this._threadConfigurationListActor = new ThreadConfigurationActor(this);
706 return this._threadConfigurationListActor;
710 * Server internal API, called by other actors, but not by the client.
711 * Used to agrement some new entries for a given data type (watchers target, resources,
714 * @param {String} type
715 * Data type to contribute to.
716 * @param {Array<*>} entries
717 * List of values to add or set for this data type.
718 * @param {String} updateType
719 * "add" will only add the new entries in the existing data set.
720 * "set" will update the data set with the new entries.
722 async addOrSetDataEntry(type, entries, updateType) {
723 WatcherRegistry.addOrSetSessionDataEntry(this, type, entries, updateType);
726 Object.values(Targets.TYPES)
729 // We process frame targets even if we aren't watching them,
730 // because frame target helper codepath handles the top level target, if it runs in the *content* process.
731 // It will do another check to `isWatchingTargets(FRAME)` internally.
732 // Note that the workaround at the end of this method, using TargetActorRegistry
733 // is specific to top level target running in the *parent* process.
734 WatcherRegistry.isWatchingTargets(this, targetType) ||
735 targetType === Targets.TYPES.FRAME
737 .map(async targetType => {
738 const targetHelperModule = TARGET_HELPERS[targetType];
739 await targetHelperModule.addOrSetSessionDataEntry({
748 // See comment in watchResources
749 const targetActor = this.getTargetActorInParentProcess();
751 await targetActor.addOrSetSessionDataEntry(
761 * Server internal API, called by other actors, but not by the client.
762 * Used to remve some existing entries for a given data type (watchers target, resources,
765 * @param {String} type
766 * Data type to modify.
767 * @param {Array<*>} entries
768 * List of values to remove from this data type.
770 removeDataEntry(type, entries) {
771 WatcherRegistry.removeSessionDataEntry(this, type, entries);
773 Object.values(Targets.TYPES)
776 // See comment in addOrSetDataEntry
777 WatcherRegistry.isWatchingTargets(this, targetType) ||
778 targetType === Targets.TYPES.FRAME
780 .forEach(targetType => {
781 const targetHelperModule = TARGET_HELPERS[targetType];
782 targetHelperModule.removeSessionDataEntry({
789 // See comment in addOrSetDataEntry
790 const targetActor = this.getTargetActorInParentProcess();
792 targetActor.removeSessionDataEntry(type, entries);
797 * Retrieve the current watched data for the provided type.
799 * @param {String} type
800 * Data type to retrieve.
802 getSessionDataForType(type) {
803 return this.sessionData?.[type];