tor-browser

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

DevToolsProcessChild.sys.mjs (19014B)


      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 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
      6 import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs";
      7 
      8 const lazy = {};
      9 ChromeUtils.defineESModuleGetters(
     10  lazy,
     11  {
     12    ContentScriptTargetWatcher:
     13      "resource://devtools/server/connectors/js-process-actor/target-watchers/content_script.sys.mjs",
     14    ProcessTargetWatcher:
     15      "resource://devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs",
     16    SessionDataHelpers:
     17      "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs",
     18    ServiceWorkerTargetWatcher:
     19      "resource://devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs",
     20    SharedWorkerTargetWatcher:
     21      "resource://devtools/server/connectors/js-process-actor/target-watchers/shared_worker.sys.mjs",
     22    WorkerTargetWatcher:
     23      "resource://devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs",
     24    WindowGlobalTargetWatcher:
     25      "resource://devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs",
     26  },
     27  { global: "contextual" }
     28 );
     29 
     30 // TargetActorRegistery has to be shared between all devtools instances
     31 // and so is loaded into the shared global.
     32 ChromeUtils.defineESModuleGetters(
     33  lazy,
     34  {
     35    TargetActorRegistry:
     36      "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
     37  },
     38  { global: "shared" }
     39 );
     40 
     41 export class DevToolsProcessChild extends JSProcessActorChild {
     42  constructor() {
     43    super();
     44 
     45    // The EventEmitter interface is used for DevToolsTransport's packet-received event.
     46    EventEmitter.decorate(this);
     47  }
     48 
     49  #watchers = {
     50    // Keys are target types, which are defined in this CommonJS Module:
     51    // https://searchfox.org/mozilla-central/rev/0e9ea50a999420d93df0e4e27094952af48dd3b8/devtools/server/actors/targets/index.js#7-14
     52    // We avoid loading it as this ESM should be lightweight and avoid spawning DevTools CommonJS Loader until
     53    // whe know we have to instantiate a Target Actor.
     54    frame: {
     55      // Number of active watcher actors currently watching for the given target type
     56      activeListener: 0,
     57 
     58      // Instance of a target watcher class whose task is to observe new target instances
     59      get watcher() {
     60        return lazy.WindowGlobalTargetWatcher;
     61      },
     62    },
     63 
     64    process: {
     65      activeListener: 0,
     66      get watcher() {
     67        return lazy.ProcessTargetWatcher;
     68      },
     69    },
     70 
     71    worker: {
     72      activeListener: 0,
     73      get watcher() {
     74        return lazy.WorkerTargetWatcher;
     75      },
     76    },
     77 
     78    service_worker: {
     79      activeListener: 0,
     80      get watcher() {
     81        return lazy.ServiceWorkerTargetWatcher;
     82      },
     83    },
     84 
     85    shared_worker: {
     86      activeListener: 0,
     87      get watcher() {
     88        return lazy.SharedWorkerTargetWatcher;
     89      },
     90    },
     91 
     92    content_script: {
     93      activeListener: 0,
     94      get watcher() {
     95        return lazy.ContentScriptTargetWatcher;
     96      },
     97    },
     98  };
     99 
    100  #initialized = false;
    101 
    102  /**
    103   * Called when this JSProcess Actor instantiate either when we start observing for first target types,
    104   * or when the process just started.
    105   */
    106  instantiate() {
    107    if (this.#initialized) {
    108      return;
    109    }
    110    this.#initialized = true;
    111    // Create and watch for future target actors for each watcher currently watching some target types
    112    for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) {
    113      this.#watchInitialTargetsForWatcher(watcherDataObject);
    114    }
    115  }
    116 
    117  /**
    118   * Instantiate and watch future target actors based on the already watched targets.
    119   *
    120   * @param Object watcherDataObject
    121   *        See ContentProcessWatcherRegistry.
    122   */
    123  #watchInitialTargetsForWatcher(watcherDataObject) {
    124    const { sessionData, sessionContext } = watcherDataObject;
    125 
    126    // About WebExtension, see note in addOrSetSessionDataEntry.
    127    // Their target actor aren't created by this class, but session data is still managed by it
    128    // and we need to pass the initial session data coming to already instantiated target actor.
    129    if (sessionContext.type == "webextension") {
    130      const { watcherActorID } = watcherDataObject;
    131      const connectionPrefix = watcherActorID.replace(/watcher\d+$/, "");
    132      const targetActors = lazy.TargetActorRegistry.getTargetActors(
    133        sessionContext,
    134        connectionPrefix
    135      );
    136      if (targetActors.length) {
    137        // Pass initialization data to the target actor
    138        for (const type in sessionData) {
    139          // `sessionData` will also contain `browserId` as well as entries with empty arrays,
    140          // which shouldn't be processed.
    141          const entries = sessionData[type];
    142          if (!Array.isArray(entries) || !entries.length) {
    143            continue;
    144          }
    145          targetActors[0].addOrSetSessionDataEntry(type, entries, false, "set");
    146        }
    147      }
    148    }
    149 
    150    // Ignore the call if the watched targets property isn't populated yet.
    151    // This typically happens when instantiating the JS Process Actor on toolbox opening,
    152    // where the actor is spawn early and a watchTarget message comes later with the `targets` array set.
    153    if (!sessionData.targets) {
    154      return;
    155    }
    156 
    157    for (const targetType of sessionData.targets) {
    158      this.#watchNewTargetTypeForWatcher(watcherDataObject, targetType, true);
    159    }
    160  }
    161 
    162  /**
    163   * Instantiate and watch future target actors based on the already watched targets.
    164   *
    165   * @param Object watcherDataObject
    166   *        See ContentProcessWatcherRegistry.
    167   * @param String targetType
    168   *        New typeof target to start watching.
    169   * @param Boolean isProcessActorStartup
    170   *        True when we are watching for targets during this JS Process actor instantiation.
    171   *        It shouldn't be the case on toolbox opening, but only when a new process starts.
    172   *        On toolbox opening, the Actor will receive an explicit watchTargets query.
    173   */
    174  #watchNewTargetTypeForWatcher(
    175    watcherDataObject,
    176    targetType,
    177    isProcessActorStartup
    178  ) {
    179    const { watchingTargetTypes } = watcherDataObject;
    180    // Ensure creating and watching only once per target type and watcher actor.
    181    if (watchingTargetTypes.includes(targetType)) {
    182      return;
    183    }
    184    watchingTargetTypes.push(targetType);
    185 
    186    // Update sessionData as watched target types are a Session Data
    187    // used later for example by worker target watcher
    188    lazy.SessionDataHelpers.addOrSetSessionDataEntry(
    189      watcherDataObject.sessionData,
    190      "targets",
    191      [targetType],
    192      "add"
    193    );
    194 
    195    this.#watchers[targetType].activeListener++;
    196 
    197    // Start listening for platform events when we are observing this type for the first time
    198    if (this.#watchers[targetType].activeListener === 1) {
    199      this.#watchers[targetType].watcher.watch();
    200    }
    201 
    202    // And instantiate targets for the already existing instances
    203    this.#watchers[targetType].watcher.createTargetsForWatcher(
    204      watcherDataObject,
    205      isProcessActorStartup
    206    );
    207  }
    208 
    209  /**
    210   * Stop watching for all target types and destroy all existing targets actor
    211   * related to a given watcher actor.
    212   *
    213   * @param {object} watcherDataObject
    214   * @param {string} targetType
    215   * @param {object} options
    216   */
    217  #unwatchTargetsForWatcher(watcherDataObject, targetType, options) {
    218    const { watchingTargetTypes } = watcherDataObject;
    219    const targetTypeIndex = watchingTargetTypes.indexOf(targetType);
    220    // Ignore targetTypes which were not observed
    221    if (targetTypeIndex === -1) {
    222      return;
    223    }
    224    // Update to the new list of currently watched target types
    225    watchingTargetTypes.splice(targetTypeIndex, 1);
    226 
    227    // Update sessionData as watched target types are a Session Data
    228    // used later for example by worker target watcher
    229    lazy.SessionDataHelpers.removeSessionDataEntry(
    230      watcherDataObject.sessionData,
    231      "targets",
    232      [targetType]
    233    );
    234 
    235    this.#watchers[targetType].activeListener--;
    236 
    237    // Stop observing for platform events
    238    if (this.#watchers[targetType].activeListener === 0) {
    239      this.#watchers[targetType].watcher.unwatch();
    240    }
    241 
    242    // Destroy all targets which are still instantiated for this type
    243    this.#watchers[targetType].watcher.destroyTargetsForWatcher(
    244      watcherDataObject,
    245      options
    246    );
    247 
    248    // Unregister the watcher if we stopped watching for all target types
    249    if (!watchingTargetTypes.length) {
    250      ContentProcessWatcherRegistry.remove(watcherDataObject);
    251    }
    252 
    253    // If we removed the last watcher, clean the internal state of this class.
    254    if (ContentProcessWatcherRegistry.isEmpty()) {
    255      this.didDestroy(options);
    256    }
    257  }
    258 
    259  /**
    260   * Cleanup everything around a given watcher actor
    261   *
    262   * @param {object} watcherDataObject
    263   */
    264  #destroyWatcher(watcherDataObject) {
    265    const { watchingTargetTypes } = watcherDataObject;
    266    // Clone the array as it will be modified during the loop execution
    267    for (const targetType of [...watchingTargetTypes]) {
    268      this.#unwatchTargetsForWatcher(watcherDataObject, targetType);
    269    }
    270  }
    271 
    272  /**
    273   * Used by DevTools Transport to send packets to the content process.
    274   *
    275   * @param {JSON} packet
    276   * @param {string} prefix
    277   */
    278  sendPacket(packet, prefix) {
    279    this.sendAsyncMessage("DevToolsProcessChild:packet", { packet, prefix });
    280  }
    281 
    282  /**
    283   * JsWindowActor API
    284   */
    285 
    286  async sendQuery(msg, args) {
    287    try {
    288      const res = await super.sendQuery(msg, args);
    289      return res;
    290    } catch (e) {
    291      console.error("Failed to sendQuery in DevToolsProcessChild", msg);
    292      console.error(e.toString());
    293      throw e;
    294    }
    295  }
    296 
    297  /**
    298   * Called by the JSProcessActor API when the process process sent us a message.
    299   */
    300  receiveMessage(message) {
    301    switch (message.name) {
    302      case "DevToolsProcessParent:watchTargets": {
    303        const { watcherActorID, targetType } = message.data;
    304        const watcherDataObject =
    305          ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID);
    306        return this.#watchNewTargetTypeForWatcher(
    307          watcherDataObject,
    308          targetType
    309        );
    310      }
    311      case "DevToolsProcessParent:unwatchTargets": {
    312        const { watcherActorID, targetType, options } = message.data;
    313        const watcherDataObject =
    314          ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID);
    315        return this.#unwatchTargetsForWatcher(
    316          watcherDataObject,
    317          targetType,
    318          options
    319        );
    320      }
    321      case "DevToolsProcessParent:addOrSetSessionDataEntry": {
    322        const { watcherActorID, type, entries, updateType } = message.data;
    323        return this.#addOrSetSessionDataEntry(
    324          watcherActorID,
    325          type,
    326          entries,
    327          updateType
    328        );
    329      }
    330      case "DevToolsProcessParent:removeSessionDataEntry": {
    331        const { watcherActorID, type, entries } = message.data;
    332        return this.#removeSessionDataEntry(watcherActorID, type, entries);
    333      }
    334      case "DevToolsProcessParent:destroyWatcher": {
    335        const { watcherActorID } = message.data;
    336        const watcherDataObject =
    337          ContentProcessWatcherRegistry.getWatcherDataObject(
    338            watcherActorID,
    339            true
    340          );
    341        // The watcher may already be destroyed if the client unwatched for all target types.
    342        if (watcherDataObject) {
    343          return this.#destroyWatcher(watcherDataObject);
    344        }
    345        return null;
    346      }
    347      case "DevToolsProcessParent:packet":
    348        return this.emit("packet-received", message);
    349      default:
    350        throw new Error(
    351          "Unsupported message in DevToolsProcessParent: " + message.name
    352        );
    353    }
    354  }
    355 
    356  /**
    357   * The parent process requested that some session data have been added or set.
    358   *
    359   * @param {string} watcherActorID
    360   *        The Watcher Actor ID requesting to add new session data
    361   * @param {string} type
    362   *        The type of data to be added
    363   * @param {Array<object>} entries
    364   *        The values to be added to this type of data
    365   * @param {string} updateType
    366   *        "add" will only add the new entries in the existing data set.
    367   *        "set" will update the data set with the new entries.
    368   */
    369  async #addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) {
    370    const watcherDataObject =
    371      ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID);
    372 
    373    // Maintain the copy of `sessionData` so that it is up-to-date when
    374    // a new worker target needs to be instantiated
    375    const { sessionData } = watcherDataObject;
    376    lazy.SessionDataHelpers.addOrSetSessionDataEntry(
    377      sessionData,
    378      type,
    379      entries,
    380      updateType
    381    );
    382 
    383    // This type is really specific to Service Workers and doesn't need to be transferred to any target.
    384    // We only need to instantiate and destroy the target actors based on this new host.
    385    const { watchingTargetTypes } = watcherDataObject;
    386    if (type == "browser-element-host") {
    387      if (watchingTargetTypes.includes("service_worker")) {
    388        this.#watchers.service_worker.watcher.updateBrowserElementHost(
    389          watcherDataObject
    390        );
    391      }
    392      return;
    393    }
    394 
    395    const promises = [];
    396    for (const targetActor of watcherDataObject.actors) {
    397      promises.push(
    398        targetActor.addOrSetSessionDataEntry(type, entries, false, updateType)
    399      );
    400    }
    401 
    402    // Very special codepath for Web Extensions.
    403    // Their WebExtension Target Actor is still created manually by WebExtensionDescritpor.getTarget,
    404    // via a message manager. That, instead of being instantiated via the WatcherActor.watchTargets and this JSProcess actor.
    405    // The Watcher Actor will still instantiate a JS Actor for the WebExt DOM Content Process
    406    // and send the addOrSetSessionDataEntry query. But as the target actor isn't managed by the JS Actor,
    407    // we have to manually retrieve it via the TargetActorRegistry.
    408    if (sessionData.sessionContext.type == "webextension") {
    409      const connectionPrefix = watcherActorID.replace(/watcher\d+$/, "");
    410      const targetActors = lazy.TargetActorRegistry.getTargetActors(
    411        sessionData.sessionContext,
    412        connectionPrefix
    413      );
    414      // We will have a single match only in the DOM Process where the add-on runs
    415      if (targetActors.length) {
    416        promises.push(
    417          targetActors[0].addOrSetSessionDataEntry(
    418            type,
    419            entries,
    420            false,
    421            updateType
    422          )
    423        );
    424      }
    425    }
    426    await Promise.all(promises);
    427 
    428    if (watchingTargetTypes.includes("worker")) {
    429      await this.#watchers.worker.watcher.addOrSetSessionDataEntry(
    430        watcherDataObject,
    431        type,
    432        entries,
    433        updateType
    434      );
    435    }
    436    if (watchingTargetTypes.includes("service_worker")) {
    437      await this.#watchers.service_worker.watcher.addOrSetSessionDataEntry(
    438        watcherDataObject,
    439        type,
    440        entries,
    441        updateType
    442      );
    443    }
    444    if (watchingTargetTypes.includes("shared_worker")) {
    445      await this.#watchers.shared_worker.watcher.addOrSetSessionDataEntry(
    446        watcherDataObject,
    447        type,
    448        entries,
    449        updateType
    450      );
    451    }
    452  }
    453 
    454  /**
    455   * The parent process requested that some session data have been removed.
    456   *
    457   * @param {string} watcherActorID
    458   *        The Watcher Actor ID requesting to remove session data
    459   * @param {string}} type
    460   *        The type of data to be removed
    461   * @param {Array<object>} entries
    462   *        The values to be removed to this type of data
    463   */
    464  #removeSessionDataEntry(watcherActorID, type, entries) {
    465    const watcherDataObject =
    466      ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID, true);
    467 
    468    // When we unwatch resources after targets during the devtools shutdown,
    469    // the watcher will be removed on last target type unwatch.
    470    if (!watcherDataObject) {
    471      return;
    472    }
    473    const { actors, sessionData, watchingTargetTypes } = watcherDataObject;
    474 
    475    // Maintain the copy of `sessionData` so that it is up-to-date when
    476    // a new worker target needs to be instantiated
    477    lazy.SessionDataHelpers.removeSessionDataEntry(sessionData, type, entries);
    478 
    479    for (const targetActor of actors) {
    480      targetActor.removeSessionDataEntry(type, entries);
    481    }
    482 
    483    // Special code paths for webextension toolboxes and worker targets
    484    // See addOrSetSessionDataEntry for more details.
    485 
    486    if (sessionData.sessionContext.type == "webextension") {
    487      const connectionPrefix = watcherActorID.replace(/watcher\d+$/, "");
    488      const targetActors = lazy.TargetActorRegistry.getTargetActors(
    489        sessionData.sessionContext,
    490        connectionPrefix
    491      );
    492      if (targetActors.length) {
    493        targetActors[0].removeSessionDataEntry(type, entries);
    494      }
    495    }
    496 
    497    if (watchingTargetTypes.includes("worker")) {
    498      this.#watchers.worker.watcher.removeSessionDataEntry(
    499        watcherDataObject,
    500        type,
    501        entries
    502      );
    503    }
    504    if (watchingTargetTypes.includes("service_worker")) {
    505      this.#watchers.service_worker.watcher.removeSessionDataEntry(
    506        watcherDataObject,
    507        type,
    508        entries
    509      );
    510    }
    511    if (watchingTargetTypes.includes("shared_worker")) {
    512      this.#watchers.shared_worker.watcher.removeSessionDataEntry(
    513        watcherDataObject,
    514        type,
    515        entries
    516      );
    517    }
    518  }
    519 
    520  /**
    521   * Observer service notification handler.
    522   *
    523   * @param {DOMWindow|Document} subject
    524   *        A window for *-document-global-created
    525   *        A document for *-page-{shown|hide}
    526   * @param {string} topic
    527   */
    528  observe = (subject, topic) => {
    529    if (topic === "init-devtools-content-process-actor") {
    530      // This is triggered by the process actor registration and some code in process-helper.js
    531      // which defines a unique topic to be observed
    532      this.instantiate();
    533    }
    534  };
    535 
    536  /**
    537   * Called by JS Process Actor API when the current process is destroyed,
    538   * but also within this class when the last watcher stopped watching for targets.
    539   */
    540  didDestroy() {
    541    // Unregister all the active watchers.
    542    // This will destroy all the active target actors and unregister the target observers.
    543    for (const watcherDataObject of ContentProcessWatcherRegistry.getAllExistingWatchersDataObjects()) {
    544      this.#destroyWatcher(watcherDataObject);
    545    }
    546 
    547    // The previous for loop should have removed all the elements,
    548    // but just to be safe, wipe all stored data to avoid any possible leak.
    549    ContentProcessWatcherRegistry.clear();
    550  }
    551 }
    552 
    553 export class BrowserToolboxDevToolsProcessChild extends DevToolsProcessChild {}