tor-browser

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

ContentProcessWatcherRegistry.sys.mjs (18256B)


      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 ChromeUtils.defineESModuleGetters(
      7  lazy,
      8  {
      9    loader: "resource://devtools/shared/loader/Loader.sys.mjs",
     10  },
     11  { global: "contextual" }
     12 );
     13 
     14 ChromeUtils.defineESModuleGetters(
     15  lazy,
     16  {
     17    releaseDistinctSystemPrincipalLoader:
     18      "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
     19    useDistinctSystemPrincipalLoader:
     20      "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
     21  },
     22  { global: "shared" }
     23 );
     24 
     25 // Name of the attribute into which we save data in `sharedData` object.
     26 const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
     27 
     28 // Map(String => Object)
     29 // Map storing the data objects for all currently active watcher actors.
     30 // The data objects are defined by `createWatcherDataObject()`.
     31 // The main attribute of interest is the `sessionData` one which is set alongside
     32 // various other attributes necessary to maintain state per watcher in the content process.
     33 //
     34 // The Session Data object is maintained by ParentProcessWatcherRegistry, in the parent process
     35 // and is fetched from the content process via `sharedData` API.
     36 // It is then manually maintained via DevToolsProcess JS Actor queries.
     37 let gAllWatcherData = null;
     38 
     39 export const ContentProcessWatcherRegistry = {
     40  _getAllWatchersDataMap() {
     41    if (gAllWatcherData) {
     42      return gAllWatcherData;
     43    }
     44    const { sharedData } = Services.cpmm;
     45    const sessionDataByWatcherActorID = sharedData.get(SHARED_DATA_KEY_NAME);
     46    if (!sessionDataByWatcherActorID) {
     47      throw new Error("Missing session data in `sharedData`");
     48    }
     49 
     50    // Initialize a distinct Map to replicate the one read from `sharedData`.
     51    // This distinct Map will be updated via DevToolsProcess JS Actor queries.
     52    // This helps better control the execution flow.
     53    gAllWatcherData = new Map();
     54 
     55    // The Browser Toolbox will load its server modules in a distinct global/compartment whose name is "DevTools global".
     56    // (See https://searchfox.org/mozilla-central/rev/0e9ea50a999420d93df0e4e27094952af48dd3b8/js/xpconnect/loader/mozJSModuleLoader.cpp#699)
     57    // It means that this class will be instantiated twice, one in each global (the shared one and the browser toolbox one).
     58    // We then have to distinguish the two subset of watcher actors accordingly within `sharedMap`,
     59    // as `sharedMap` will be shared between the two module instances.
     60    // Session type "all" relates to the Browser Toolbox.
     61    const isInBrowserToolboxLoader =
     62      // eslint-disable-next-line mozilla/reject-globalThis-modification
     63      Cu.getRealmLocation(globalThis) == "DevTools global";
     64 
     65    for (const [watcherActorID, sessionData] of sessionDataByWatcherActorID) {
     66      // Filter in/out the watchers based on the current module loader and the watcher session type.
     67      const isBrowserToolboxWatcher = sessionData.sessionContext.type == "all";
     68      if (
     69        (isInBrowserToolboxLoader && !isBrowserToolboxWatcher) ||
     70        (!isInBrowserToolboxLoader && isBrowserToolboxWatcher)
     71      ) {
     72        continue;
     73      }
     74 
     75      gAllWatcherData.set(
     76        watcherActorID,
     77        createWatcherDataObject(watcherActorID, sessionData)
     78      );
     79    }
     80 
     81    return gAllWatcherData;
     82  },
     83 
     84  /**
     85   * Get all data objects for all currently active watcher actors.
     86   * If a specific target type is passed, this will only return objects of watcher actively watching for a given target type.
     87   *
     88   * @param {string} targetType
     89   *        Optional target type to filter only a subset of watchers.
     90   * @return {Array|Iterator}
     91   *         List of data objects. (see createWatcherDataObject)
     92   */
     93  getAllWatchersDataObjects(targetType) {
     94    if (targetType) {
     95      const list = [];
     96      for (const watcherDataObject of this._getAllWatchersDataMap().values()) {
     97        if (watcherDataObject.sessionData.targets?.includes(targetType)) {
     98          list.push(watcherDataObject);
     99        }
    100      }
    101      return list;
    102    }
    103    return this._getAllWatchersDataMap().values();
    104  },
    105 
    106  /**
    107   * Similar to `getAllWatcherDataObjects`, but will only return the already existing registered watchers in this process.
    108   */
    109  getAllExistingWatchersDataObjects() {
    110    if (!gAllWatcherData) {
    111      return [];
    112    }
    113    return gAllWatcherData.values();
    114  },
    115 
    116  /**
    117   * Get the watcher data object for a given watcher actor.
    118   *
    119   * @param {string} watcherActorID
    120   * @param {boolean} onlyFromCache
    121   *        If set explicitly to true, will avoid falling back to shared data.
    122   *        This is typically useful on destructor/removing/cleanup to avoid creating unexpected data.
    123   *        It is also used to avoid the exception thrown when sharedData is cleared on toolbox destruction.
    124   */
    125  getWatcherDataObject(watcherActorID, onlyFromCache = false) {
    126    let data =
    127      ContentProcessWatcherRegistry._getAllWatchersDataMap().get(
    128        watcherActorID
    129      );
    130    if (!data && !onlyFromCache) {
    131      // When there is more than one DevTools opened, the DevToolsProcess JS Actor spawned by the first DevTools
    132      // created a cached Map in `_getAllWatchersDataMap`.
    133      // When opening a second DevTools, this cached Map may miss some new SessionData related to this new DevTools instance,
    134      // and new Watcher Actor.
    135      // When such scenario happens, fallback to `sharedData` which should hopefully be containing the latest DevTools instance SessionData.
    136      //
    137      // May be the watcher should trigger a very first JS Actor query before any others in order to transfer the base Session Data object?
    138      const { sharedData } = Services.cpmm;
    139      const sessionDataByWatcherActorID = sharedData.get(SHARED_DATA_KEY_NAME);
    140      const sessionData = sessionDataByWatcherActorID.get(watcherActorID);
    141      if (!sessionData) {
    142        throw new Error("Unable to find data for watcher " + watcherActorID);
    143      }
    144      data = createWatcherDataObject(watcherActorID, sessionData);
    145      gAllWatcherData.set(watcherActorID, data);
    146    }
    147    return data;
    148  },
    149 
    150  /**
    151   * Instantiate a DevToolsServerConnection for a given Watcher.
    152   *
    153   * This function will be the one forcing to load the first DevTools CommonJS modules
    154   * and spawning the DevTools Loader as well as the DevToolsServer. So better call it
    155   * only once when it is strictly necessary.
    156   *
    157   * This connection will be the communication channel for RDP between this content process
    158   * and the parent process, which will route RDP packets from/to the client by using
    159   * a unique "forwarding prefix".
    160   *
    161   * @param {string} watcherActorID
    162   * @param {boolean} useDistinctLoader
    163   *        To be set to true when debugging a privileged context running the shared system principal global.
    164   *        This is a requirement for spidermonkey Debugger API used by the thread actor.
    165   * @return {object}
    166   *         Object with connection (DevToolsServerConnection) and loader (DevToolsLoader) attributes.
    167   */
    168  getOrCreateConnectionForWatcher(watcherActorID, useDistinctLoader) {
    169    const watcherDataObject =
    170      ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID);
    171    let { connection, loader } = watcherDataObject;
    172 
    173    if (connection) {
    174      return { connection, loader };
    175    }
    176 
    177    // When debugging a privileged page, like about:addons, this module will run in the same compartment
    178    // as the debugged page. Both will run in the shared system compartment.
    179    // The thread actor ultimately need to be in a distinct compartments from its debuggees.
    180    // So we are using a special loader, which will use a distinct privileged global and compartment
    181    // to load itself as well as all its modules.
    182    //
    183    // Note that when we are running the Browser Toolbox, this module will already be loaded in a special, distinct global and compartment
    184    // Thanks to `loadInDevToolsLoader` flag of BrowserToolboxDevToolsProcess's JS Process Actor configuration.
    185    // So that the Loader will also be loaded in the right, distinct compartment.
    186    loader =
    187      useDistinctLoader || watcherDataObject.sessionContext.type == "all"
    188        ? lazy.useDistinctSystemPrincipalLoader(watcherDataObject)
    189        : lazy.loader;
    190    watcherDataObject.loader = loader;
    191 
    192    // Note that this a key step in loading DevTools backend / modules.
    193    const { DevToolsServer } = loader.require(
    194      "resource://devtools/server/devtools-server.js"
    195    );
    196 
    197    DevToolsServer.init();
    198 
    199    // Within the content process, we only need the target scoped actors.
    200    // (inspector, console, storage,...)
    201    DevToolsServer.registerActors({ target: true });
    202 
    203    // Instantiate a DevToolsServerConnection which will pipe all its outgoing RDP packets
    204    // up to the parent process manager via DevToolsProcess JS Actor messages.
    205    const { forwardingPrefix } = watcherDataObject;
    206    connection = DevToolsServer.connectToParentWindowActor(
    207      watcherDataObject.jsProcessActor,
    208      forwardingPrefix,
    209      "DevToolsProcessChild:packet"
    210    );
    211    watcherDataObject.connection = connection;
    212 
    213    return { connection, loader };
    214  },
    215 
    216  /**
    217   * Method to be called each time a new target actor is instantiated.
    218   *
    219   * @param {object} watcherDataObject
    220   * @param {Actor} targetActor
    221   * @param {boolean} isDocumentCreation
    222   */
    223  onNewTargetActor(watcherDataObject, targetActor, isDocumentCreation = false) {
    224    // There is no root actor in content processes and so
    225    // the target actor can't be managed by it, but we do have to manage
    226    // the actor to have it working and be registered in the DevToolsServerConnection.
    227    // We make it manage itself and become a top level actor.
    228    targetActor.manage(targetActor);
    229 
    230    const { watcherActorID } = watcherDataObject;
    231    targetActor.once("destroyed", options => {
    232      // Maintain the registry and notify the parent process
    233      ContentProcessWatcherRegistry.destroyTargetActor(
    234        watcherDataObject,
    235        targetActor,
    236        options
    237      );
    238    });
    239 
    240    watcherDataObject.actors.push(targetActor);
    241 
    242    // Immediately queue a message for the parent process,
    243    // in order to ensure that the JSWindowActorTransport is instantiated
    244    // before any packet is sent from the content process.
    245    // As messages are guaranteed to be delivered in the order they
    246    // were queued, we don't have to wait for anything around this sendAsyncMessage call.
    247    // In theory, the Target Actor may emit events in its constructor.
    248    // If it does, such RDP packets may be lost. But in practice, no events
    249    // are emitted during its construction. Instead the frontend will start
    250    // the communication first.
    251    const { forwardingPrefix } = watcherDataObject;
    252    watcherDataObject.jsProcessActor.sendAsyncMessage(
    253      "DevToolsProcessChild:targetAvailable",
    254      {
    255        watcherActorID,
    256        forwardingPrefix,
    257        targetActorForm: targetActor.form(),
    258      }
    259    );
    260 
    261    // Pass initialization data to the target actor
    262    const { sessionData } = watcherDataObject;
    263    for (const type in sessionData) {
    264      // `sessionData` will also contain `browserId` as well as entries with empty arrays,
    265      // which shouldn't be processed.
    266      const entries = sessionData[type];
    267      if (!Array.isArray(entries) || !entries.length) {
    268        continue;
    269      }
    270      targetActor.addOrSetSessionDataEntry(
    271        type,
    272        sessionData[type],
    273        isDocumentCreation,
    274        "set"
    275      );
    276    }
    277  },
    278 
    279  /**
    280   * Method to be called each time a target actor is meant to be destroyed.
    281   *
    282   * @param {object} watcherDataObject
    283   * @param {Actor} targetActor
    284   * @param {object} options
    285   * @param {boolean} options.isModeSwitching
    286   *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
    287   */
    288  destroyTargetActor(watcherDataObject, targetActor, options) {
    289    const idx = watcherDataObject.actors.indexOf(targetActor);
    290    if (idx != -1) {
    291      watcherDataObject.actors.splice(idx, 1);
    292    }
    293    const form = targetActor.form();
    294    targetActor.destroy(options);
    295 
    296    // And this will destroy the parent process one
    297    try {
    298      watcherDataObject.jsProcessActor.sendAsyncMessage(
    299        "DevToolsProcessChild:targetDestroyed",
    300        {
    301          actors: [
    302            {
    303              watcherActorID: watcherDataObject.watcherActorID,
    304              targetActorForm: form,
    305            },
    306          ],
    307          options,
    308        }
    309      );
    310    } catch (e) {
    311      // Ignore exception when the JSProcessActorChild has already been destroyed.
    312      // We often try to emit this message while the process is being destroyed,
    313      // but sendAsyncMessage doesn't have time to complete and throws.
    314      if (
    315        !e.message.includes("JSProcessActorChild cannot send at the moment")
    316      ) {
    317        throw e;
    318      }
    319    }
    320  },
    321 
    322  /**
    323   * Method to know if a given Watcher Actor is still registered.
    324   *
    325   * @param {string} watcherActorID
    326   * @return {boolean}
    327   */
    328  has(watcherActorID) {
    329    return gAllWatcherData.has(watcherActorID);
    330  },
    331 
    332  /**
    333   * Method to unregister a given Watcher Actor.
    334   *
    335   * @param {object} watcherDataObject
    336   */
    337  remove(watcherDataObject) {
    338    // We do not need to destroy each actor individually as they
    339    // are all registered in this DevToolsServerConnection, which will
    340    // destroy all the registered actors.
    341    if (watcherDataObject.connection) {
    342      watcherDataObject.connection.close();
    343    }
    344    // If we were using a distinct and dedicated loader,
    345    // we have to manually release it.
    346    if (watcherDataObject.loader && watcherDataObject.loader !== lazy.loader) {
    347      lazy.releaseDistinctSystemPrincipalLoader(watcherDataObject);
    348    }
    349 
    350    if (watcherDataObject.htmlSourcesCache) {
    351      watcherDataObject.htmlSourcesCache.destroy();
    352    }
    353    gAllWatcherData.delete(watcherDataObject.watcherActorID);
    354    if (gAllWatcherData.size == 0) {
    355      gAllWatcherData = null;
    356    }
    357  },
    358 
    359  /**
    360   * Method to know if there is no more Watcher registered.
    361   *
    362   * @return {boolean}
    363   */
    364  isEmpty() {
    365    return !gAllWatcherData || gAllWatcherData.size == 0;
    366  },
    367 
    368  /**
    369   * Method to unregister all the Watcher Actors
    370   */
    371  clear() {
    372    if (!gAllWatcherData) {
    373      return;
    374    }
    375    // Query gAllWatcherData internal map directly as we don't want to re-create the map from sharedData
    376    for (const watcherDataObject of gAllWatcherData.values()) {
    377      ContentProcessWatcherRegistry.remove(watcherDataObject);
    378    }
    379    gAllWatcherData = null;
    380  },
    381 };
    382 
    383 function createWatcherDataObject(watcherActorID, sessionData) {
    384  // The prefix of the DevToolsServerConnection of the Watcher Actor in the parent process.
    385  // This is used to compute a unique ID for this process.
    386  const parentConnectionPrefix = sessionData.connectionPrefix;
    387 
    388  // Compute a unique prefix, just for this DOM Process.
    389  // (nsIDOMProcessChild's childID should be unique across processes)
    390  //
    391  // This prefix will be used to create a JSWindowActorTransport pair between content and parent processes.
    392  // This is slightly hacky as we typically compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`,
    393  // but here, we can't have access to any DevTools connection as we could run really early in the content process startup.
    394  //
    395  // Ensure appending a final slash, otherwise the prefix may be the same between childID 1 and 10...
    396  const forwardingPrefix =
    397    parentConnectionPrefix +
    398    ".process" +
    399    ChromeUtils.domProcessChild.childID +
    400    "/";
    401 
    402  // The browser toolbox uses a distinct JS Actor, loaded in the "devtools" ESM loader.
    403  const jsActorName =
    404    sessionData.sessionContext.type == "all"
    405      ? "BrowserToolboxDevToolsProcess"
    406      : "DevToolsProcess";
    407  const jsProcessActor = ChromeUtils.domProcessChild.getActor(jsActorName);
    408 
    409  return {
    410    // {String}
    411    // Actor ID for this watcher
    412    watcherActorID,
    413 
    414    // {Array<String>}
    415    // List of currently watched target types for this watcher
    416    watchingTargetTypes: [],
    417 
    418    // {DevtoolsServerConnection}
    419    // Connection bridge made from this content process to the parent process.
    420    connection: null,
    421 
    422    // {JSActor}
    423    // Reference to the related DevToolsProcessChild instance.
    424    jsProcessActor,
    425 
    426    // {Object}
    427    // Watcher's sessionContext object, which help identify the browser toolbox usecase.
    428    sessionContext: sessionData.sessionContext,
    429 
    430    // {Object}
    431    // Watcher's sessionData object, which is initiated with `sharedData` version,
    432    // but is later updated on each Session Data update (addOrSetSessionDataEntry/removeSessionDataEntry).
    433    // `sharedData` isn't timely updated and can be out of date.
    434    sessionData,
    435 
    436    // {String}
    437    // Prefix used against all RDP packets to route them correctly from/to this content process
    438    forwardingPrefix,
    439 
    440    // {Array<Object>}
    441    // List of active WindowGlobal and ContentProcess target actor instances.
    442    actors: [],
    443 
    444    // {Object<Array<Object>>}
    445    // We can't use `actors` list for workers as this code runs in the main thread and the WorkerTargetActors
    446    // run in the worker thread.
    447    // We store in each array, specific to each worker type (having a dedicated target watcher class),
    448    // an object with the following attributes:
    449    // - {WorkerDebugger} dbg
    450    // - {String} workerThreadServerForwardingPrefix
    451    // - {Object} workerTargetForm
    452    // - {DevToolsTransport} transport
    453    workers: {
    454      service_worker: [],
    455      shared_worker: [],
    456      worker: [],
    457    },
    458 
    459    // {Object<Set<Array<Object>>>}
    460    // A Set of arrays which will be populated with concurrent Session Data updates
    461    // being done while a worker target is being instantiated.
    462    // Each pending worker being initialized register a new dedicated array which will be removed
    463    // from the Set once its initialization is over.
    464    // We maintain one Set per target type which is managed by a dedicated target watcher class.
    465    pendingWorkers: {
    466      service_worker: new Set(),
    467      shared_worker: new Set(),
    468      worker: new Set(),
    469    },
    470  };
    471 }