/* 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 Targets = require("resource://devtools/server/actors/targets/index.js"); const TYPES = { CONSOLE_MESSAGE: "console-message", CSS_CHANGE: "css-change", CSS_MESSAGE: "css-message", CSS_REGISTERED_PROPERTIES: "css-registered-properties", DOCUMENT_EVENT: "document-event", ERROR_MESSAGE: "error-message", LAST_PRIVATE_CONTEXT_EXIT: "last-private-context-exit", NETWORK_EVENT: "network-event", NETWORK_EVENT_STACKTRACE: "network-event-stacktrace", PLATFORM_MESSAGE: "platform-message", REFLOW: "reflow", SERVER_SENT_EVENT: "server-sent-event", SOURCE: "source", STYLESHEET: "stylesheet", THREAD_STATE: "thread-state", JSTRACER_TRACE: "jstracer-trace", JSTRACER_STATE: "jstracer-state", WEBSOCKET: "websocket", // storage types CACHE_STORAGE: "Cache", COOKIE: "cookies", EXTENSION_STORAGE: "extension-storage", INDEXED_DB: "indexed-db", LOCAL_STORAGE: "local-storage", SESSION_STORAGE: "session-storage", // root types EXTENSIONS_BGSCRIPT_STATUS: "extensions-backgroundscript-status", }; exports.TYPES = TYPES; // Helper dictionaries, which will contain data specific to each resource type. // - `path` is the absolute path to the module defining the Resource Watcher class. // // Also see the attributes added by `augmentResourceDictionary` for each type: // - `watchers` is a weak map which will store Resource Watchers // (i.e. devtools/server/actors/resources/ class instances) // keyed by target actor -or- watcher actor. // - `WatcherClass` is a shortcut to the Resource Watcher module. // Each module exports a Resource Watcher class. // // These are several dictionaries, which depend how the resource watcher classes are instantiated. // Frame target resources are spawned via a BrowsingContext Target Actor. // Their watcher class receives a target actor as first argument. // They are instantiated for each observed BrowsingContext, from the content process where it runs. // They are meant to observe all resources related to a given Browsing Context. const FrameTargetResources = augmentResourceDictionary({ [TYPES.CACHE_STORAGE]: { path: "devtools/server/actors/resources/storage-cache", }, [TYPES.CONSOLE_MESSAGE]: { path: "devtools/server/actors/resources/console-messages", }, [TYPES.CSS_CHANGE]: { path: "devtools/server/actors/resources/css-changes", }, [TYPES.CSS_MESSAGE]: { path: "devtools/server/actors/resources/css-messages", }, [TYPES.CSS_REGISTERED_PROPERTIES]: { path: "devtools/server/actors/resources/css-registered-properties", }, [TYPES.DOCUMENT_EVENT]: { path: "devtools/server/actors/resources/document-event", }, [TYPES.ERROR_MESSAGE]: { path: "devtools/server/actors/resources/error-messages", }, [TYPES.JSTRACER_STATE]: { path: "devtools/server/actors/resources/jstracer-state", }, [TYPES.JSTRACER_TRACE]: { path: "devtools/server/actors/resources/jstracer-trace", }, [TYPES.LOCAL_STORAGE]: { path: "devtools/server/actors/resources/storage-local-storage", }, [TYPES.PLATFORM_MESSAGE]: { path: "devtools/server/actors/resources/platform-messages", }, [TYPES.SESSION_STORAGE]: { path: "devtools/server/actors/resources/storage-session-storage", }, [TYPES.STYLESHEET]: { path: "devtools/server/actors/resources/stylesheets", }, [TYPES.NETWORK_EVENT]: { path: "devtools/server/actors/resources/network-events-content", }, [TYPES.NETWORK_EVENT_STACKTRACE]: { path: "devtools/server/actors/resources/network-events-stacktraces", }, [TYPES.REFLOW]: { path: "devtools/server/actors/resources/reflow", }, [TYPES.SOURCE]: { path: "devtools/server/actors/resources/sources", }, [TYPES.THREAD_STATE]: { path: "devtools/server/actors/resources/thread-states", }, [TYPES.SERVER_SENT_EVENT]: { path: "devtools/server/actors/resources/server-sent-events", }, [TYPES.WEBSOCKET]: { path: "devtools/server/actors/resources/websockets", }, }); // Process target resources are spawned via a Process Target Actor. // Their watcher class receives a process target actor as first argument. // They are instantiated for each observed Process (parent and all content processes). // They are meant to observe all resources related to a given process. const ProcessTargetResources = augmentResourceDictionary({ [TYPES.CONSOLE_MESSAGE]: { path: "devtools/server/actors/resources/console-messages", }, [TYPES.JSTRACER_TRACE]: { path: "devtools/server/actors/resources/jstracer-trace", }, [TYPES.JSTRACER_STATE]: { path: "devtools/server/actors/resources/jstracer-state", }, [TYPES.ERROR_MESSAGE]: { path: "devtools/server/actors/resources/error-messages", }, [TYPES.PLATFORM_MESSAGE]: { path: "devtools/server/actors/resources/platform-messages", }, [TYPES.SOURCE]: { path: "devtools/server/actors/resources/sources", }, [TYPES.THREAD_STATE]: { path: "devtools/server/actors/resources/thread-states", }, }); // Worker target resources are spawned via a Worker Target Actor. // Their watcher class receives a worker target actor as first argument. // They are instantiated for each observed worker, from the worker thread. // They are meant to observe all resources related to a given worker. // // We'll only support a few resource types in Workers (console-message, source, // thread state, …) as error and platform messages are not supported since we need access // to Ci, which isn't available in worker context. // Errors are emitted from the content process main thread so the user would still get them. const WorkerTargetResources = augmentResourceDictionary({ [TYPES.CONSOLE_MESSAGE]: { path: "devtools/server/actors/resources/console-messages", }, [TYPES.JSTRACER_TRACE]: { path: "devtools/server/actors/resources/jstracer-trace", }, [TYPES.JSTRACER_STATE]: { path: "devtools/server/actors/resources/jstracer-state", }, [TYPES.SOURCE]: { path: "devtools/server/actors/resources/sources", }, [TYPES.THREAD_STATE]: { path: "devtools/server/actors/resources/thread-states", }, }); // Parent process resources are spawned via the Watcher Actor. // Their watcher class receives the watcher actor as first argument. // They are instantiated once per watcher from the parent process. // They are meant to observe all resources related to a given context designated by the Watcher (and its sessionContext) // they should be observed from the parent process. const ParentProcessResources = augmentResourceDictionary({ [TYPES.NETWORK_EVENT]: { path: "devtools/server/actors/resources/network-events", }, [TYPES.COOKIE]: { path: "devtools/server/actors/resources/storage-cookie", }, [TYPES.EXTENSION_STORAGE]: { path: "devtools/server/actors/resources/storage-extension", }, [TYPES.INDEXED_DB]: { path: "devtools/server/actors/resources/storage-indexed-db", }, [TYPES.DOCUMENT_EVENT]: { path: "devtools/server/actors/resources/parent-process-document-event", }, [TYPES.LAST_PRIVATE_CONTEXT_EXIT]: { path: "devtools/server/actors/resources/last-private-context-exit", }, }); // Root resources are spawned via the Root Actor. // Their watcher class receives the root actor as first argument. // They are instantiated only once from the parent process. // They are meant to observe anything easily observable from the parent process // that isn't related to any particular context/target. // This is especially useful when you need to observe something without having to instantiate a Watcher actor. const RootResources = augmentResourceDictionary({ [TYPES.EXTENSIONS_BGSCRIPT_STATUS]: { path: "devtools/server/actors/resources/extensions-backgroundscript-status", }, }); exports.RootResources = RootResources; function augmentResourceDictionary(dict) { for (const resource of Object.values(dict)) { resource.watchers = new WeakMap(); loader.lazyRequireGetter(resource, "WatcherClass", resource.path); } return dict; } /** * For a given actor, return the related dictionary defined just before, * that contains info about how to listen for a given resource type, from a given actor. * * @param Actor rootOrWatcherOrTargetActor * Either a RootActor or WatcherActor or a TargetActor which can be listening to a resource. */ function getResourceTypeDictionary(rootOrWatcherOrTargetActor) { const { typeName } = rootOrWatcherOrTargetActor; if (typeName == "root") { return RootResources; } if (typeName == "watcher") { return ParentProcessResources; } const { targetType } = rootOrWatcherOrTargetActor; return getResourceTypeDictionaryForTargetType(targetType); } /** * For a targetType, return the related dictionary. * * @param String targetType * A targetType string (See Targets.TYPES) */ function getResourceTypeDictionaryForTargetType(targetType) { switch (targetType) { case Targets.TYPES.FRAME: return FrameTargetResources; case Targets.TYPES.PROCESS: return ProcessTargetResources; case Targets.TYPES.WORKER: return WorkerTargetResources; case Targets.TYPES.SERVICE_WORKER: return WorkerTargetResources; default: throw new Error(`Unsupported target actor typeName '${targetType}'`); } } /** * For a given actor, return the object stored in one of the previous dictionary * that contains info about how to listen for a given resource type, from a given actor. * * @param Actor rootOrWatcherOrTargetActor * Either a RootActor or WatcherActor or a TargetActor which can be listening to a resource. * @param String resourceType * The resource type to be observed. */ function getResourceTypeEntry(rootOrWatcherOrTargetActor, resourceType) { const dict = getResourceTypeDictionary(rootOrWatcherOrTargetActor); if (!(resourceType in dict)) { throw new Error( `Unsupported resource type '${resourceType}' for ${rootOrWatcherOrTargetActor.typeName}` ); } return dict[resourceType]; } /** * Start watching for a new list of resource types. * This will also emit all already existing resources before resolving. * * @param Actor rootOrWatcherOrTargetActor * Either a RootActor or WatcherActor or a TargetActor which can be listening to a resource: * * RootActor will be used for resources observed from the parent process and aren't related to any particular * context/descriptor. They can be observed right away when connecting to the RDP server * without instantiating any actor other than the root actor. * * WatcherActor will be used for resources listened from the parent process. * * TargetActor will be used for resources listened from the content process. * This actor: * - defines what context to observe (browsing context, process, worker, ...) * Via browsingContextID, windows, docShells attributes for the target actor. * Via the `sessionContext` object for the watcher actor. * (only for Watcher and Target actors. Root actor is context-less.) * - exposes `notifyResources` method to be notified about all the resources updates * This method will receive two arguments: * - {String} updateType, which can be "available", "updated", or "destroyed" * - {Array} resources, which will be the list of resource's forms * or special update object for "updated" scenario. * @param Array resourceTypes * List of all type of resource to listen to. */ async function watchResources(rootOrWatcherOrTargetActor, resourceTypes) { // If we are given a target actor, filter out the resource types supported by the target. // When using sharedData to pass types between processes, we are passing them for all target types. const { targetType } = rootOrWatcherOrTargetActor; // Only target actors usecase will have a target type. // For Root and Watcher we process the `resourceTypes` list unfiltered. if (targetType) { resourceTypes = getResourceTypesForTargetType(resourceTypes, targetType); } const promises = []; for (const resourceType of resourceTypes) { const { watchers, WatcherClass } = getResourceTypeEntry( rootOrWatcherOrTargetActor, resourceType ); // Ignore resources we're already listening to if (watchers.has(rootOrWatcherOrTargetActor)) { continue; } // Don't watch for console messages from the worker target if worker messages are still // being cloned to the main process, otherwise we'll get duplicated messages in the // console output (See Bug 1778852). if ( resourceType == TYPES.CONSOLE_MESSAGE && rootOrWatcherOrTargetActor.workerConsoleApiMessagesDispatchedToMainThread ) { continue; } const watcher = new WatcherClass(); promises.push( watcher.watch(rootOrWatcherOrTargetActor, { onAvailable: rootOrWatcherOrTargetActor.notifyResources.bind( rootOrWatcherOrTargetActor, "available" ), onUpdated: rootOrWatcherOrTargetActor.notifyResources.bind( rootOrWatcherOrTargetActor, "updated" ), onDestroyed: rootOrWatcherOrTargetActor.notifyResources.bind( rootOrWatcherOrTargetActor, "destroyed" ), }) ); watchers.set(rootOrWatcherOrTargetActor, watcher); } await Promise.all(promises); } exports.watchResources = watchResources; function getParentProcessResourceTypes(resourceTypes) { return resourceTypes.filter(resourceType => { return resourceType in ParentProcessResources; }); } exports.getParentProcessResourceTypes = getParentProcessResourceTypes; function getResourceTypesForTargetType(resourceTypes, targetType) { const resourceDictionnary = getResourceTypeDictionaryForTargetType(targetType); return resourceTypes.filter(resourceType => { return resourceType in resourceDictionnary; }); } exports.getResourceTypesForTargetType = getResourceTypesForTargetType; function hasResourceTypesForTargets(resourceTypes) { return resourceTypes.some(resourceType => { return resourceType in FrameTargetResources; }); } exports.hasResourceTypesForTargets = hasResourceTypesForTargets; /** * Stop watching for a list of resource types. * * @param Actor rootOrWatcherOrTargetActor * The related actor, already passed to watchResources. * @param Array resourceTypes * List of all type of resource to stop listening to. */ function unwatchResources(rootOrWatcherOrTargetActor, resourceTypes) { for (const resourceType of resourceTypes) { // Pull all info about this resource type from `Resources` global object const { watchers } = getResourceTypeEntry( rootOrWatcherOrTargetActor, resourceType ); const watcher = watchers.get(rootOrWatcherOrTargetActor); if (watcher) { watcher.destroy(); watchers.delete(rootOrWatcherOrTargetActor); } } } exports.unwatchResources = unwatchResources; /** * Clear resources for a list of resource types. * * @param Actor rootOrWatcherOrTargetActor * The related actor, already passed to watchResources. * @param Array resourceTypes * List of all type of resource to clear. */ function clearResources(rootOrWatcherOrTargetActor, resourceTypes) { for (const resourceType of resourceTypes) { const { watchers } = getResourceTypeEntry( rootOrWatcherOrTargetActor, resourceType ); const watcher = watchers.get(rootOrWatcherOrTargetActor); if (watcher && typeof watcher.clear == "function") { watcher.clear(); } } } exports.clearResources = clearResources; /** * Stop watching for all watched resources on a given actor. * * @param Actor rootOrWatcherOrTargetActor * The related actor, already passed to watchResources. */ function unwatchAllResources(rootOrWatcherOrTargetActor) { for (const { watchers } of Object.values( getResourceTypeDictionary(rootOrWatcherOrTargetActor) )) { const watcher = watchers.get(rootOrWatcherOrTargetActor); if (watcher) { watcher.destroy(); watchers.delete(rootOrWatcherOrTargetActor); } } } exports.unwatchAllResources = unwatchAllResources; /** * If we are watching for the given resource type, * return the current ResourceWatcher instance used by this target actor * in order to observe this resource type. * * @param Actor watcherOrTargetActor * Either a WatcherActor or a TargetActor which can be listening to a resource. * WatcherActor will be used for resources listened from the parent process, * and TargetActor will be used for resources listened from the content process. * @param String resourceType * The resource type to query * @return ResourceWatcher * The resource watcher instance, defined in devtools/server/actors/resources/ */ function getResourceWatcher(watcherOrTargetActor, resourceType) { const { watchers } = getResourceTypeEntry(watcherOrTargetActor, resourceType); return watchers.get(watcherOrTargetActor); } exports.getResourceWatcher = getResourceWatcher;