276 lines
9.9 KiB
JavaScript
276 lines
9.9 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
getMessageHandlerClass:
|
|
"chrome://remote/content/shared/messagehandler/MessageHandlerRegistry.sys.mjs",
|
|
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
|
RootMessageHandler:
|
|
"chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
|
|
});
|
|
|
|
const protocols = {
|
|
bidi: {},
|
|
test: {},
|
|
};
|
|
// eslint-disable-next-line mozilla/lazy-getter-object-name
|
|
ChromeUtils.defineESModuleGetters(protocols.bidi, {
|
|
// Additional protocols might use a different registry for their modules,
|
|
// in which case this will no longer be a constant but will instead depend on
|
|
// the protocol owning the MessageHandler. See Bug 1722464.
|
|
modules:
|
|
"chrome://remote/content/webdriver-bidi/modules/ModuleRegistry.sys.mjs",
|
|
});
|
|
// eslint-disable-next-line mozilla/lazy-getter-object-name
|
|
ChromeUtils.defineESModuleGetters(protocols.test, {
|
|
modules:
|
|
"chrome://mochitests/content/browser/remote/shared/messagehandler/test/browser/resources/modules/ModuleRegistry.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
|
|
|
|
/**
|
|
* ModuleCache instances are dedicated to lazily create and cache the instances
|
|
* of all the modules related to a specific MessageHandler instance.
|
|
*
|
|
* ModuleCache also implements the logic to resolve the path to the file for a
|
|
* given module, which depends both on the current MessageHandler context and on
|
|
* the expected destination.
|
|
*
|
|
* In order to implement module logic in any context, separate module files
|
|
* should be created for each situation. For instance, for a given module,
|
|
* - ${MODULES_FOLDER}/root/{ModuleName}.sys.mjs contains the implementation for
|
|
* commands intended for the destination ROOT, and will be created for a ROOT
|
|
* MessageHandler only. Typically, they will run in the parent process.
|
|
* - ${MODULES_FOLDER}/windowglobal/{ModuleName}.sys.mjs contains the implementation
|
|
* for commands intended for a WINDOW_GLOBAL destination, and will be created
|
|
* for a WINDOW_GLOBAL MessageHandler only. Those will usually run in a
|
|
* content process.
|
|
* - ${MODULES_FOLDER}/windowglobal-in-root/{ModuleName}.sys.mjs also handles
|
|
* commands intended for a WINDOW_GLOBAL destination, but they will be created
|
|
* for the ROOT MessageHandler and will run in the parent process. This can be
|
|
* useful if some code has to be executed in the parent process, even though
|
|
* the final destination is a WINDOW_GLOBAL.
|
|
* - And so on, as more MessageHandler types get added, more combinations will
|
|
* follow based on the same pattern:
|
|
* - {contextName}/{ModuleName}.sys.mjs
|
|
* - or {destinationType}-in-{currentType}/{ModuleName}.sys.mjs
|
|
*
|
|
* All those implementations are optional. If a module cannot be found, based on
|
|
* the logic detailed above, the MessageHandler will assume that the command
|
|
* should simply be forwarded to the next layer of the network.
|
|
*/
|
|
export class ModuleCache {
|
|
#messageHandler;
|
|
#messageHandlerType;
|
|
#modules;
|
|
#protocol;
|
|
|
|
/*
|
|
* @param {MessageHandler} messageHandler
|
|
* The MessageHandler instance which owns this ModuleCache instance.
|
|
*/
|
|
constructor(messageHandler) {
|
|
this.#messageHandler = messageHandler;
|
|
this.#messageHandlerType = messageHandler.constructor.type;
|
|
|
|
// Map of absolute module paths to module instances.
|
|
this.#modules = new Map();
|
|
|
|
// Use the module class from the WebDriverBiDi ModuleRegistry if we
|
|
// are not using test modules.
|
|
this.#protocol = Services.prefs.getBoolPref(
|
|
"remote.messagehandler.modulecache.useBrowserTestRoot",
|
|
false
|
|
)
|
|
? protocols.test
|
|
: protocols.bidi;
|
|
}
|
|
|
|
/**
|
|
* Destroy all instantiated modules.
|
|
*/
|
|
destroy() {
|
|
this.#modules.forEach(module => module?.destroy());
|
|
}
|
|
|
|
/**
|
|
* Retrieve all module classes matching the provided module name to reach the
|
|
* provided destination, from the current context.
|
|
*
|
|
* This corresponds to the path a command can take to reach its destination.
|
|
* A command's method must be implemented in one of the classes returned by
|
|
* getAllModuleClasses in order to be successfully handled.
|
|
*
|
|
* @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) {
|
|
const destinationType = destination.type;
|
|
const classes = [
|
|
this.#getModuleClass(
|
|
moduleName,
|
|
this.#messageHandlerType,
|
|
destinationType
|
|
),
|
|
];
|
|
|
|
// Bug 1733242: Extend the implementation of this method to handle workers.
|
|
// It assumes layers have at most one level of nesting, for instance
|
|
// "root -> windowglobal", but it wouldn't work for something such as
|
|
// "root -> windowglobal -> worker".
|
|
if (destinationType !== this.#messageHandlerType) {
|
|
classes.push(
|
|
this.#getModuleClass(moduleName, destinationType, destinationType)
|
|
);
|
|
}
|
|
|
|
return classes.filter(cls => !!cls);
|
|
}
|
|
|
|
/**
|
|
* Get a module instance corresponding to the provided moduleName and
|
|
* destination. If no existing module can be found in the cache, ModuleCache
|
|
* will attempt to import the module file and create a new instance, which
|
|
* will then be cached and returned for subsequent calls.
|
|
*
|
|
* @param {string} moduleName
|
|
* The name of the module which should implement the command.
|
|
* @param {CommandDestination} destination
|
|
* The destination of the command for which we need to instantiate a
|
|
* module. See MessageHandler.sys.mjs for the CommandDestination typedef.
|
|
* @returns {object=}
|
|
* A module instance corresponding to the provided moduleName and
|
|
* destination, or null if it could not be instantiated.
|
|
*/
|
|
getModuleInstance(moduleName, destination) {
|
|
const key = `${moduleName}-${destination.type}`;
|
|
|
|
if (this.#modules.has(key)) {
|
|
// If there is already a cached instance (potentially null) for the
|
|
// module name + destination type pair, return it.
|
|
return this.#modules.get(key);
|
|
}
|
|
|
|
const ModuleClass = this.#getModuleClass(
|
|
moduleName,
|
|
this.#messageHandlerType,
|
|
destination.type
|
|
);
|
|
|
|
let module = null;
|
|
if (ModuleClass) {
|
|
module = new ModuleClass(this.#messageHandler);
|
|
module.moduleName = moduleName;
|
|
}
|
|
|
|
this.#modules.set(key, module);
|
|
return module;
|
|
}
|
|
|
|
/**
|
|
* Check if the given module exists for the destination.
|
|
*
|
|
* @param {string} moduleName
|
|
* The name of the module.
|
|
* @param {Destination} destination
|
|
* The destination.
|
|
* @returns {boolean}
|
|
* True if the module exists.
|
|
*/
|
|
hasModuleClass(moduleName, destination) {
|
|
const classes = this.getAllModuleClasses(moduleName, destination);
|
|
return !!classes.length;
|
|
}
|
|
|
|
toString() {
|
|
return `[object ${this.constructor.name} ${this.#messageHandler.name}]`;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the module class matching the provided module name and folder.
|
|
*
|
|
* @param {string} moduleName
|
|
* The name of the module to get the class for.
|
|
* @param {string} originType
|
|
* The MessageHandler type from where the command comes.
|
|
* @param {string} destinationType
|
|
* The MessageHandler type where the command should go to.
|
|
* @returns {Class=}
|
|
* The class corresponding to the module name and folder, null if no match
|
|
* was found.
|
|
* @throws {Error}
|
|
* If the provided module folder is unexpected.
|
|
*/
|
|
#getModuleClass = function (moduleName, originType, destinationType) {
|
|
if (
|
|
destinationType === lazy.RootMessageHandler.type &&
|
|
originType !== destinationType
|
|
) {
|
|
// If we are trying to reach the root layer from a lower layer, no module
|
|
// class should attempt to handle the command in the current layer and
|
|
// the command should be forwarded unconditionally.
|
|
return null;
|
|
}
|
|
|
|
const moduleFolder = this.#getModuleFolder(originType, destinationType);
|
|
if (!this.#protocol.modules[moduleFolder]) {
|
|
throw new Error(
|
|
`Invalid module folder "${moduleFolder}", expected one of "${Object.keys(
|
|
this.#protocol.modules
|
|
)}"`
|
|
);
|
|
}
|
|
|
|
let modulePath = this.#protocol.modules[moduleFolder];
|
|
if (moduleName.includes(":")) {
|
|
const parts = moduleName.split(":");
|
|
moduleName = parts[1];
|
|
modulePath = modulePath[parts[0]];
|
|
}
|
|
|
|
const moduleClass = modulePath?.[moduleName] ?? null;
|
|
|
|
// Module hit/miss logs generate a lot of spam. Only log if verbose is true.
|
|
//
|
|
// Note: Due to https://bugzilla.mozilla.org/show_bug.cgi?id=1828395
|
|
// verbose is currently always false if the log level is trace.
|
|
// If those logs are needed before the bug is fixed, temporarily remove the
|
|
// condition.
|
|
if (lazy.Log.verbose) {
|
|
if (moduleClass) {
|
|
lazy.logger.trace(
|
|
`Module ${moduleFolder}/${moduleName}.sys.mjs found for ${destinationType}`
|
|
);
|
|
} else {
|
|
lazy.logger.trace(
|
|
`Module ${moduleFolder}/${moduleName}.sys.mjs not found for ${destinationType}`
|
|
);
|
|
}
|
|
}
|
|
|
|
return moduleClass;
|
|
};
|
|
|
|
#getModuleFolder(originType, destinationType) {
|
|
const originPath = lazy.getMessageHandlerClass(originType).modulePath;
|
|
if (originType === destinationType) {
|
|
// If the command is targeting the current type, the module is expected to
|
|
// be in eg "windowglobal/${moduleName}.sys.mjs".
|
|
return originPath;
|
|
}
|
|
|
|
// If the command is targeting another type, the module is expected to
|
|
// be in a composed folder eg "windowglobal-in-root/${moduleName}.sys.mjs".
|
|
const destinationPath =
|
|
lazy.getMessageHandlerClass(destinationType).modulePath;
|
|
return `${destinationPath}-in-${originPath}`;
|
|
}
|
|
}
|