ModuleCache.sys.mjs (10537B)
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 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 getMessageHandlerClass: 9 "chrome://remote/content/shared/messagehandler/MessageHandlerRegistry.sys.mjs", 10 Log: "chrome://remote/content/shared/Log.sys.mjs", 11 RootMessageHandler: 12 "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs", 13 WindowGlobalMessageHandler: 14 "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs", 15 }); 16 17 const protocols = { 18 bidi: {}, 19 test: {}, 20 }; 21 22 /** 23 * Defines the hierarchy between MessageHandler layers. 24 * The keys of this map are a type of MessageHandler, and the value is the type 25 * of the parent MessageHandler. 26 * 27 * For instance at the moment "windowglobal" has "root" as parent, 28 * but if we introduce an intermediary "process" layer for performance reasons, 29 * we would instead define: 30 * - "windowglobal" -> "process" 31 * - "process" -> "root" 32 */ 33 ChromeUtils.defineLazyGetter(lazy, "MessageHandlerParentMap", () => { 34 const MessageHandlerParentMap = new Map([ 35 [lazy.WindowGlobalMessageHandler.type, lazy.RootMessageHandler.type], 36 ]); 37 return MessageHandlerParentMap; 38 }); 39 40 // eslint-disable-next-line mozilla/lazy-getter-object-name 41 ChromeUtils.defineESModuleGetters(protocols.bidi, { 42 // Additional protocols might use a different registry for their modules, 43 // in which case this will no longer be a constant but will instead depend on 44 // the protocol owning the MessageHandler. See Bug 1722464. 45 modules: 46 "chrome://remote/content/webdriver-bidi/modules/ModuleRegistry.sys.mjs", 47 }); 48 // eslint-disable-next-line mozilla/lazy-getter-object-name 49 ChromeUtils.defineESModuleGetters(protocols.test, { 50 modules: 51 "chrome://mochitests/content/browser/remote/shared/messagehandler/test/browser/resources/modules/ModuleRegistry.sys.mjs", 52 }); 53 54 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); 55 56 /** 57 * ModuleCache instances are dedicated to lazily create and cache the instances 58 * of all the modules related to a specific MessageHandler instance. 59 * 60 * ModuleCache also implements the logic to resolve the path to the file for a 61 * given module, which depends both on the current MessageHandler context and on 62 * the expected destination. 63 * 64 * In order to implement module logic in any context, separate module files 65 * should be created for each situation. For instance, for a given module, 66 * - ${MODULES_FOLDER}/root/{ModuleName}.sys.mjs contains the implementation for 67 * commands intended for the destination ROOT, and will be created for a ROOT 68 * MessageHandler only. Typically, they will run in the parent process. 69 * - ${MODULES_FOLDER}/windowglobal/{ModuleName}.sys.mjs contains the implementation 70 * for commands intended for a WINDOW_GLOBAL destination, and will be created 71 * for a WINDOW_GLOBAL MessageHandler only. Those will usually run in a 72 * content process. 73 * - ${MODULES_FOLDER}/windowglobal-in-root/{ModuleName}.sys.mjs also handles 74 * commands intended for a WINDOW_GLOBAL destination, but they will be created 75 * for the ROOT MessageHandler and will run in the parent process. This can be 76 * useful if some code has to be executed in the parent process, even though 77 * the final destination is a WINDOW_GLOBAL. 78 * - And so on, as more MessageHandler types get added, more combinations will 79 * follow based on the same pattern: 80 * - {contextName}/{ModuleName}.sys.mjs 81 * - or {destinationType}-in-{currentType}/{ModuleName}.sys.mjs 82 * 83 * All those implementations are optional. If a module cannot be found, based on 84 * the logic detailed above, the MessageHandler will assume that the command 85 * should simply be forwarded to the next layer of the network. 86 */ 87 export class ModuleCache { 88 #messageHandler; 89 #messageHandlerType; 90 #modules; 91 #protocol; 92 93 /** 94 * @param {MessageHandler} messageHandler 95 * The MessageHandler instance which owns this ModuleCache instance. 96 */ 97 constructor(messageHandler) { 98 this.#messageHandler = messageHandler; 99 this.#messageHandlerType = messageHandler.constructor.type; 100 101 // Map of absolute module paths to module instances. 102 this.#modules = new Map(); 103 104 // Use the module class from the WebDriverBiDi ModuleRegistry if we 105 // are not using test modules. 106 this.#protocol = Services.prefs.getBoolPref( 107 "remote.messagehandler.modulecache.useBrowserTestRoot", 108 false 109 ) 110 ? protocols.test 111 : protocols.bidi; 112 } 113 114 /** 115 * Destroy all instantiated modules. 116 */ 117 destroy() { 118 this.#modules.forEach(module => module?.destroy()); 119 } 120 121 /** 122 * Retrieve all module classes matching the provided module name to reach the 123 * provided destination, from the current context. 124 * 125 * This corresponds to the path a command can take to reach its destination. 126 * A command's method must be implemented in one of the classes returned by 127 * getAllModuleClasses in order to be successfully handled. 128 * 129 * @param {string} moduleName 130 * The name of the module. 131 * @param {Destination} destination 132 * The destination. 133 * @returns {Array<class<Module>|null>} 134 * An array of Module classes. 135 */ 136 getAllModuleClasses(moduleName, destination) { 137 const classes = []; 138 139 let currentType = destination.type; 140 while (currentType) { 141 classes.push( 142 this.#getModuleClass(moduleName, currentType, destination.type) 143 ); 144 currentType = lazy.MessageHandlerParentMap.get(currentType); 145 } 146 147 return classes.filter(cls => !!cls); 148 } 149 150 /** 151 * Get a module instance corresponding to the provided moduleName and 152 * destination. If no existing module can be found in the cache, ModuleCache 153 * will attempt to import the module file and create a new instance, which 154 * will then be cached and returned for subsequent calls. 155 * 156 * @param {string} moduleName 157 * The name of the module which should implement the command. 158 * @param {CommandDestination} destination 159 * The destination of the command for which we need to instantiate a 160 * module. See MessageHandler.sys.mjs for the CommandDestination typedef. 161 * @returns {object=} 162 * A module instance corresponding to the provided moduleName and 163 * destination, or null if it could not be instantiated. 164 */ 165 getModuleInstance(moduleName, destination) { 166 const key = `${moduleName}-${destination.type}`; 167 168 if (this.#modules.has(key)) { 169 // If there is already a cached instance (potentially null) for the 170 // module name + destination type pair, return it. 171 return this.#modules.get(key); 172 } 173 174 const ModuleClass = this.#getModuleClass( 175 moduleName, 176 this.#messageHandlerType, 177 destination.type 178 ); 179 180 let module = null; 181 if (ModuleClass) { 182 module = new ModuleClass(this.#messageHandler); 183 module.moduleName = moduleName; 184 } 185 186 this.#modules.set(key, module); 187 return module; 188 } 189 190 /** 191 * Check if the given module exists for the destination. 192 * 193 * @param {string} moduleName 194 * The name of the module. 195 * @param {Destination} destination 196 * The destination. 197 * @returns {boolean} 198 * True if the module exists. 199 */ 200 hasModuleClass(moduleName, destination) { 201 const classes = this.getAllModuleClasses(moduleName, destination); 202 return !!classes.length; 203 } 204 205 toString() { 206 return `[object ${this.constructor.name} ${this.#messageHandler.name}]`; 207 } 208 209 /** 210 * Retrieve the module class matching the provided module name and folder. 211 * 212 * @param {string} moduleName 213 * The name of the module to get the class for. 214 * @param {string} originType 215 * The MessageHandler type from where the command comes. 216 * @param {string} destinationType 217 * The MessageHandler type where the command should go to. 218 * @returns {Class=} 219 * The class corresponding to the module name and folder, null if no match 220 * was found. 221 * @throws {Error} 222 * If the provided module folder is unexpected. 223 */ 224 #getModuleClass = function (moduleName, originType, destinationType) { 225 if ( 226 destinationType === lazy.RootMessageHandler.type && 227 originType !== destinationType 228 ) { 229 // If we are trying to reach the root layer from a lower layer, no module 230 // class should attempt to handle the command in the current layer and 231 // the command should be forwarded unconditionally. 232 return null; 233 } 234 235 const moduleFolder = this.#getModuleFolder(originType, destinationType); 236 if (!this.#protocol.modules[moduleFolder]) { 237 throw new Error( 238 `Invalid module folder "${moduleFolder}", expected one of "${Object.keys( 239 this.#protocol.modules 240 )}"` 241 ); 242 } 243 244 let modulePath = this.#protocol.modules[moduleFolder]; 245 if (moduleName.includes(":")) { 246 const parts = moduleName.split(":"); 247 moduleName = parts[1]; 248 modulePath = modulePath[parts[0]]; 249 } 250 251 const moduleClass = modulePath?.[moduleName] ?? null; 252 253 // Module hit/miss logs generate a lot of spam. Only log if verbose is true. 254 // 255 // Note: Due to https://bugzilla.mozilla.org/show_bug.cgi?id=1828395 256 // verbose is currently always false if the log level is trace. 257 // If those logs are needed before the bug is fixed, temporarily remove the 258 // condition. 259 if (lazy.Log.verbose) { 260 if (moduleClass) { 261 lazy.logger.trace( 262 `Module ${moduleFolder}/${moduleName}.sys.mjs found for ${destinationType}` 263 ); 264 } else { 265 lazy.logger.trace( 266 `Module ${moduleFolder}/${moduleName}.sys.mjs not found for ${destinationType}` 267 ); 268 } 269 } 270 271 return moduleClass; 272 }; 273 274 #getModuleFolder(originType, destinationType) { 275 const originPath = lazy.getMessageHandlerClass(originType).modulePath; 276 if (originType === destinationType) { 277 // If the command is targeting the current type, the module is expected to 278 // be in eg "windowglobal/${moduleName}.sys.mjs". 279 return originPath; 280 } 281 282 // If the command is targeting another type, the module is expected to 283 // be in a composed folder eg "windowglobal-in-root/${moduleName}.sys.mjs". 284 const destinationPath = 285 lazy.getMessageHandlerClass(destinationType).modulePath; 286 return `${destinationPath}-in-${originPath}`; 287 } 288 }