summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/watcher.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/watcher.js')
-rw-r--r--devtools/server/actors/watcher.js530
1 files changed, 530 insertions, 0 deletions
diff --git a/devtools/server/actors/watcher.js b/devtools/server/actors/watcher.js
new file mode 100644
index 0000000000..9ddc365c4d
--- /dev/null
+++ b/devtools/server/actors/watcher.js
@@ -0,0 +1,530 @@
+/* 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 <browser> 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 <browser> 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<String>
+ * 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 <browser> 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<json> 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<string>} 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<string>} 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);
+ }
+ },
+});