tor-browser

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

content-process-script.js (9687B)


      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 "use strict";
      6 
      7 /**
      8 * Main entry point for DevTools in content processes.
      9 *
     10 * This module is loaded early when a content process is started.
     11 * Note that (at least) JS XPCOM registered at app-startup, will be running before.
     12 * It is used by the multiprocess browser toolbox in order to debug privileged resources.
     13 * When debugging a Web page loaded in a Tab, DevToolsFrame JS Window Actor is used instead
     14 * (DevToolsFrameParent.jsm and DevToolsFrameChild.jsm).
     15 *
     16 * This module won't do anything unless DevTools codebase starts adding some data
     17 * in `Services.cpmm.sharedData` object or send a message manager message via `Services.cpmm`.
     18 * Also, this module is only loaded, on-demand from process-helper if devtools are watching for process targets.
     19 */
     20 
     21 const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
     22 
     23 class ContentProcessStartup {
     24  constructor() {
     25    // The map is indexed by the Watcher Actor ID.
     26    // The values are objects containing the following properties:
     27    // - connection: the DevToolsServerConnection itself
     28    // - actor: the ContentProcessTargetActor instance
     29    this._connections = new Map();
     30 
     31    this.observe = this.observe.bind(this);
     32    this.receiveMessage = this.receiveMessage.bind(this);
     33 
     34    this.addListeners();
     35    this.maybeCreateExistingTargetActors();
     36  }
     37 
     38  observe(subject, topic) {
     39    switch (topic) {
     40      case "xpcom-shutdown": {
     41        this.destroy();
     42        break;
     43      }
     44    }
     45  }
     46 
     47  destroy(options) {
     48    this.removeListeners();
     49 
     50    for (const [, connectionInfo] of this._connections) {
     51      connectionInfo.connection.close(options);
     52    }
     53    this._connections.clear();
     54  }
     55 
     56  addListeners() {
     57    Services.obs.addObserver(this.observe, "xpcom-shutdown");
     58 
     59    Services.cpmm.addMessageListener(
     60      "debug:instantiate-already-available",
     61      this.receiveMessage
     62    );
     63    Services.cpmm.addMessageListener(
     64      "debug:destroy-target",
     65      this.receiveMessage
     66    );
     67    Services.cpmm.addMessageListener(
     68      "debug:add-or-set-session-data-entry",
     69      this.receiveMessage
     70    );
     71    Services.cpmm.addMessageListener(
     72      "debug:remove-session-data-entry",
     73      this.receiveMessage
     74    );
     75    Services.cpmm.addMessageListener(
     76      "debug:destroy-process-script",
     77      this.receiveMessage
     78    );
     79  }
     80 
     81  removeListeners() {
     82    Services.obs.removeObserver(this.observe, "xpcom-shutdown");
     83 
     84    Services.cpmm.removeMessageListener(
     85      "debug:instantiate-already-available",
     86      this.receiveMessage
     87    );
     88    Services.cpmm.removeMessageListener(
     89      "debug:destroy-target",
     90      this.receiveMessage
     91    );
     92    Services.cpmm.removeMessageListener(
     93      "debug:add-or-set-session-data-entry",
     94      this.receiveMessage
     95    );
     96    Services.cpmm.removeMessageListener(
     97      "debug:remove-session-data-entry",
     98      this.receiveMessage
     99    );
    100    Services.cpmm.removeMessageListener(
    101      "debug:destroy-process-script",
    102      this.receiveMessage
    103    );
    104  }
    105 
    106  receiveMessage(msg) {
    107    switch (msg.name) {
    108      case "debug:instantiate-already-available":
    109        this.createTargetActor(
    110          msg.data.watcherActorID,
    111          msg.data.connectionPrefix,
    112          msg.data.sessionData,
    113          true
    114        );
    115        break;
    116      case "debug:destroy-target":
    117        this.destroyTarget(msg.data.watcherActorID);
    118        break;
    119      case "debug:add-or-set-session-data-entry":
    120        this.addOrSetSessionDataEntry(
    121          msg.data.watcherActorID,
    122          msg.data.type,
    123          msg.data.entries,
    124          msg.data.updateType
    125        );
    126        break;
    127      case "debug:remove-session-data-entry":
    128        this.removeSessionDataEntry(
    129          msg.data.watcherActorID,
    130          msg.data.type,
    131          msg.data.entries
    132        );
    133        break;
    134      case "debug:destroy-process-script":
    135        this.destroy(msg.data.options);
    136        break;
    137      default:
    138        throw new Error(`Unsupported message name ${msg.name}`);
    139    }
    140  }
    141 
    142  /**
    143   * Called when the content process just started.
    144   * This will start creating ContentProcessTarget actors, but only if DevTools code (WatcherActor / ParentProcessWatcherRegistry.sys.mjs)
    145   * put some data in `sharedData` telling us to do so.
    146   */
    147  maybeCreateExistingTargetActors() {
    148    const { sharedData } = Services.cpmm;
    149 
    150    // Accessing `sharedData` right off the app-startup returns null.
    151    // Spinning the event loop with dispatchToMainThread seems enough,
    152    // but it means that we let some more Javascript code run before
    153    // instantiating the target actor.
    154    // So we may miss a few resources and will register the breakpoints late.
    155    if (!sharedData) {
    156      Services.tm.dispatchToMainThread(
    157        this.maybeCreateExistingTargetActors.bind(this)
    158      );
    159      return;
    160    }
    161 
    162    const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
    163    if (!sessionDataByWatcherActor) {
    164      return;
    165    }
    166 
    167    // Create one Target actor for each prefix/client which listen to process
    168    for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
    169      const { connectionPrefix, targets } = sessionData;
    170      // This is where we only do something significant only if DevTools are opened
    171      // and requesting to create target actor for content processes
    172      if (targets?.includes("process")) {
    173        this.createTargetActor(watcherActorID, connectionPrefix, sessionData);
    174      }
    175    }
    176  }
    177 
    178  /**
    179   * Instantiate a new ContentProcessTarget for the given connection.
    180   * This is where we start doing some significant computation that only occurs when DevTools are opened.
    181   *
    182   * @param String watcherActorID
    183   *        The ID of the WatcherActor who requested to observe and create these target actors.
    184   * @param String parentConnectionPrefix
    185   *        The prefix of the DevToolsServerConnection of the Watcher Actor.
    186   *        This is used to compute a unique ID for the target actor.
    187   * @param Object sessionData
    188   *        All data managed by the Watcher Actor and ParentProcessWatcherRegistry.jsm, containing
    189   *        target types, resources types to be listened as well as breakpoints and any
    190   *        other data meant to be shared across processes and threads.
    191   * @param Object options Dictionary with optional values:
    192   * @param Boolean options.ignoreAlreadyCreated
    193   *        If true, do not throw if the target actor has already been created.
    194   */
    195  createTargetActor(
    196    watcherActorID,
    197    parentConnectionPrefix,
    198    sessionData,
    199    ignoreAlreadyCreated = false
    200  ) {
    201    if (this._connections.get(watcherActorID)) {
    202      if (ignoreAlreadyCreated) {
    203        return;
    204      }
    205      throw new Error(
    206        "ContentProcessStartup createTargetActor was called more than once" +
    207          ` for the Watcher Actor (ID: "${watcherActorID}")`
    208      );
    209    }
    210    // Compute a unique prefix, just for this content process,
    211    // which will be used to create a ChildDebuggerTransport pair between content and parent processes.
    212    // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`,
    213    // but here, we can't have access to any DevTools connection as we are really early in the content process startup
    214    const prefix =
    215      parentConnectionPrefix + "contentProcess" + Services.appinfo.processID;
    216    //TODO: probably merge content-process.jsm with this module
    217    const { initContentProcessTarget } = ChromeUtils.importESModule(
    218      "resource://devtools/server/startup/content-process.sys.mjs"
    219    );
    220    const { actor, connection } = initContentProcessTarget({
    221      target: Services.cpmm,
    222      data: {
    223        watcherActorID,
    224        parentConnectionPrefix,
    225        prefix,
    226        sessionContext: sessionData.sessionContext,
    227      },
    228    });
    229    this._connections.set(watcherActorID, {
    230      actor,
    231      connection,
    232    });
    233 
    234    // Pass initialization data to the target actor
    235    for (const type in sessionData) {
    236      actor.addOrSetSessionDataEntry(type, sessionData[type], false, "set");
    237    }
    238  }
    239 
    240  destroyTarget(watcherActorID) {
    241    const connectionInfo = this._connections.get(watcherActorID);
    242    // This connection has already been cleaned?
    243    if (!connectionInfo) {
    244      throw new Error(
    245        `Trying to destroy a content process target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
    246      );
    247    }
    248    connectionInfo.connection.close();
    249    this._connections.delete(watcherActorID);
    250  }
    251 
    252  async addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) {
    253    const connectionInfo = this._connections.get(watcherActorID);
    254    if (!connectionInfo) {
    255      throw new Error(
    256        `No content process target actor for this Watcher Actor ID:"${watcherActorID}"`
    257      );
    258    }
    259    const { actor } = connectionInfo;
    260    await actor.addOrSetSessionDataEntry(type, entries, false, updateType);
    261    Services.cpmm.sendAsyncMessage("debug:add-or-set-session-data-entry-done", {
    262      watcherActorID,
    263    });
    264  }
    265 
    266  removeSessionDataEntry(watcherActorID, type, entries) {
    267    const connectionInfo = this._connections.get(watcherActorID);
    268    if (!connectionInfo) {
    269      return;
    270    }
    271    const { actor } = connectionInfo;
    272    actor.removeSessionDataEntry(type, entries);
    273  }
    274 }
    275 
    276 // Only start this component for content processes.
    277 // i.e. explicitely avoid running it for the parent process
    278 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
    279  new ContentProcessStartup();
    280 }