tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }