tor-browser

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

DevToolsProcessParent.sys.mjs (14605B)


      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 { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
      6 import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs";
      7 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
      8 
      9 const { ParentProcessWatcherRegistry } = ChromeUtils.importESModule(
     10  "resource://devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs",
     11  // ParentProcessWatcherRegistry needs to be a true singleton and loads ActorManagerParent
     12  // which also has to be a true singleton.
     13  { global: "shared" }
     14 );
     15 
     16 const lazy = {};
     17 loader.lazyRequireGetter(
     18  lazy,
     19  "JsWindowActorTransport",
     20  "devtools/shared/transport/js-window-actor-transport",
     21  true
     22 );
     23 
     24 export class DevToolsProcessParent extends JSProcessActorParent {
     25  constructor() {
     26    super();
     27 
     28    EventEmitter.decorate(this);
     29  }
     30 
     31  // Boolean to indicate if the related content process is slow to respond
     32  // and may be hanging or frozen. When true, we should avoid waiting for its replies.
     33  #frozen = false;
     34 
     35  #destroyed = false;
     36 
     37  // Map of various data specific to each active Watcher Actor.
     38  // A Watcher will become "active" once we receive a first target actor
     39  // notified by the content process, which happens only once
     40  // the watcher starts watching for some target types.
     41  //
     42  // This Map is keyed by "watcher connection prefix".
     43  // This is a unique prefix, per watcher actor, which will
     44  // be used in the "forwardingPrefix" documented below.
     45  //
     46  // Note that We may have multiple Watcher actors,
     47  // which will resuse the same DevToolsServerConnection (watcher.conn)
     48  // if they are instantiated from the same client connection.
     49  // Or be using distinct DevToolsServerConnection, if they
     50  // are instantiated via distinct client connections.
     51  //
     52  // The Map's values are objects containing the following properties:
     53  // - watcher:
     54  //     The watcher actor instance.
     55  // - targetActorForms:
     56  //     The list of all active target actor forms
     57  //     related to this watcher actor.
     58  // - transport:
     59  //     The JsWindowActorTransport which will receive and emit
     60  //     the RDP packets from the current JS Process Actor's content process.
     61  //     We spawn one transpart per Watcher and Content process pair.
     62  // - forwardingPrefix:
     63  //     The forwarding prefix is specific to the transport.
     64  //     It helps redirect RDP packets between this "transport" and
     65  //     the DevToolsServerConnection (watcher.conn), in the parent process,
     66  //     which receives and emits RDP packet from/to the client.
     67 
     68  #watchers = new Map();
     69 
     70  /**
     71   * Request the content process to create all the targets currently watched
     72   * and start observing for new ones to be created later.
     73   */
     74  watchTargets({ watcherActorID, targetType }) {
     75    return this.sendQuery("DevToolsProcessParent:watchTargets", {
     76      watcherActorID,
     77      targetType,
     78    });
     79  }
     80 
     81  /**
     82   * Request the content process to stop observing for currently watched targets
     83   * and destroy all the currently active ones.
     84   */
     85  unwatchTargets({ watcherActorID, targetType, options }) {
     86    this.sendAsyncMessage("DevToolsProcessParent:unwatchTargets", {
     87      watcherActorID,
     88      targetType,
     89      options,
     90    });
     91  }
     92 
     93  /**
     94   * Communicate to the content process that some data have been added or set.
     95   */
     96  addOrSetSessionDataEntry({ watcherActorID, type, entries, updateType }) {
     97    return this.sendQuery("DevToolsProcessParent:addOrSetSessionDataEntry", {
     98      watcherActorID,
     99      type,
    100      entries,
    101      updateType,
    102    });
    103  }
    104 
    105  /**
    106   * Communicate to the content process that some data have been removed.
    107   */
    108  removeSessionDataEntry({ watcherActorID, type, entries }) {
    109    this.sendAsyncMessage("DevToolsProcessParent:removeSessionDataEntry", {
    110      watcherActorID,
    111      type,
    112      entries,
    113    });
    114  }
    115 
    116  destroyWatcher({ watcherActorID }) {
    117    return this.sendAsyncMessage("DevToolsProcessParent:destroyWatcher", {
    118      watcherActorID,
    119    });
    120  }
    121 
    122  /**
    123   * Called when the content process notified us about a new target actor
    124   */
    125  #onTargetAvailable({ watcherActorID, forwardingPrefix, targetActorForm }) {
    126    const watcher = ParentProcessWatcherRegistry.getWatcher(watcherActorID);
    127 
    128    if (!watcher) {
    129      throw new Error(
    130        `Watcher Actor with ID '${watcherActorID}' can't be found.`
    131      );
    132    }
    133    const connection = watcher.conn;
    134 
    135    // If this is the first target actor for this watcher,
    136    // hook up the DevToolsServerConnection which will bridge
    137    // communication between the parent process DevToolsServer
    138    // and the content process.
    139    if (!this.#watchers.get(watcher.watcherConnectionPrefix)) {
    140      connection.on("closed", this.#onConnectionClosed);
    141 
    142      // Create a js-window-actor based transport.
    143      const transport = new lazy.JsWindowActorTransport(
    144        this,
    145        forwardingPrefix,
    146        "DevToolsProcessParent:packet"
    147      );
    148      transport.hooks = {
    149        onPacket: connection.send.bind(connection),
    150        onClosed() {},
    151      };
    152      transport.ready();
    153 
    154      connection.setForwarding(forwardingPrefix, transport);
    155 
    156      this.#watchers.set(watcher.watcherConnectionPrefix, {
    157        watcher,
    158        // This prefix is the prefix of the DevToolsServerConnection, running
    159        // in the content process, for which we should forward packets to, based on its prefix.
    160        // While `watcher.connection` is also a DevToolsServerConnection, but from this process,
    161        // the parent process. It is the one receiving Client packets and the one, from which
    162        // we should forward packets from.
    163        forwardingPrefix,
    164        transport,
    165        targetActorForms: [],
    166      });
    167    }
    168 
    169    this.#watchers
    170      .get(watcher.watcherConnectionPrefix)
    171      .targetActorForms.push(targetActorForm);
    172 
    173    watcher.notifyTargetAvailable(targetActorForm);
    174  }
    175 
    176  /**
    177   * Called when the content process notified us about a target actor that has been destroyed.
    178   */
    179  #onTargetDestroyed({ actors, options }) {
    180    for (const { watcherActorID, targetActorForm } of actors) {
    181      const watcher = ParentProcessWatcherRegistry.getWatcher(watcherActorID);
    182      // As we instruct to destroy all targets when the watcher is destroyed,
    183      // we may easily receive the target destruction notification *after*
    184      // the watcher has been removed from the registry.
    185      if (!watcher || watcher.isDestroyed()) {
    186        continue;
    187      }
    188      watcher.notifyTargetDestroyed(targetActorForm, options);
    189      const watcherInfo = this.#watchers.get(watcher.watcherConnectionPrefix);
    190      if (watcherInfo) {
    191        const idx = watcherInfo.targetActorForms.findIndex(
    192          form => form.actor == targetActorForm.actor
    193        );
    194        if (idx != -1) {
    195          watcherInfo.targetActorForms.splice(idx, 1);
    196        }
    197        // Once the last active target is removed, disconnect the DevTools transport
    198        // and cleanup everything bound to this DOM Process. We will re-instantiate
    199        // a new connection/transport on the next reported target actor.
    200        if (!watcherInfo.targetActorForms.length) {
    201          this.#unregisterWatcher(watcherInfo.watcher, options);
    202        }
    203      }
    204    }
    205  }
    206 
    207  #onConnectionClosed = (status, prefix) => {
    208    for (const watcherInfo of this.#watchers.values()) {
    209      if (watcherInfo.watcher.conn?.prefix == prefix) {
    210        this.#unregisterWatcher(watcherInfo.watcher);
    211      }
    212    }
    213  };
    214 
    215  /**
    216   * Unregister a given watcher.
    217   * This will also close and unregister the related given DevToolsServerConnection,
    218   * if no other watcher is active on the same, possibly shared, connection.
    219   * (when remote debugging many tabs on the same connection)
    220   *
    221   * @param {WatcherActor} watcher
    222   * @param {object} options
    223   * @param {boolean} options.isModeSwitching
    224   *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
    225   */
    226  #unregisterWatcher(watcher, options = {}) {
    227    const watcherInfo = this.#watchers.get(watcher.watcherConnectionPrefix);
    228    if (!watcherInfo) {
    229      return;
    230    }
    231    this.#watchers.delete(watcher.watcherConnectionPrefix);
    232 
    233    for (const actor of watcherInfo.targetActorForms) {
    234      watcherInfo.watcher.notifyTargetDestroyed(actor, options);
    235    }
    236 
    237    let connectionUsedByAnotherWatcher = false;
    238    for (const info of this.#watchers.values()) {
    239      if (info.watcher.conn == watcherInfo.watcher.conn) {
    240        connectionUsedByAnotherWatcher = true;
    241        break;
    242      }
    243    }
    244 
    245    if (!connectionUsedByAnotherWatcher) {
    246      const { forwardingPrefix, transport } = watcherInfo;
    247      if (transport) {
    248        // If we have a child transport, the actor has already
    249        // been created. We need to stop using this transport.
    250        transport.close(options);
    251      }
    252      // When cancelling the forwarding, one RDP event is sent to the client to purge all requests
    253      // and actors related to a given prefix.
    254      // Be careful that any late RDP event would be ignored by the client passed this call.
    255      watcherInfo.watcher.conn.cancelForwarding(forwardingPrefix);
    256    }
    257 
    258    if (!this.#watchers.size) {
    259      this.#destroy(options);
    260    }
    261  }
    262 
    263  /**
    264   * Destroy and cleanup everything for this DOM Process.
    265   *
    266   * @param {object} options
    267   * @param {boolean} options.isModeSwitching
    268   *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
    269   */
    270  #destroy(options) {
    271    if (this.#destroyed) {
    272      return;
    273    }
    274    this.#destroyed = true;
    275 
    276    for (const watcherInfo of this.#watchers.values()) {
    277      this.#unregisterWatcher(watcherInfo.watcher, options);
    278    }
    279  }
    280 
    281  /**
    282   * Used by DevTools Transport to send packets to the content process.
    283   */
    284 
    285  sendPacket(packet, prefix) {
    286    this.sendAsyncMessage("DevToolsProcessParent:packet", { packet, prefix });
    287  }
    288 
    289  /**
    290   * JsProcessActor API
    291   */
    292 
    293  /**
    294   * JS Actor override of `sendQuery` method, whose main goal is the ignore possibly freezing processes.
    295   * This also prints a warning when the query failed to be sent, or when a process hangs.
    296   *
    297   * @param String msg
    298   * @param Array<json> args
    299   * @return Promise<undefined>
    300   *   We only use sendQuery for two queries ("watchTargets" and "addOrSetSessionDataEntry") and
    301   *   none of them use any returned value (except a promise to know when their processing is done).
    302   */
    303  async sendQuery(msg, args) {
    304    // If any preview query timed out and did not reply yet, the process is considered frozen
    305    // and are no longer waiting for the process response.
    306    if (this.#frozen) {
    307      this.sendAsyncMessage(msg, args);
    308      return Promise.resolve();
    309    }
    310 
    311    // Cache `osPid` and avoid querying `this.manager` attribute later as it may result into
    312    // a `AssertReturnTypeMatchesJitinfo` assertion crash into GenericGetter .
    313    const { osPid } = this.manager;
    314 
    315    return new Promise((resolve, reject) => {
    316      // The process may be slow to resolve the query, or even be completely frozen.
    317      // Use a timeout to detect when it happens.
    318      const timeout = setTimeout(() => {
    319        this.#frozen = true;
    320        console.error(
    321          `Content process ${osPid} isn't responsive while sending "${msg}" request. DevTools will ignore this process for now.`
    322        );
    323        // Do not consider timeout as an error as it may easily break the frontend.
    324        resolve();
    325      }, 1000);
    326 
    327      super.sendQuery(msg, args).then(
    328        () => {
    329          if (this.#frozen && !this.#destroyed) {
    330            console.error(
    331              `Content process ${osPid} is responsive again. DevTools resumes operations against it.`
    332            );
    333          }
    334          clearTimeout(timeout);
    335          // If any of the ongoing query resolved, consider the process as responsive again
    336          this.#frozen = false;
    337 
    338          resolve();
    339        },
    340        async e => {
    341          // Ignore frozen processes when the JS Process Actor is destroyed.
    342          // Either the process was shut down or DevTools unregistered the Actor.
    343          if (this.#frozen && !this.#destroyed) {
    344            console.error(
    345              `Content process ${osPid} is responsive again. DevTools resumes operations against it.`
    346            );
    347          }
    348          clearTimeout(timeout);
    349          // If any of the ongoing query resolved, consider the process as responsive again
    350          this.#frozen = false;
    351 
    352          console.error("Failed to sendQuery in DevToolsProcessParent", msg);
    353          console.error(e.toString());
    354          reject(e);
    355        }
    356      );
    357    });
    358  }
    359 
    360  /**
    361   * Called by the JSProcessActor API when the content process sent us a message
    362   */
    363  receiveMessage(message) {
    364    switch (message.name) {
    365      case "DevToolsProcessChild:targetAvailable":
    366        return this.#onTargetAvailable(message.data);
    367      case "DevToolsProcessChild:packet":
    368        return this.emit("packet-received", message);
    369      case "DevToolsProcessChild:targetDestroyed":
    370        return this.#onTargetDestroyed(message.data);
    371      case "DevToolsProcessChild:bf-cache-navigation-pageshow": {
    372        const browsingContext = BrowsingContext.get(
    373          message.data.browsingContextId
    374        );
    375        for (const watcherActor of ParentProcessWatcherRegistry.getWatchersForBrowserId(
    376          browsingContext.browserId
    377        )) {
    378          watcherActor.emit("bf-cache-navigation-pageshow", {
    379            windowGlobal: browsingContext.currentWindowGlobal,
    380          });
    381        }
    382        return null;
    383      }
    384      case "DevToolsProcessChild:bf-cache-navigation-pagehide": {
    385        const browsingContext = BrowsingContext.get(
    386          message.data.browsingContextId
    387        );
    388        for (const watcherActor of ParentProcessWatcherRegistry.getWatchersForBrowserId(
    389          browsingContext.browserId
    390        )) {
    391          watcherActor.emit("bf-cache-navigation-pagehide", {
    392            windowGlobal: browsingContext.currentWindowGlobal,
    393          });
    394        }
    395        return null;
    396      }
    397      default:
    398        throw new Error(
    399          "Unsupported message in DevToolsProcessParent: " + message.name
    400        );
    401    }
    402  }
    403 
    404  /**
    405   * Called by the JSProcessActor API when this content process is destroyed.
    406   */
    407  didDestroy() {
    408    this.#destroy();
    409  }
    410 }
    411 
    412 export class BrowserToolboxDevToolsProcessParent extends DevToolsProcessParent {}