357 lines
11 KiB
JavaScript
357 lines
11 KiB
JavaScript
/* 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.
|
|
* For ContextDescriptorType.UserContext, the id should be the
|
|
* platform user context id.
|
|
*/
|
|
|
|
/**
|
|
* Enum of ContextDescriptor types.
|
|
*
|
|
* @enum {string}
|
|
*/
|
|
export const ContextDescriptorType = {
|
|
All: "All",
|
|
TopBrowsingContext: "TopBrowsingContext",
|
|
UserContext: "UserContext",
|
|
};
|
|
|
|
/**
|
|
* 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 | number=} 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.
|
|
* If not explicitly set, the framework will automatically retry if the
|
|
* destination is likely to be replaced (e.g. browsingContext on the
|
|
* initial document or loading a document).
|
|
*/
|
|
|
|
/**
|
|
* Retrieve all module classes matching the moduleName and destination.
|
|
* See `getAllModuleClasses` (ModuleCache.sys.mjs) 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.
|
|
*/
|
|
async initialize() {}
|
|
|
|
/**
|
|
* 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() {
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
/**
|
|
* Forward a command to other MessageHandlers.
|
|
*
|
|
* Needs to be implemented in the sub class.
|
|
*/
|
|
forwardCommand() {
|
|
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() {
|
|
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,
|
|
};
|
|
}
|
|
}
|