/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const protocol = require("devtools/shared/protocol"); const { watcherSpec } = require("devtools/shared/specs/watcher"); const Services = require("Services"); const Resources = require("devtools/server/actors/resources/index"); const { TargetActorRegistry, } = require("devtools/server/actors/targets/target-actor-registry.jsm"); const { WatcherRegistry, } = require("devtools/server/actors/watcher/WatcherRegistry.jsm"); const Targets = require("devtools/server/actors/targets/index"); const TARGET_HELPERS = {}; loader.lazyRequireGetter( TARGET_HELPERS, Targets.TYPES.FRAME, "devtools/server/actors/watcher/target-helpers/frame-helper" ); loader.lazyRequireGetter( TARGET_HELPERS, Targets.TYPES.PROCESS, "devtools/server/actors/watcher/target-helpers/process-helper" ); loader.lazyRequireGetter( TARGET_HELPERS, Targets.TYPES.WORKER, "devtools/server/actors/watcher/target-helpers/worker-helper" ); loader.lazyRequireGetter( this, "NetworkParentActor", "devtools/server/actors/network-monitor/network-parent", true ); loader.lazyRequireGetter( this, "BreakpointListActor", "devtools/server/actors/breakpoint-list", true ); exports.WatcherActor = protocol.ActorClassWithSpec(watcherSpec, { /** * Optionally pass a `browser` in the second argument * in order to focus only on targets related to a given element. */ initialize: function(conn, options) { protocol.Actor.prototype.initialize.call(this, conn); this._browser = options && options.browser; this.notifyResourceAvailable = this.notifyResourceAvailable.bind(this); this.notifyResourceDestroyed = this.notifyResourceDestroyed.bind(this); this.notifyResourceUpdated = this.notifyResourceUpdated.bind(this); }, /** * If we are debugging only one Tab or Document, returns its BrowserElement. * For Tabs, it will be the element used to load the web page. * * This is typicaly used to fetch: * - its `browserId` attribute, which uniquely defines it, * - its `browsingContextID` or `browsingContext`, which helps inspecting its content. */ get browserElement() { return this._browser; }, /** * Unique identifier, which helps designates one precise browser element, the one * we may debug. This is only set if we actually debug a browser element. * So, that will be typically set when we debug a tab, but not when we debug * a process, or a worker. */ get browserId() { return this._browser?.browserId; }, destroy: function() { // Force unwatching for all types, even if we weren't watching. // This is fine as unwatchTarget is NOOP if we weren't already watching for this target type. for (const targetType of Object.values(Targets.TYPES)) { this.unwatchTargets(targetType); } this.unwatchResources(Object.values(Resources.TYPES)); WatcherRegistry.unregisterWatcher(this); // Destroy the actor at the end so that its actorID keeps being defined. protocol.Actor.prototype.destroy.call(this); }, /* * Get the list of the currently watched resources for this watcher. * * @return Array * Returns the list of currently watched resource types. */ get watchedData() { return WatcherRegistry.getWatchedData(this); }, form() { const enableServerWatcher = Services.prefs.getBoolPref( "devtools.testing.enableServerWatcherSupport", false ); const hasBrowserElement = !!this.browserElement; return { actor: this.actorID, // The resources and target traits should be removed all at the same time since the // client has generic ways to deal with all of them (See Bug 1680280). traits: { [Targets.TYPES.FRAME]: true, [Targets.TYPES.PROCESS]: true, [Targets.TYPES.WORKER]: hasBrowserElement, resources: { // In Firefox 81 we added support for: // - CONSOLE_MESSAGE // - CSS_CHANGE // - CSS_MESSAGE // - DOCUMENT_EVENT // - ERROR_MESSAGE // - PLATFORM_MESSAGE // // We enabled them for content toolboxes only because we don't support // content process targets yet. Bug 1620248 should help supporting // them and enable this more broadly. // // New server-side resources can be gated behind // `devtools.testing.enableServerWatcherSupport` if needed. [Resources.TYPES.CONSOLE_MESSAGE]: hasBrowserElement, [Resources.TYPES.CSS_CHANGE]: hasBrowserElement, [Resources.TYPES.CSS_MESSAGE]: hasBrowserElement, [Resources.TYPES.DOCUMENT_EVENT]: hasBrowserElement, [Resources.TYPES.ERROR_MESSAGE]: hasBrowserElement, [Resources.TYPES.LOCAL_STORAGE]: hasBrowserElement, [Resources.TYPES.SESSION_STORAGE]: hasBrowserElement, [Resources.TYPES.PLATFORM_MESSAGE]: true, [Resources.TYPES.NETWORK_EVENT]: hasBrowserElement, [Resources.TYPES.NETWORK_EVENT_STACKTRACE]: hasBrowserElement, [Resources.TYPES.STYLESHEET]: enableServerWatcher && hasBrowserElement, [Resources.TYPES.SOURCE]: hasBrowserElement, }, // @backward-compat { version 85 } When removing this trait, consumers using // the TargetList to retrieve the Breakpoints front should still be careful to check // that the Watcher is available "set-breakpoints": true, }, }; }, /** * Start watching for a new target type. * * This will instantiate Target Actors for existing debugging context of this type, * but will also create actors as context of this type get created. * The actors are notified to the client via "target-available-form" RDP events. * We also notify about target actors destruction via "target-destroyed-form". * Note that we are guaranteed to receive all existing target actor by the time this method * resolves. * * @param {string} targetType * Type of context to observe. See Targets.TYPES object. */ async watchTargets(targetType) { WatcherRegistry.watchTargets(this, targetType); const targetHelperModule = TARGET_HELPERS[targetType]; // Await the registration in order to ensure receiving the already existing targets await targetHelperModule.createTargets(this); }, /** * Stop watching for a given target type. * * @param {string} targetType * Type of context to observe. See Targets.TYPES object. */ unwatchTargets(targetType) { const isWatchingTargets = WatcherRegistry.unwatchTargets(this, targetType); if (!isWatchingTargets) { return; } const targetHelperModule = TARGET_HELPERS[targetType]; targetHelperModule.destroyTargets(this); // Unregister the JS Window Actor if there is no more DevTools code observing any target/resource WatcherRegistry.maybeUnregisteringJSWindowActor(); }, /** * Called by a Watcher module, whenever a new target is available */ notifyTargetAvailable(actor) { this.emit("target-available-form", actor); }, /** * Called by a Watcher module, whenever a target has been destroyed */ notifyTargetDestroyed(actor) { this.emit("target-destroyed-form", actor); }, /** * Given a browsingContextID, returns its parent browsingContextID. Returns null if a * parent browsing context couldn't be found. Throws if the browsing context * corresponding to the passed browsingContextID couldn't be found. * * @param {Integer} browsingContextID * @returns {Integer|null} */ getParentBrowsingContextID(browsingContextID) { const browsingContext = BrowsingContext.get(browsingContextID); if (!browsingContext) { throw new Error( `BrowsingContext with ID=${browsingContextID} doesn't exist.` ); } // Top-level documents of tabs, loaded in a element expose a null `parent`. // i.e. Their BrowsingContext has no parent and is considered top level. // But... in the context of the Browser Toolbox, we still consider them as child of the browser window. // So, for them, fallback on `embedderWindowGlobal`, which will typically be the WindowGlobal for browser.xhtml. if (browsingContext.parent) { return browsingContext.parent.id; } if (browsingContext.embedderWindowGlobal) { return browsingContext.embedderWindowGlobal.browsingContext.id; } return null; }, /** * Called by Resource Watchers, when new resources are available. * * @param Array resources * List of all available resources. A resource is a JSON object piped over to the client. * It may contain actor IDs, actor forms, to be manually marshalled by the client. */ notifyResourceAvailable(resources) { this._emitResourcesForm("resource-available-form", resources); }, notifyResourceDestroyed(resources) { this._emitResourcesForm("resource-destroyed-form", resources); }, notifyResourceUpdated(resources) { this._emitResourcesForm("resource-updated-form", resources); }, /** * Wrapper around emit for resource forms. */ _emitResourcesForm(name, resources) { if (resources.length === 0) { // Don't try to emit if the resources array is empty. return; } this.emit(name, resources); }, /** * Try to retrieve a parent process TargetActor: * - either when debugging a parent process page (when browserElement is set to the page's tab), * - or when debugging the main process (when browserElement is null). * * See comment in `watchResources`, this will handle targets which are ignored by Frame and Process * target helpers. (and only those which are ignored) */ _getTargetActorInParentProcess() { return this.browserElement ? // Note: if any, the BrowsingContextTargetActor returned here is created for a parent process // page and lives in the parent process. TargetActorRegistry.getTargetActor(this.browserId) : TargetActorRegistry.getParentProcessTargetActor(); }, /** * Start watching for a list of resource types. * This should only resolve once all "already existing" resources of these types * are notified to the client via resource-available-form event on related target actors. * * @param {Array} resourceTypes * List of all types to listen to. */ async watchResources(resourceTypes) { // First process resources which have to be listened from the parent process // (the watcher actor always runs in the parent process) await Resources.watchResources( this, Resources.getParentProcessResourceTypes(resourceTypes) ); // Bail out early if all resources were watched from parent process. // In this scenario, we do not need to update these resource types in the WatcherRegistry // as targets do not care about them. if (!Resources.hasResourceTypesForTargets(resourceTypes)) { return; } WatcherRegistry.watchResources(this, resourceTypes); // Fetch resources from all existing targets for (const targetType in TARGET_HELPERS) { // We process frame targets even if we aren't watching them, // because frame target helper codepath handles the top level target, if it runs in the *content* process. // It will do another check to `isWatchingTargets(FRAME)` internally. // Note that the workaround at the end of this method, using TargetActorRegistry // is specific to top level target running in the *parent* process. if ( !WatcherRegistry.isWatchingTargets(this, targetType) && targetType != Targets.TYPES.FRAME ) { continue; } const targetResourceTypes = Resources.getResourceTypesForTargetType( resourceTypes, targetType ); if (targetResourceTypes.length == 0) { continue; } const targetHelperModule = TARGET_HELPERS[targetType]; await targetHelperModule.addWatcherDataEntry({ watcher: this, type: "resources", entries: targetResourceTypes, }); } /* * The Watcher actor doesn't support watching the top level target * (bug 1644397 and possibly some other followup). * * Because of that, we miss reaching these targets in the previous lines of this function. * Since all BrowsingContext target actors register themselves to the TargetActorRegistry, * we use it here in order to reach those missing targets, which are running in the * parent process (where this WatcherActor lives as well): * - the parent process target (which inherits from BrowsingContextTargetActor) * - top level tab target for documents loaded in the parent process (e.g. about:robots). * When the tab loads document in the content process, the FrameTargetHelper will * reach it via the JSWindowActor API. Even if it uses MessageManager for anything * else (RDP packet forwarding, creation and destruction). * * We will eventually get rid of this code once all targets are properly supported by * the Watcher Actor and we have target helpers for all of them. */ const frameResourceTypes = Resources.getResourceTypesForTargetType( resourceTypes, Targets.TYPES.FRAME ); if (frameResourceTypes.length > 0) { const targetActor = this._getTargetActorInParentProcess(); if (targetActor) { await targetActor.addWatcherDataEntry("resources", frameResourceTypes); } } }, /** * Stop watching for a list of resource types. * * @param {Array} resourceTypes * List of all types to listen to. */ unwatchResources(resourceTypes) { // First process resources which are listened from the parent process // (the watcher actor always runs in the parent process) Resources.unwatchResources( this, Resources.getParentProcessResourceTypes(resourceTypes) ); // Bail out early if all resources were all watched from parent process. // In this scenario, we do not need to update these resource types in the WatcherRegistry // as targets do not care about them. if (!Resources.hasResourceTypesForTargets(resourceTypes)) { return; } const isWatchingResources = WatcherRegistry.unwatchResources( this, resourceTypes ); if (!isWatchingResources) { return; } // Prevent trying to unwatch when the related BrowsingContext has already // been destroyed if (!this.browserElement || this.browserElement.browsingContext) { for (const targetType in TARGET_HELPERS) { // Frame target helper handles the top level target, if it runs in the content process // so we should always process it. It does a second check to isWatchingTargets. if ( !WatcherRegistry.isWatchingTargets(this, targetType) && targetType != Targets.TYPES.FRAME ) { continue; } const targetResourceTypes = Resources.getResourceTypesForTargetType( resourceTypes, targetType ); if (targetResourceTypes.length == 0) { continue; } const targetHelperModule = TARGET_HELPERS[targetType]; targetHelperModule.removeWatcherDataEntry({ watcher: this, type: "resources", entries: targetResourceTypes, }); } } // See comment in watchResources. const frameResourceTypes = Resources.getResourceTypesForTargetType( resourceTypes, Targets.TYPES.FRAME ); if (frameResourceTypes.length > 0) { const targetActor = this._getTargetActorInParentProcess(); if (targetActor) { targetActor.removeWatcherDataEntry("resources", frameResourceTypes); } } // Unregister the JS Window Actor if there is no more DevTools code observing any target/resource WatcherRegistry.maybeUnregisteringJSWindowActor(); }, /** * Returns the network actor. * * @return {Object} actor * The network actor. */ getNetworkParentActor() { return new NetworkParentActor(this); }, /** * Returns the breakpoint list actor. * * @return {Object} actor * The breakpoint list actor. */ getBreakpointListActor() { return new BreakpointListActor(this); }, /** * Server internal API, called by other actors, but not by the client. * Used to agrement some new entries for a given data type (watchers target, resources, * breakpoints,...) * * @param {String} type * Data type to contribute to. * @param {Array<*>} entries * List of values to add for this data type. */ async addDataEntry(type, entries) { WatcherRegistry.addWatcherDataEntry(this, type, entries); await Promise.all( Object.values(Targets.TYPES) .filter(targetType => WatcherRegistry.isWatchingTargets(this, targetType) ) .map(async targetType => { const targetHelperModule = TARGET_HELPERS[targetType]; await targetHelperModule.addWatcherDataEntry({ watcher: this, type, entries, }); }) ); // See comment in watchResources const targetActor = this._getTargetActorInParentProcess(); if (targetActor) { await targetActor.addWatcherDataEntry(type, entries); } }, /** * Server internal API, called by other actors, but not by the client. * Used to remve some existing entries for a given data type (watchers target, resources, * breakpoints,...) * * @param {String} type * Data type to modify. * @param {Array<*>} entries * List of values to remove from this data type. */ removeDataEntry(type, entries) { WatcherRegistry.removeWatcherDataEntry(this, type, entries); Object.values(Targets.TYPES) .filter(targetType => WatcherRegistry.isWatchingTargets(this, targetType)) .forEach(targetType => { const targetHelperModule = TARGET_HELPERS[targetType]; targetHelperModule.removeWatcherDataEntry({ watcher: this, type, entries, }); }); // See comment in watchResources const targetActor = this._getTargetActorInParentProcess(); if (targetActor) { targetActor.removeWatcherDataEntry(type, entries); } }, });