tor-browser

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

ParentProcessWatcherRegistry.sys.mjs (15975B)


      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 /**
      6 * Helper module around `sharedData` object that helps storing the state
      7 * of all observed Targets and Resources, that, for all DevTools connections.
      8 * Here is a few words about the C++ implementation of sharedData:
      9 * https://searchfox.org/mozilla-central/rev/bc3600def806859c31b2c7ac06e3d69271052a89/dom/ipc/SharedMap.h#30-55
     10 *
     11 * We may have more than one DevToolsServer and one server may have more than one
     12 * client. This module will be the single source of truth in the parent process,
     13 * in order to know which targets/resources are currently observed. It will also
     14 * be used to declare when something starts/stops being observed.
     15 *
     16 * `sharedData` is a platform API that helps sharing JS Objects across processes.
     17 * We use it in order to communicate to the content process which targets and resources
     18 * should be observed. Content processes read this data only once, as soon as they are created.
     19 * It isn't used beyond this point. Content processes are not going to update it.
     20 * We will notify about changes in observed targets and resources for already running
     21 * processes by some other means. (Via JS Window Actor queries "DevTools:(un)watch(Resources|Target)")
     22 * This means that only this module will update the "DevTools:watchedPerWatcher" value.
     23 * From the parent process, we should be going through this module to fetch the data,
     24 * while from the content process, we will read `sharedData` directly.
     25 */
     26 
     27 const { SessionDataHelpers } = ChromeUtils.importESModule(
     28  "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs",
     29  { global: "contextual" }
     30 );
     31 
     32 const { SUPPORTED_DATA } = SessionDataHelpers;
     33 const SUPPORTED_DATA_TYPES = Object.values(SUPPORTED_DATA);
     34 
     35 // Define the Map that will be saved in `sharedData`.
     36 // It is keyed by WatcherActor ID and values contains following attributes:
     37 // - targets: Set of strings, refering to target types to be listened to
     38 // - resources: Set of strings, refering to resource types to be observed
     39 // - sessionContext Object, The Session Context to help know what is debugged.
     40 //     See devtools/server/actors/watcher/session-context.js
     41 // - connectionPrefix: The DevToolsConnection prefix of the watcher actor. Used to compute new actor ID in the content processes.
     42 //
     43 // Unfortunately, `sharedData` is subject to race condition and may have side effect
     44 // when read/written from multiple places in the same process,
     45 // which is why this map should be considered as the single source of truth.
     46 const sessionDataByWatcherActor = new Map();
     47 
     48 // In parallel to the previous map, keep all the WatcherActor keyed by the same WatcherActor ID,
     49 // the WatcherActor ID. We don't (and can't) propagate the WatcherActor instances to the content
     50 // processes, but still would like to match them by their ID.
     51 const watcherActors = new Map();
     52 
     53 // Name of the attribute into which we save this Map in `sharedData` object.
     54 const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
     55 
     56 /**
     57 * Use `sharedData` to allow processes, early during their creation,
     58 * to know which resources should be listened to. This will be read
     59 * from the Target actor, when it gets created early during process start,
     60 * in order to start listening to the expected resource types.
     61 */
     62 function persistMapToSharedData() {
     63  Services.ppmm.sharedData.set(SHARED_DATA_KEY_NAME, sessionDataByWatcherActor);
     64  // Request to immediately flush the data to the content processes in order to prevent
     65  // races (bug 1644649). Otherwise content process may have outdated sharedData
     66  // and try to create targets for Watcher actor that already stopped watching for targets.
     67  Services.ppmm.sharedData.flush();
     68 }
     69 
     70 export const ParentProcessWatcherRegistry = {
     71  /**
     72   * Tells if a given watcher currently watches for a given target type.
     73   *
     74   * @param WatcherActor watcher
     75   *               The WatcherActor which should be listening.
     76   * @param string targetType
     77   *               The new target type to query.
     78   * @return boolean
     79   *         Returns true if already watching.
     80   */
     81  isWatchingTargets(watcher, targetType) {
     82    const sessionData = this.getSessionData(watcher);
     83    return !!sessionData?.targets?.includes(targetType);
     84  },
     85 
     86  /**
     87   * Retrieve the data saved into `sharedData` that is used to know
     88   * about which type of targets and resources we care listening about.
     89   * `sessionDataByWatcherActor` is saved into `sharedData` after each mutation,
     90   * but `sessionDataByWatcherActor` is the source of truth.
     91   *
     92   * @param WatcherActor watcher
     93   *               The related WatcherActor which starts/stops observing.
     94   * @param object options (optional)
     95   *               A dictionary object with `createData` boolean attribute.
     96   *               If this attribute is set to true, we create the data structure in the Map
     97   *               if none exists for this prefix.
     98   */
     99  getSessionData(watcher, { createData = false } = {}) {
    100    // Use WatcherActor ID as a key as we may have multiple clients willing to watch for targets.
    101    // For example, a Browser Toolbox debugging everything and a Content Toolbox debugging
    102    // just one tab. We might also have multiple watchers, on the same connection when using about:debugging.
    103    const watcherActorID = watcher.actorID;
    104    let sessionData = sessionDataByWatcherActor.get(watcherActorID);
    105    if (!sessionData && createData) {
    106      sessionData = {
    107        // The "session context" object help understand what should be debugged and which target should be created.
    108        // See WatcherActor constructor for more info.
    109        sessionContext: watcher.sessionContext,
    110        // The DevToolsServerConnection prefix will be used to compute actor IDs created in the content process.
    111        // This prefix is unique per watcher, as we may have many watchers for a single connection.
    112        connectionPrefix: watcher.watcherConnectionPrefix,
    113      };
    114      sessionDataByWatcherActor.set(watcherActorID, sessionData);
    115      watcherActors.set(watcherActorID, watcher);
    116    }
    117    return sessionData;
    118  },
    119 
    120  /**
    121   * Given a Watcher Actor ID, return the related Watcher Actor instance.
    122   *
    123   * @param String actorID
    124   *        The Watcher Actor ID to search for.
    125   * @return WatcherActor
    126   *         The Watcher Actor instance.
    127   */
    128  getWatcher(actorID) {
    129    return watcherActors.get(actorID);
    130  },
    131 
    132  /**
    133   * Return an array of the watcher actors that match the passed browserId
    134   *
    135   * @param {number} browserId
    136   * @returns {Array<WatcherActor>} An array of the matching watcher actors
    137   */
    138  getWatchersForBrowserId(browserId) {
    139    const watchers = [];
    140    for (const watcherActor of watcherActors.values()) {
    141      if (
    142        watcherActor.sessionContext.type == "browser-element" &&
    143        watcherActor.sessionContext.browserId === browserId
    144      ) {
    145        watchers.push(watcherActor);
    146      }
    147    }
    148 
    149    return watchers;
    150  },
    151 
    152  /**
    153   * Notify that a given watcher added or set some entries for given data type.
    154   *
    155   * @param WatcherActor watcher
    156   *               The WatcherActor which starts observing.
    157   * @param string type
    158   *               The type of data to be added
    159   * @param Array<Object> entries
    160   *               The values to be added to this type of data
    161   * @param String updateType
    162   *               "add" will only add the new entries in the existing data set.
    163   *               "set" will update the data set with the new entries.
    164   */
    165  addOrSetSessionDataEntry(watcher, type, entries, updateType) {
    166    const sessionData = this.getSessionData(watcher, {
    167      createData: true,
    168    });
    169 
    170    if (!SUPPORTED_DATA_TYPES.includes(type)) {
    171      throw new Error(`Unsupported session data type: ${type}`);
    172    }
    173 
    174    SessionDataHelpers.addOrSetSessionDataEntry(
    175      sessionData,
    176      type,
    177      entries,
    178      updateType
    179    );
    180 
    181    // Flush sharedData before registering the JS Actors as it is used
    182    // during their instantiation.
    183    persistMapToSharedData();
    184 
    185    // Register the JS Window Actor the first time we start watching for something (e.g. resource, target, …).
    186    if (watcher.sessionContext.type == "all") {
    187      registerBrowserToolboxJSProcessActor();
    188    } else {
    189      registerJSProcessActor();
    190    }
    191  },
    192 
    193  /**
    194   * Notify that a given watcher removed an entry in a given data type.
    195   *
    196   * @param WatcherActor watcher
    197   *               The WatcherActor which stops observing.
    198   * @param string type
    199   *               The type of data to be removed
    200   * @param Array<Object> entries
    201   *               The values to be removed to this type of data
    202   *
    203   * @return boolean
    204   *         True if we such entry was already registered, for this watcher actor.
    205   */
    206  removeSessionDataEntry(watcher, type, entries) {
    207    const sessionData = this.getSessionData(watcher);
    208    if (!sessionData) {
    209      return false;
    210    }
    211 
    212    if (!SUPPORTED_DATA_TYPES.includes(type)) {
    213      throw new Error(`Unsupported session data type: ${type}`);
    214    }
    215 
    216    if (
    217      !SessionDataHelpers.removeSessionDataEntry(sessionData, type, entries)
    218    ) {
    219      return false;
    220    }
    221 
    222    persistMapToSharedData();
    223 
    224    return true;
    225  },
    226 
    227  /**
    228   * Cleanup everything about a given watcher actor.
    229   * Remove it from any registry so that we stop interacting with it.
    230   *
    231   * The watcher would be automatically unregistered from removeWatcherEntry,
    232   * if we remove all entries. But we aren't removing all breakpoints.
    233   * So here, we force clearing any reference to the watcher actor when it destroys.
    234   */
    235  unregisterWatcher(watcherActorID) {
    236    sessionDataByWatcherActor.delete(watcherActorID);
    237    watcherActors.delete(watcherActorID);
    238    this.maybeUnregisterJSActors();
    239  },
    240 
    241  /**
    242   * Unregister the JS Actors if there is no more DevTools code observing any target/resource.
    243   */
    244  maybeUnregisterJSActors() {
    245    if (sessionDataByWatcherActor.size == 0) {
    246      unregisterBrowserToolboxJSProcessActor();
    247      unregisterJSProcessActor();
    248    }
    249  },
    250 
    251  /**
    252   * Notify that a given watcher starts observing a new target type.
    253   *
    254   * @param WatcherActor watcher
    255   *               The WatcherActor which starts observing.
    256   * @param string targetType
    257   *               The new target type to start listening to.
    258   */
    259  watchTargets(watcher, targetType) {
    260    this.addOrSetSessionDataEntry(
    261      watcher,
    262      SUPPORTED_DATA.TARGETS,
    263      [targetType],
    264      "add"
    265    );
    266  },
    267 
    268  /**
    269   * Notify that a given watcher stops observing a given target type.
    270   *
    271   * @param WatcherActor watcher
    272   *               The WatcherActor which stops observing.
    273   * @param string targetType
    274   *               The new target type to stop listening to.
    275   * @return boolean
    276   *         True if we were watching for this target type, for this watcher actor.
    277   */
    278  unwatchTargets(watcher, targetType) {
    279    return this.removeSessionDataEntry(watcher, SUPPORTED_DATA.TARGETS, [
    280      targetType,
    281    ]);
    282  },
    283 
    284  /**
    285   * Notify that a given watcher starts observing new resource types.
    286   *
    287   * @param WatcherActor watcher
    288   *               The WatcherActor which starts observing.
    289   * @param Array<string> resourceTypes
    290   *               The new resource types to start listening to.
    291   */
    292  watchResources(watcher, resourceTypes) {
    293    this.addOrSetSessionDataEntry(
    294      watcher,
    295      SUPPORTED_DATA.RESOURCES,
    296      resourceTypes,
    297      "add"
    298    );
    299  },
    300 
    301  /**
    302   * Notify that a given watcher stops observing given resource types.
    303   *
    304   * See `watchResources` for argument definition.
    305   *
    306   * @return boolean
    307   *         True if we were watching for this resource type, for this watcher actor.
    308   */
    309  unwatchResources(watcher, resourceTypes) {
    310    return this.removeSessionDataEntry(
    311      watcher,
    312      SUPPORTED_DATA.RESOURCES,
    313      resourceTypes
    314    );
    315  },
    316 };
    317 
    318 // Boolean flag to know if the DevToolsProcess JS Process Actor is currently registered
    319 let isJSProcessActorRegistered = false;
    320 let isBrowserToolboxJSProcessActorRegistered = false;
    321 
    322 const JSProcessActorConfig = {
    323  parent: {
    324    esModuleURI:
    325      "resource://devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs",
    326  },
    327  child: {
    328    esModuleURI:
    329      "resource://devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs",
    330    // There is no good observer service notification we can listen to to instantiate the JSProcess Actor
    331    // reliably as soon as the process start.
    332    // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification...
    333    observers: ["init-devtools-content-process-actor"],
    334  },
    335  // The parent process is handled very differently from content processes
    336  // This uses the ParentProcessTarget which inherits from BrowsingContextTarget
    337  // and is, for now, manually created by the descriptor as the top level target.
    338  includeParent: true,
    339 };
    340 
    341 const BrowserToolboxJSProcessActorConfig = {
    342  ...JSProcessActorConfig,
    343 
    344  // This JS Process Actor is used to bootstrap DevTools code debugging the privileged code
    345  // in content processes. The privileged code runs in the "shared JSM global" (See mozJSModuleLoader).
    346  // DevTools modules should be loaded in a distinct global in order to be able to debug this privileged code.
    347  // There is a strong requirement in spidermonkey for the debuggee and debugger to be using distinct compartments.
    348  // This flag will force both parent and child modules to be loaded via a dedicated loader (See mozJSModuleLoader::GetOrCreateDevToolsLoader)
    349  //
    350  // Note that as a side effect, it makes these modules and all their dependencies to be invisible to the debugger.
    351  loadInDevToolsLoader: true,
    352 };
    353 
    354 const PROCESS_SCRIPT_URL =
    355  "resource://devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js";
    356 
    357 function registerJSProcessActor() {
    358  if (isJSProcessActorRegistered) {
    359    return;
    360  }
    361  isJSProcessActorRegistered = true;
    362  ChromeUtils.registerProcessActor("DevToolsProcess", JSProcessActorConfig);
    363 
    364  // There is no good observer service notification we can listen to to instantiate the JSProcess Actor
    365  // as soon as the process start.
    366  // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification...
    367  Services.ppmm.loadProcessScript(PROCESS_SCRIPT_URL, true);
    368 }
    369 
    370 function registerBrowserToolboxJSProcessActor() {
    371  if (isBrowserToolboxJSProcessActorRegistered) {
    372    return;
    373  }
    374  isBrowserToolboxJSProcessActorRegistered = true;
    375  ChromeUtils.registerProcessActor(
    376    "BrowserToolboxDevToolsProcess",
    377    BrowserToolboxJSProcessActorConfig
    378  );
    379 
    380  // There is no good observer service notification we can listen to to instantiate the JSProcess Actor
    381  // as soon as the process start.
    382  // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification...
    383  Services.ppmm.loadProcessScript(PROCESS_SCRIPT_URL, true);
    384 }
    385 
    386 function unregisterJSProcessActor() {
    387  if (!isJSProcessActorRegistered) {
    388    return;
    389  }
    390  isJSProcessActorRegistered = false;
    391  try {
    392    ChromeUtils.unregisterProcessActor("DevToolsProcess");
    393  } catch (e) {
    394    // If any pending query was still ongoing, this would throw
    395  }
    396  if (isBrowserToolboxJSProcessActorRegistered) {
    397    return;
    398  }
    399  Services.ppmm.removeDelayedProcessScript(PROCESS_SCRIPT_URL);
    400 }
    401 
    402 function unregisterBrowserToolboxJSProcessActor() {
    403  if (!isBrowserToolboxJSProcessActorRegistered) {
    404    return;
    405  }
    406  isBrowserToolboxJSProcessActorRegistered = false;
    407  try {
    408    ChromeUtils.unregisterProcessActor("BrowserToolboxDevToolsProcess");
    409  } catch (e) {
    410    // If any pending query was still ongoing, this would throw
    411  }
    412  if (isJSProcessActorRegistered) {
    413    return;
    414  }
    415  Services.ppmm.removeDelayedProcessScript(PROCESS_SCRIPT_URL);
    416 }