summaryrefslogtreecommitdiffstats
path: root/remote/shared/messagehandler/MessageHandler.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'remote/shared/messagehandler/MessageHandler.sys.mjs')
-rw-r--r--remote/shared/messagehandler/MessageHandler.sys.mjs355
1 files changed, 355 insertions, 0 deletions
diff --git a/remote/shared/messagehandler/MessageHandler.sys.mjs b/remote/shared/messagehandler/MessageHandler.sys.mjs
new file mode 100644
index 0000000000..18ec6b820c
--- /dev/null
+++ b/remote/shared/messagehandler/MessageHandler.sys.mjs
@@ -0,0 +1,355 @@
+/* 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/. */
+
+import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ error: "chrome://remote/content/shared/messagehandler/Errors.sys.mjs",
+ EventsDispatcher:
+ "chrome://remote/content/shared/messagehandler/EventsDispatcher.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ ModuleCache:
+ "chrome://remote/content/shared/messagehandler/ModuleCache.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
+
+/**
+ * A ContextDescriptor object provides information to decide if a broadcast or
+ * a session data item should be applied to a specific MessageHandler context.
+ *
+ * @typedef {object} ContextDescriptor
+ * @property {ContextDescriptorType} type
+ * The type of context
+ * @property {string=} id
+ * Unique id of a given context for the provided type.
+ * For ContextDescriptorType.All, id can be ommitted.
+ * For ContextDescriptorType.TopBrowsingContext, the id should be the
+ * browserId corresponding to a top-level browsing context.
+ */
+
+/**
+ * Enum of ContextDescriptor types.
+ *
+ * @enum {string}
+ */
+export const ContextDescriptorType = {
+ All: "All",
+ TopBrowsingContext: "TopBrowsingContext",
+};
+
+/**
+ * A ContextInfo identifies a given context that can be linked to a MessageHandler
+ * instance. It should be used to identify events coming from this context.
+ *
+ * It can either be provided by the MessageHandler itself, when the event is
+ * emitted from the context it relates to.
+ *
+ * Or it can be assembled manually, for instance when emitting an event which
+ * relates to a window global from the root layer (eg browsingContext.contextCreated).
+ *
+ * @typedef {object} ContextInfo
+ * @property {string} contextId
+ * Unique id of the MessageHandler corresponding to this context.
+ * @property {string} type
+ * One of MessageHandler.type.
+ */
+
+/**
+ * MessageHandler instances are dedicated to handle both Commands and Events
+ * to enable automation and introspection for remote control protocols.
+ *
+ * MessageHandler instances are designed to form a network, where each instance
+ * should allow to inspect a specific context (eg. a BrowsingContext, a Worker,
+ * etc). Those instances might live in different processes and threads but
+ * should be linked together by the usage of a single sessionId, shared by all
+ * the instances of a single MessageHandler network.
+ *
+ * MessageHandler instances will be dynamically spawned depending on which
+ * Command or which Event needs to be processed and should therefore not be
+ * explicitly created by consumers, nor used directly.
+ *
+ * The only exception is the ROOT MessageHandler. This MessageHandler will be
+ * the entry point to send commands to the rest of the network. It will also
+ * emit all the relevant events captured by the network.
+ *
+ * However, even to create this ROOT MessageHandler, consumers should use the
+ * RootMessageHandlerRegistry. This singleton will ensure that MessageHandler
+ * instances are properly registered and can be retrieved based on a given
+ * session id as well as some other context information.
+ */
+export class MessageHandler extends EventEmitter {
+ #context;
+ #contextId;
+ #eventsDispatcher;
+ #moduleCache;
+ #registry;
+ #sessionId;
+
+ /**
+ * Create a new MessageHandler instance.
+ *
+ * @param {string} sessionId
+ * ID of the session the handler is used for.
+ * @param {object} context
+ * The context linked to this MessageHandler instance.
+ * @param {MessageHandlerRegistry} registry
+ * The MessageHandlerRegistry which owns this MessageHandler instance.
+ */
+ constructor(sessionId, context, registry) {
+ super();
+
+ this.#moduleCache = new lazy.ModuleCache(this);
+
+ this.#sessionId = sessionId;
+ this.#context = context;
+ this.#contextId = this.constructor.getIdFromContext(context);
+ this.#eventsDispatcher = new lazy.EventsDispatcher(this);
+ this.#registry = registry;
+ }
+
+ get context() {
+ return this.#context;
+ }
+
+ get contextId() {
+ return this.#contextId;
+ }
+
+ get eventsDispatcher() {
+ return this.#eventsDispatcher;
+ }
+
+ get moduleCache() {
+ return this.#moduleCache;
+ }
+
+ get name() {
+ return [this.sessionId, this.constructor.type, this.contextId].join("-");
+ }
+
+ get registry() {
+ return this.#registry;
+ }
+
+ get sessionId() {
+ return this.#sessionId;
+ }
+
+ destroy() {
+ lazy.logger.trace(
+ `MessageHandler ${this.constructor.type} for session ${this.sessionId} is being destroyed`
+ );
+ this.#eventsDispatcher.destroy();
+ this.#moduleCache.destroy();
+
+ // At least the MessageHandlerRegistry will be expecting this event in order
+ // to remove the instance from the registry when destroyed.
+ this.emit("message-handler-destroyed", this);
+ }
+
+ /**
+ * Emit a message handler event.
+ *
+ * Such events should bubble up to the root of a MessageHandler network.
+ *
+ * @param {string} name
+ * Name of the event. Protocol level events should be of the
+ * form [module name].[event name].
+ * @param {object} data
+ * The event's data.
+ * @param {ContextInfo=} contextInfo
+ * The event's context info, used to identify the origin of the event.
+ * If not provided, the context info of the current MessageHandler will be
+ * used.
+ */
+ emitEvent(name, data, contextInfo) {
+ // If no contextInfo field is provided on the event, extract it from the
+ // MessageHandler instance.
+ contextInfo = contextInfo || this.#getContextInfo();
+
+ // Events are emitted both under their own name for consumers listening to
+ // a specific and as `message-handler-event` for consumers which need to
+ // catch all events.
+ this.emit(name, data, contextInfo);
+ this.emit("message-handler-event", {
+ name,
+ contextInfo,
+ data,
+ sessionId: this.sessionId,
+ });
+ }
+
+ /**
+ * @typedef {object} CommandDestination
+ * @property {string} type
+ * One of MessageHandler.type.
+ * @property {string=} id
+ * Unique context identifier. The format depends on the type.
+ * For WINDOW_GLOBAL destinations, this is a browsing context id.
+ * Optional, should only be provided if `contextDescriptor` is missing.
+ * @property {ContextDescriptor=} contextDescriptor
+ * Descriptor used to match several contexts, which will all receive the
+ * command.
+ * Optional, should only be provided if `id` is missing.
+ */
+
+ /**
+ * @typedef {object} Command
+ * @property {string} commandName
+ * The name of the command to execute.
+ * @property {string} moduleName
+ * The name of the module.
+ * @property {object} params
+ * Optional command parameters.
+ * @property {CommandDestination} destination
+ * The destination describing a debuggable context.
+ * @property {boolean=} retryOnAbort
+ * Optional. When true, commands will be retried upon AbortError, which
+ * can occur when the underlying JSWindowActor pair is destroyed.
+ * Defaults to `false`.
+ */
+
+ /**
+ * Retrieve all module classes matching the moduleName and destination.
+ * See `getAllModuleClasses` (ModuleCache.jsm) for more details.
+ *
+ * @param {string} moduleName
+ * The name of the module.
+ * @param {Destination} destination
+ * The destination.
+ * @returns {Array.<class<Module>|null>}
+ * An array of Module classes.
+ */
+ getAllModuleClasses(moduleName, destination) {
+ return this.#moduleCache.getAllModuleClasses(moduleName, destination);
+ }
+
+ /**
+ * Handle a command, either in one of the modules owned by this MessageHandler
+ * or in a another MessageHandler after forwarding the command.
+ *
+ * @param {Command} command
+ * The command that should be either handled in this layer or forwarded to
+ * the next layer leading to the destination.
+ * @returns {Promise} A Promise that will resolve with the return value of the
+ * command once it has been executed.
+ */
+ handleCommand(command) {
+ const { moduleName, commandName, params, destination } = command;
+ lazy.logger.trace(
+ `Received command ${moduleName}.${commandName} for destination ${destination.type}`
+ );
+
+ if (!this.supportsCommand(moduleName, commandName, destination)) {
+ throw new lazy.error.UnsupportedCommandError(
+ `${moduleName}.${commandName} not supported for destination ${destination?.type}`
+ );
+ }
+
+ const module = this.#moduleCache.getModuleInstance(moduleName, destination);
+ if (module && module.supportsMethod(commandName)) {
+ return module[commandName](params, destination);
+ }
+
+ return this.forwardCommand(command);
+ }
+
+ toString() {
+ return `[object ${this.constructor.name} ${this.name}]`;
+ }
+
+ /**
+ * Execute the required initialization steps, inlcluding apply the initial session data items
+ * provided to this MessageHandler on startup. Implementation is specific to each MessageHandler class.
+ *
+ * By default the implementation is a no-op.
+ *
+ * @param {Array<SessionDataItem>} sessionDataItems
+ * Initial session data items for this MessageHandler.
+ */
+ async initialize(sessionDataItems) {}
+
+ /**
+ * Returns the module path corresponding to this MessageHandler class.
+ *
+ * Needs to be implemented in the sub class.
+ */
+ static get modulePath() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Returns the type corresponding to this MessageHandler class.
+ *
+ * Needs to be implemented in the sub class.
+ */
+ static get type() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Returns the id corresponding to a context compatible with this
+ * MessageHandler class.
+ *
+ * Needs to be implemented in the sub class.
+ */
+ static getIdFromContext(context) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Forward a command to other MessageHandlers.
+ *
+ * Needs to be implemented in the sub class.
+ */
+ forwardCommand(command) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Check if contextDescriptor matches the context linked
+ * to this MessageHandler instance.
+ *
+ * Needs to be implemented in the sub class.
+ */
+ matchesContext(contextDescriptor) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Check if the given command is supported in the module
+ * for the destination
+ *
+ * @param {string} moduleName
+ * The name of the module.
+ * @param {string} commandName
+ * The name of the command.
+ * @param {Destination} destination
+ * The destination.
+ * @returns {boolean}
+ * True if the command is supported.
+ */
+ supportsCommand(moduleName, commandName, destination) {
+ return this.getAllModuleClasses(moduleName, destination).some(cls =>
+ cls.supportsMethod(commandName)
+ );
+ }
+
+ /**
+ * Return the context information for this MessageHandler instance, which
+ * can be used to identify the origin of an event.
+ *
+ * @returns {ContextInfo}
+ * The context information for this MessageHandler.
+ */
+ #getContextInfo() {
+ return {
+ contextId: this.contextId,
+ type: this.constructor.type,
+ };
+ }
+}