MessageHandler.sys.mjs (12395B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 error: "chrome://remote/content/shared/messagehandler/Errors.sys.mjs", 11 EventsDispatcher: 12 "chrome://remote/content/shared/messagehandler/EventsDispatcher.sys.mjs", 13 Log: "chrome://remote/content/shared/Log.sys.mjs", 14 ModuleCache: 15 "chrome://remote/content/shared/messagehandler/ModuleCache.sys.mjs", 16 }); 17 18 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); 19 20 /** 21 * A ContextDescriptor object provides information to decide if a broadcast or 22 * a session data item should be applied to a specific MessageHandler context. 23 * 24 * @typedef {object} ContextDescriptor 25 * @property {ContextDescriptorType} type 26 * The type of context 27 * @property {string=} id 28 * Unique id of a given context for the provided type. 29 * For ContextDescriptorType.All, id can be omitted. 30 * For ContextDescriptorType.TopBrowsingContext, the id should be the 31 * browserId corresponding to a top-level browsing context. 32 * For ContextDescriptorType.UserContext, the id should be the 33 * platform user context id. 34 */ 35 36 /** 37 * Enum of ContextDescriptor types. 38 * 39 * @enum {string} 40 */ 41 export const ContextDescriptorType = { 42 All: "All", 43 TopBrowsingContext: "TopBrowsingContext", 44 UserContext: "UserContext", 45 }; 46 47 /** 48 * A ContextInfo identifies a given context that can be linked to a MessageHandler 49 * instance. It should be used to identify events coming from this context. 50 * 51 * It can either be provided by the MessageHandler itself, when the event is 52 * emitted from the context it relates to. 53 * 54 * Or it can be assembled manually, for instance when emitting an event which 55 * relates to a window global from the root layer (eg browsingContext.contextCreated). 56 * 57 * @typedef {object} ContextInfo 58 * @property {string} contextId 59 * Unique id of the MessageHandler corresponding to this context. 60 * @property {string} type 61 * One of MessageHandler.type. 62 */ 63 64 /** 65 * MessageHandler instances are dedicated to handle both Commands and Events 66 * to enable automation and introspection for remote control protocols. 67 * 68 * MessageHandler instances are designed to form a network, where each instance 69 * should allow to inspect a specific context (eg. a BrowsingContext, a Worker, 70 * etc). Those instances might live in different processes and threads but 71 * should be linked together by the usage of a single sessionId, shared by all 72 * the instances of a single MessageHandler network. 73 * 74 * MessageHandler instances will be dynamically spawned depending on which 75 * Command or which Event needs to be processed and should therefore not be 76 * explicitly created by consumers, nor used directly. 77 * 78 * The only exception is the ROOT MessageHandler. This MessageHandler will be 79 * the entry point to send commands to the rest of the network. It will also 80 * emit all the relevant events captured by the network. 81 * 82 * However, even to create this ROOT MessageHandler, consumers should use the 83 * RootMessageHandlerRegistry. This singleton will ensure that MessageHandler 84 * instances are properly registered and can be retrieved based on a given 85 * session id as well as some other context information. 86 */ 87 export class MessageHandler extends EventEmitter { 88 #context; 89 #contextId; 90 #eventsDispatcher; 91 #moduleCache; 92 #registry; 93 #sessionId; 94 95 /** 96 * Create a new MessageHandler instance. 97 * 98 * @param {string} sessionId 99 * ID of the session the handler is used for. 100 * @param {object} context 101 * The context linked to this MessageHandler instance. 102 * @param {MessageHandlerRegistry} registry 103 * The MessageHandlerRegistry which owns this MessageHandler instance. 104 */ 105 constructor(sessionId, context, registry) { 106 super(); 107 108 this.#moduleCache = new lazy.ModuleCache(this); 109 110 this.#sessionId = sessionId; 111 this.#context = context; 112 this.#contextId = this.constructor.getIdFromContext(context); 113 this.#eventsDispatcher = new lazy.EventsDispatcher(this); 114 this.#registry = registry; 115 } 116 117 get context() { 118 return this.#context; 119 } 120 121 get contextId() { 122 return this.#contextId; 123 } 124 125 get eventsDispatcher() { 126 return this.#eventsDispatcher; 127 } 128 129 get moduleCache() { 130 return this.#moduleCache; 131 } 132 133 get name() { 134 return [this.sessionId, this.constructor.type, this.contextId].join("-"); 135 } 136 137 get registry() { 138 return this.#registry; 139 } 140 141 get sessionId() { 142 return this.#sessionId; 143 } 144 145 destroy() { 146 lazy.logger.trace( 147 `MessageHandler ${this.constructor.type} for session ${this.sessionId} is being destroyed` 148 ); 149 this.#eventsDispatcher.destroy(); 150 this.#moduleCache.destroy(); 151 152 // At least the MessageHandlerRegistry will be expecting this event in order 153 // to remove the instance from the registry when destroyed. 154 this.emit("message-handler-destroyed", this); 155 } 156 157 /** 158 * Check if the provided context matches provided contextDescriptor. 159 * 160 * @param {BrowsingContext} browsingContext 161 * The browsing context to verify. 162 * @param {ContextDescriptor} contextDescriptor 163 * The context descriptor to match. 164 * 165 * @returns {boolean} 166 * Return "true" if the context matches the context descriptor, 167 * "false" otherwise. 168 */ 169 contextMatchesDescriptor(browsingContext, contextDescriptor) { 170 return ( 171 contextDescriptor.type === ContextDescriptorType.All || 172 (contextDescriptor.type === ContextDescriptorType.TopBrowsingContext && 173 contextDescriptor.id === browsingContext.browserId) || 174 (contextDescriptor.type === ContextDescriptorType.UserContext && 175 contextDescriptor.id === browsingContext.originAttributes.userContextId) 176 ); 177 } 178 179 /** 180 * Emit a message handler event. 181 * 182 * Such events should bubble up to the root of a MessageHandler network. 183 * 184 * @param {string} name 185 * Name of the event. Protocol level events should be of the 186 * form [module name].[event name]. 187 * @param {object} data 188 * The event's data. 189 * @param {ContextInfo=} contextInfo 190 * The event's context info, used to identify the origin of the event. 191 * If not provided, the context info of the current MessageHandler will be 192 * used. 193 */ 194 emitEvent(name, data, contextInfo) { 195 // If no contextInfo field is provided on the event, extract it from the 196 // MessageHandler instance. 197 contextInfo = contextInfo || this.#getContextInfo(); 198 199 // Events are emitted both under their own name for consumers listening to 200 // a specific and as `message-handler-event` for consumers which need to 201 // catch all events. 202 this.emit(name, data, contextInfo); 203 this.emit("message-handler-event", { 204 name, 205 contextInfo, 206 data, 207 sessionId: this.sessionId, 208 }); 209 } 210 211 /** 212 * @typedef {object} CommandDestination 213 * @property {string} type 214 * One of MessageHandler.type. 215 * @property {string | number=} id 216 * Unique context identifier. The format depends on the type. 217 * For WINDOW_GLOBAL destinations, this is a browsing context id. 218 * Optional, should only be provided if `contextDescriptor` is missing. 219 * @property {ContextDescriptor=} contextDescriptor 220 * Descriptor used to match several contexts, which will all receive the 221 * command. 222 * Optional, should only be provided if `id` is missing. 223 */ 224 225 /** 226 * @typedef {object} Command 227 * @property {string} commandName 228 * The name of the command to execute. 229 * @property {string} moduleName 230 * The name of the module. 231 * @property {object} params 232 * Optional command parameters. 233 * @property {CommandDestination} destination 234 * The destination describing a debuggable context. 235 * @property {boolean=} retryOnAbort 236 * Optional. When true, commands will be retried upon AbortError, which 237 * can occur when the underlying JSWindowActor pair is destroyed. 238 * If not explicitly set, the framework will automatically retry if the 239 * destination is likely to be replaced (e.g. browsingContext on the 240 * initial document or loading a document). 241 */ 242 243 /** 244 * Retrieve all module classes matching the moduleName and destination. 245 * See `getAllModuleClasses` (ModuleCache.sys.mjs) for more details. 246 * 247 * @param {string} moduleName 248 * The name of the module. 249 * @param {Destination} destination 250 * The destination. 251 * @returns {Array.<class<Module>|null>} 252 * An array of Module classes. 253 */ 254 getAllModuleClasses(moduleName, destination) { 255 return this.#moduleCache.getAllModuleClasses(moduleName, destination); 256 } 257 258 /** 259 * Handle a command, either in one of the modules owned by this MessageHandler 260 * or in a another MessageHandler after forwarding the command. 261 * 262 * @param {Command} command 263 * The command that should be either handled in this layer or forwarded to 264 * the next layer leading to the destination. 265 * @returns {Promise} A Promise that will resolve with the return value of the 266 * command once it has been executed. 267 */ 268 handleCommand(command) { 269 const { moduleName, commandName, params, destination } = command; 270 lazy.logger.trace( 271 `Received command ${moduleName}.${commandName} for destination ${destination.type}` 272 ); 273 274 if (!this.supportsCommand(moduleName, commandName, destination)) { 275 throw new lazy.error.UnsupportedCommandError( 276 `${moduleName}.${commandName} not supported for destination ${destination?.type}` 277 ); 278 } 279 280 const module = this.#moduleCache.getModuleInstance(moduleName, destination); 281 if (module && module.supportsMethod(commandName)) { 282 return module[commandName](params, destination); 283 } 284 285 return this.forwardCommand(command); 286 } 287 288 toString() { 289 return `[object ${this.constructor.name} ${this.name}]`; 290 } 291 292 /** 293 * Execute the required initialization steps, including apply the initial session data items 294 * provided to this MessageHandler on startup. Implementation is specific to each MessageHandler class. 295 * 296 * By default the implementation is a no-op. 297 */ 298 async initialize() {} 299 300 /** 301 * Returns the module path corresponding to this MessageHandler class. 302 * 303 * Needs to be implemented in the sub class. 304 */ 305 static get modulePath() { 306 throw new Error("Not implemented"); 307 } 308 309 /** 310 * Returns the type corresponding to this MessageHandler class. 311 * 312 * Needs to be implemented in the sub class. 313 */ 314 static get type() { 315 throw new Error("Not implemented"); 316 } 317 318 /** 319 * Returns the id corresponding to a context compatible with this 320 * MessageHandler class. 321 * 322 * Needs to be implemented in the sub class. 323 */ 324 static getIdFromContext() { 325 throw new Error("Not implemented"); 326 } 327 328 /** 329 * Forward a command to other MessageHandlers. 330 * 331 * Needs to be implemented in the sub class. 332 */ 333 forwardCommand() { 334 throw new Error("Not implemented"); 335 } 336 337 /** 338 * Check if contextDescriptor matches the context linked 339 * to this MessageHandler instance. 340 * 341 * Needs to be implemented in the sub class. 342 */ 343 matchesContext() { 344 throw new Error("Not implemented"); 345 } 346 347 /** 348 * Check if the given command is supported in the module 349 * for the destination 350 * 351 * @param {string} moduleName 352 * The name of the module. 353 * @param {string} commandName 354 * The name of the command. 355 * @param {Destination} destination 356 * The destination. 357 * @returns {boolean} 358 * True if the command is supported. 359 */ 360 supportsCommand(moduleName, commandName, destination) { 361 return this.getAllModuleClasses(moduleName, destination).some(cls => 362 cls.supportsMethod(commandName) 363 ); 364 } 365 366 /** 367 * Return the context information for this MessageHandler instance, which 368 * can be used to identify the origin of an event. 369 * 370 * @returns {ContextInfo} 371 * The context information for this MessageHandler. 372 */ 373 #getContextInfo() { 374 return { 375 contextId: this.contextId, 376 type: this.constructor.type, 377 }; 378 } 379 }