tor-browser

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

devtools-client.js (33256B)


      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 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
      8 const {
      9  getStack,
     10  callFunctionWithAsyncStack,
     11 } = require("resource://devtools/shared/platform/stack.js");
     12 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     13 const {
     14  UnsolicitedNotifications,
     15 } = require("resource://devtools/client/constants.js");
     16 const { AppConstants } = ChromeUtils.importESModule(
     17  "resource://gre/modules/AppConstants.sys.mjs"
     18 );
     19 
     20 loader.lazyRequireGetter(
     21  this,
     22  "Authentication",
     23  "resource://devtools/shared/security/auth.js"
     24 );
     25 loader.lazyRequireGetter(
     26  this,
     27  "DebuggerSocket",
     28  "resource://devtools/shared/security/socket.js",
     29  true
     30 );
     31 loader.lazyRequireGetter(
     32  this,
     33  "EventEmitter",
     34  "resource://devtools/shared/event-emitter.js"
     35 );
     36 
     37 loader.lazyRequireGetter(
     38  this,
     39  ["createRootFront", "Front"],
     40  "resource://devtools/shared/protocol.js",
     41  true
     42 );
     43 
     44 loader.lazyRequireGetter(
     45  this,
     46  "ObjectFront",
     47  "resource://devtools/client/fronts/object.js",
     48  true
     49 );
     50 
     51 /**
     52 * Creates a client for the remote debugging protocol server. This client
     53 * provides the means to communicate with the server and exchange the messages
     54 * required by the protocol in a traditional JavaScript API.
     55 */
     56 class DevToolsClient extends EventEmitter {
     57  constructor(transport) {
     58    super();
     59 
     60    this._transport = transport;
     61    this._transport.hooks = this;
     62 
     63    this._pendingRequests = new Map();
     64    this._activeRequests = new Map();
     65    this._eventsEnabled = true;
     66 
     67    this.traits = {};
     68 
     69    this.request = this.request.bind(this);
     70 
     71    /*
     72     * As the first thing on the connection, expect a greeting packet from
     73     * the connection's root actor.
     74     */
     75    this.mainRoot = null;
     76    this.expectReply("root", async packet => {
     77      if (packet.error) {
     78        console.error("Error when waiting for root actor", packet);
     79        return;
     80      }
     81 
     82      this.mainRoot = createRootFront(this, packet);
     83 
     84      // Once the root actor has been communicated by the server,
     85      // emit a request to it to also push informations down to the server.
     86      //
     87      // This request has been added in Firefox 133.
     88      try {
     89        await this.mainRoot.connect({
     90          frontendVersion: AppConstants.MOZ_APP_VERSION,
     91        });
     92      } catch (e) {
     93        // Ignore errors of unsupported packet as the server may not yet support this request.
     94        // The request may also fail to complete in tests when closing DevTools quickly after opening.
     95        if (!e.message.includes("unrecognizedPacketType")) {
     96          throw e;
     97        }
     98      }
     99 
    100      this.emit("connected", packet.applicationType, packet.traits);
    101    });
    102  }
    103 
    104  // Expose these to save callers the trouble of importing DebuggerSocket
    105  static socketConnect(options) {
    106    // Defined here instead of just copying the function to allow lazy-load
    107    return DebuggerSocket.connect(options);
    108  }
    109 
    110  static get Authenticators() {
    111    return Authentication.Authenticators;
    112  }
    113 
    114  static get AuthenticationResult() {
    115    return Authentication.AuthenticationResult;
    116  }
    117 
    118  /**
    119   * Connect to the server and start exchanging protocol messages.
    120   *
    121   * @return Promise
    122   *         Resolves once connected with an array whose first element
    123   *         is the application type, by default "browser", and the second
    124   *         element is the traits object (help figure out the features
    125   *         and behaviors of the server we connect to. See RootActor).
    126   */
    127  connect() {
    128    return new Promise(resolve => {
    129      this.once("connected", (applicationType, traits) => {
    130        this.traits = traits;
    131 
    132        resolve([applicationType, traits]);
    133      });
    134 
    135      this._transport.ready();
    136    });
    137  }
    138 
    139  /**
    140   * Shut down communication with the debugging server.
    141   *
    142   * @return Promise
    143   *         Resolves after the underlying transport is closed.
    144   */
    145  close() {
    146    if (this._transportClosed) {
    147      return Promise.resolve();
    148    }
    149    if (this._closePromise) {
    150      return this._closePromise;
    151    }
    152    // Immediately set the destroy promise,
    153    // as the following code is fully synchronous and can be reentrant.
    154    this._closePromise = this.once("closed");
    155 
    156    // Disable detach event notifications, because event handlers will be in a
    157    // cleared scope by the time they run.
    158    this._eventsEnabled = false;
    159 
    160    if (this._transport) {
    161      this._transport.close();
    162      this._transport = null;
    163    }
    164 
    165    return this._closePromise;
    166  }
    167 
    168  /**
    169   * Send a request to the debugging server.
    170   *
    171   * @param packet object
    172   *        A JSON packet to send to the debugging server.
    173   * @return Request
    174   *         This object emits a number of events to allow you to respond to
    175   *         different parts of the request lifecycle.
    176   *         It is also a Promise object, with a `then` method, that is resolved
    177   *         whenever a JSON or a Bulk response is received; and is rejected
    178   *         if the response is an error.
    179   *
    180   *         Events emitted:
    181   *         * json-reply: The server replied with a JSON packet, which is
    182   *           passed as event data.
    183   *         * bulk-reply: The server replied with bulk data, which you can read
    184   *           using the event data object containing:
    185   *           * actor:        Name of actor that received the packet
    186   *           * type:         Name of actor's method that was called on receipt
    187   *           * length:       Size of the data to be read
    188   *           * stream:       This input stream should only be used directly if you
    189   *                           can ensure that you will read exactly |length| bytes
    190   *                           and will not close the stream when reading is complete
    191   *           * done:         If you use the stream directly (instead of |copyTo|
    192   *                           or |copyToBuffer| below), you must signal
    193   *                           completion by resolving / rejecting this promise.
    194   *                           If it's rejected, the transport will be closed.
    195   *                           If an Error is supplied as a rejection value, it
    196   *                           will be logged via |dumpn|.  If you do use
    197   *                           |copyTo| or |copyToBuffer|, resolving is taken
    198   *                           care of for you when copying completes.
    199   *           * copyTo:       A helper function for getting your data out of the
    200   *                           stream that meets the stream handling requirements
    201   *                           above, and has the following signature:
    202   *             @param  output nsIAsyncOutputStream
    203   *                           The stream to copy to.
    204   *             @return Promise
    205   *                           The promise is resolved when copying completes or
    206   *                           rejected if any (unexpected) errors occur.
    207   *                           This object also emits "progress" events for each chunk
    208   *                           that is copied.  See stream-utils.js.
    209   *           * copyToBuffer: a helper function for getting your data out of the stream
    210   *                           that meets the stream handling requirements above, and has
    211   *                           the following signature:
    212   *             @param output ArrayBuffer
    213   *                           The buffer to copy to. It needs to be the same length as the data
    214   *                           to be transfered.
    215   *             @return Promise
    216   *                           The promise is resolved when copying completes or rejected if any
    217   *                           (unexpected) error occurs.
    218   */
    219  request(packet) {
    220    if (!this.mainRoot) {
    221      throw Error("Have not yet received a hello packet from the server.");
    222    }
    223    const type = packet.type || "";
    224    if (!packet.to) {
    225      throw Error("'" + type + "' request packet has no destination.");
    226    }
    227 
    228    if (this._transportClosed) {
    229      const msg =
    230        "'" +
    231        type +
    232        "' request packet to " +
    233        "'" +
    234        packet.to +
    235        "' " +
    236        "can't be sent as the connection is closed.";
    237      return Promise.reject({ error: "connectionClosed", message: msg });
    238    }
    239 
    240    const request = new Request(packet);
    241    request.format = "json";
    242    request.stack = getStack();
    243 
    244    // Implement a Promise like API on the returned object
    245    // that resolves/rejects on request response
    246    const promise = new Promise((resolve, reject) => {
    247      function listenerJson(resp) {
    248        removeRequestListeners();
    249        if (resp.error) {
    250          reject(resp);
    251        } else {
    252          resolve(resp);
    253        }
    254      }
    255      function listenerBulk(resp) {
    256        removeRequestListeners();
    257        resolve(resp);
    258      }
    259 
    260      const removeRequestListeners = () => {
    261        request.off("json-reply", listenerJson);
    262        request.off("bulk-reply", listenerBulk);
    263      };
    264 
    265      request.on("json-reply", listenerJson);
    266      request.on("bulk-reply", listenerBulk);
    267    });
    268 
    269    this._sendOrQueueRequest(request);
    270    request.then = promise.then.bind(promise);
    271    request.catch = promise.catch.bind(promise);
    272 
    273    return request;
    274  }
    275 
    276  /**
    277   * Transmit streaming data via a bulk request.
    278   *
    279   * This method initiates the bulk send process by queuing up the header data.
    280   * The caller receives eventual access to a stream for writing.
    281   *
    282   * Since this opens up more options for how the server might respond (it could
    283   * send back either JSON or bulk data), and the returned Request object emits
    284   * events for different stages of the request process that you may want to
    285   * react to.
    286   *
    287   * @param request Object
    288   *        This is modeled after the format of JSON packets above, but does not
    289   *        actually contain the data, but is instead just a routing header:
    290   *          * actor:  Name of actor that will receive the packet
    291   *          * type:   Name of actor's method that should be called on receipt
    292   *          * length: Size of the data to be sent
    293   * @return Request
    294   *         This object emits a number of events to allow you to respond to
    295   *         different parts of the request lifecycle.
    296   *
    297   *         Events emitted:
    298   *         * bulk-send-ready: Ready to send bulk data to the server, using the
    299   *           event data object containing:
    300   *           * stream:         This output stream should only be used directly if
    301   *                             you can ensure that you will write exactly |length|
    302   *                             bytes and will not close the stream when writing is
    303   *                             complete
    304   *           * done:           If you use the stream directly (instead of |copyFrom|
    305   *                             or |copyFromBuffer| below), you must signal
    306   *                             completion by resolving / rejecting this
    307   *                             promise.  If it's rejected, the transport will
    308   *                             be closed.  If an Error is supplied as a
    309   *                             rejection value, it will be logged via |dumpn|.
    310   *                             If you do use |copyFrom| or |copyFromBuffer|,
    311   *                             resolving is taken care of for you when copying
    312   *                             completes.
    313   *           * copyFrom:       A helper function for getting your data onto the
    314   *                             stream that meets the stream handling requirements
    315   *                             above, and has the following signature:
    316   *             @param  input nsIAsyncInputStream
    317   *                     The stream to copy from.
    318   *             @return Promise
    319   *                     The promise is resolved when copying completes or
    320   *                     rejected if any (unexpected) errors occur.
    321   *                     This object also emits "progress" events for each chunk
    322   *                     that is copied.  See stream-utils.js.
    323   *           * copyFromBuffer: A helper function for getting your data onto
    324   *                             the stream that meets the stream handling
    325   *                             requirements above, and has the following
    326   *                             signature:
    327   *             @param  input ArrayBuffer
    328   *                     The buffer to read from. It needs to be the same length
    329   *                     as the data to write.
    330   *             @return Promise
    331   *                     The promise is resolved when copying completes or
    332   *                     rejected if any (unexpected) errors occur.
    333   *         * json-reply: The server replied with a JSON packet, which is
    334   *           passed as event data.
    335   *         * bulk-reply: The server replied with bulk data, which you can read
    336   *           using the event data object containing:
    337   *           * actor:        Name of actor that received the packet
    338   *           * type:         Name of actor's method that was called on receipt
    339   *           * length:       Size of the data to be read
    340   *           * stream:       This input stream should only be used directly if you
    341   *                           can ensure that you will read exactly |length| bytes
    342   *                           and will not close the stream when reading is complete
    343   *           * done:         If you use the stream directly (instead of |copyTo|
    344   *                           |copyToBuffer| below), you must signal completion
    345   *                           by resolving / rejecting this promise.  If it's
    346   *                           rejected, the transport will be closed.  If an
    347   *                           Error is supplied as a rejection value, it will
    348   *                           be logged via |dumpn|.  If you do use |copyTo| or
    349   *                           |copyToBuffer|, resolving is taken care of for
    350   *                           you when copying completes.
    351   *           * copyTo:       A helper function for getting your data out of the
    352   *                           stream that meets the stream handling requirements
    353   *                           above, and has the following signature:
    354   *             @param  output nsIAsyncOutputStream
    355   *                           The stream to copy to.
    356   *             @return Promise
    357   *                           The promise is resolved when copying completes or
    358   *                           rejected if any (unexpected) errors occur.
    359   *                           This object also emits "progress" events for each chunk
    360   *                           that is copied.  See stream-utils.js.
    361   *
    362   *           * copyToBuffer: a helper function for getting your data out of the stream
    363   *                           that meets the stream handling requirements above, and has
    364   *                           the following signature:
    365   *             @param output ArrayBuffer
    366   *                           The buffer to copy to. It needs to be the same length as the data
    367   *                           to be transfered.
    368   *             @return Promise
    369   *                           The promise is resolved when copying completes or rejected if any
    370   *                           (unexpected) error occurs.
    371   */
    372  startBulkRequest(request) {
    373    if (!this.mainRoot) {
    374      throw Error("Have not yet received a hello packet from the server.");
    375    }
    376    if (!request.type) {
    377      throw Error("Bulk packet is missing the required 'type' field.");
    378    }
    379    if (!request.actor) {
    380      throw Error("'" + request.type + "' bulk packet has no destination.");
    381    }
    382    if (!request.length) {
    383      throw Error("'" + request.type + "' bulk packet has no length.");
    384    }
    385 
    386    request = new Request(request);
    387    request.format = "bulk";
    388 
    389    this._sendOrQueueRequest(request);
    390 
    391    return request;
    392  }
    393 
    394  /**
    395   * If a new request can be sent immediately, do so.  Otherwise, queue it.
    396   */
    397  _sendOrQueueRequest(request) {
    398    const actor = request.actor;
    399    if (!this._activeRequests.has(actor)) {
    400      this._sendRequest(request);
    401    } else {
    402      this._queueRequest(request);
    403    }
    404  }
    405 
    406  /**
    407   * Send a request.
    408   *
    409   * @throws Error if there is already an active request in flight for the same
    410   *         actor.
    411   */
    412  _sendRequest(request) {
    413    const actor = request.actor;
    414    this.expectReply(actor, request);
    415 
    416    if (request.format === "json") {
    417      this._transport.send(request.request);
    418      return;
    419    }
    420 
    421    this._transport.startBulkSend(request.request).then((...args) => {
    422      request.emit("bulk-send-ready", ...args);
    423    });
    424  }
    425 
    426  /**
    427   * Queue a request to be sent later.  Queues are only drained when an in
    428   * flight request to a given actor completes.
    429   */
    430  _queueRequest(request) {
    431    const actor = request.actor;
    432    const queue = this._pendingRequests.get(actor) || [];
    433    queue.push(request);
    434    this._pendingRequests.set(actor, queue);
    435  }
    436 
    437  /**
    438   * Attempt the next request to a given actor (if any).
    439   */
    440  _attemptNextRequest(actor) {
    441    if (this._activeRequests.has(actor)) {
    442      return;
    443    }
    444    const queue = this._pendingRequests.get(actor);
    445    if (!queue) {
    446      return;
    447    }
    448    const request = queue.shift();
    449    if (queue.length === 0) {
    450      this._pendingRequests.delete(actor);
    451    }
    452    this._sendRequest(request);
    453  }
    454 
    455  /**
    456   * Arrange to hand the next reply from |actor| to the handler bound to
    457   * |request|.
    458   *
    459   * DevToolsClient.prototype.request / startBulkRequest usually takes care of
    460   * establishing the handler for a given request, but in rare cases (well,
    461   * greetings from new root actors, is the only case at the moment) we must be
    462   * prepared for a "reply" that doesn't correspond to any request we sent.
    463   */
    464  expectReply(actor, request) {
    465    if (this._activeRequests.has(actor)) {
    466      throw Error("clashing handlers for next reply from " + actor);
    467    }
    468 
    469    // If a handler is passed directly (as it is with the handler for the root
    470    // actor greeting), create a dummy request to bind this to.
    471    if (typeof request === "function") {
    472      const handler = request;
    473      request = new Request();
    474      request.on("json-reply", handler);
    475    }
    476 
    477    this._activeRequests.set(actor, request);
    478  }
    479 
    480  // Transport hooks.
    481 
    482  /**
    483   * Called by DebuggerTransport to dispatch incoming packets as appropriate.
    484   *
    485   * @param packet object
    486   *        The incoming packet.
    487   */
    488  onPacket(packet) {
    489    if (!packet.from) {
    490      DevToolsUtils.reportException(
    491        "onPacket",
    492        new Error(
    493          "Server did not specify an actor, dropping packet: " +
    494            JSON.stringify(packet)
    495        )
    496      );
    497      return;
    498    }
    499 
    500    // Check for "forwardingCancelled" here instead of using a front to handle it.
    501    // This is necessary because we might receive this event while the client is closing,
    502    // and the fronts have already been removed by that point.
    503    if (
    504      this.mainRoot &&
    505      packet.from == this.mainRoot.actorID &&
    506      packet.type == "forwardingCancelled"
    507    ) {
    508      this.purgeRequests(packet.prefix);
    509      return;
    510    }
    511 
    512    // If we have a registered Front for this actor, let it handle the packet
    513    // and skip all the rest of this unpleasantness.
    514    const front = this.getFrontByID(packet.from);
    515    if (front) {
    516      front.onPacket(packet);
    517      return;
    518    }
    519 
    520    let activeRequest;
    521    // See if we have a handler function waiting for a reply from this
    522    // actor. (Don't count unsolicited notifications or pauses as
    523    // replies.)
    524    if (
    525      this._activeRequests.has(packet.from) &&
    526      !(packet.type in UnsolicitedNotifications)
    527    ) {
    528      activeRequest = this._activeRequests.get(packet.from);
    529      this._activeRequests.delete(packet.from);
    530    }
    531 
    532    // If there is a subsequent request for the same actor, hand it off to the
    533    // transport.  Delivery of packets on the other end is always async, even
    534    // in the local transport case.
    535    this._attemptNextRequest(packet.from);
    536 
    537    // Only try to notify listeners on events, not responses to requests
    538    // that lack a packet type.
    539    if (packet.type) {
    540      this.emit(packet.type, packet);
    541    }
    542 
    543    if (activeRequest) {
    544      const emitReply = () => activeRequest.emit("json-reply", packet);
    545      if (activeRequest.stack) {
    546        callFunctionWithAsyncStack(
    547          emitReply,
    548          activeRequest.stack,
    549          "DevTools RDP"
    550        );
    551      } else {
    552        emitReply();
    553      }
    554    }
    555  }
    556 
    557  /**
    558   * Called by the DebuggerTransport to dispatch incoming bulk packets as
    559   * appropriate.
    560   *
    561   * @param packet object
    562   *        The incoming packet, which contains:
    563   *        * actor:        Name of actor that will receive the packet
    564   *        * type:         Name of actor's method that should be called on receipt
    565   *        * length:       Size of the data to be read
    566   *        * stream:       This input stream should only be used directly if you can
    567   *                        ensure that you will read exactly |length| bytes and will
    568   *                        not close the stream when reading is complete
    569   *        * done:         If you use the stream directly (instead of |copyTo|
    570   *                        or |copyToBuffer| below), you must signal completion
    571   *                        by resolving / rejecting this promise.  If it's
    572   *                        rejected, the transport will be closed.  If an Error
    573   *                        is supplied as a rejection value, it will be logged
    574   *                        via |dumpn|.  If you do use |copyTo| or
    575   *                        |copyToBuffer|, resolving is taken care of for you
    576   *                        when copying completes.
    577   *        * copyTo:       A helper function for getting your data out of the stream
    578   *                        that meets the stream handling requirements above, and has
    579   *                        the following signature:
    580   *          @param  output nsIAsyncOutputStream
    581   *                        The stream to copy to.
    582   *          @return Promise
    583   *                        The promise is resolved when copying completes or rejected
    584   *                        if any (unexpected) errors occur.
    585   *                        This object also emits "progress" events for each chunk
    586   *                        that is copied.  See stream-utils.js.
    587   *        * copyToBuffer: a helper function for getting your data out of the stream
    588   *                        that meets the stream handling requirements above, and has
    589   *                        the following signature:
    590   *          @param output ArrayBuffer
    591   *                        The buffer to copy to. It needs to be the same length as the data
    592   *                        to be transfered.
    593   *          @return Promise
    594   *                        The promise is resolved when copying completes or rejected if any
    595   *                        (unexpected) error occurs.
    596   */
    597  onBulkPacket(packet) {
    598    const { actor } = packet;
    599 
    600    if (!actor) {
    601      DevToolsUtils.reportException(
    602        "onBulkPacket",
    603        new Error(
    604          "Server did not specify an actor, dropping bulk packet: " +
    605            JSON.stringify(packet)
    606        )
    607      );
    608      return;
    609    }
    610 
    611    const front = this.getFrontByID(actor);
    612    if (front) {
    613      front.onBulkPacket(packet);
    614      return;
    615    }
    616 
    617    // See if we have a handler function waiting for a reply from this
    618    // actor.
    619    if (!this._activeRequests.has(actor)) {
    620      return;
    621    }
    622 
    623    const activeRequest = this._activeRequests.get(actor);
    624    this._activeRequests.delete(actor);
    625 
    626    // If there is a subsequent request for the same actor, hand it off to the
    627    // transport.  Delivery of packets on the other end is always async, even
    628    // in the local transport case.
    629    this._attemptNextRequest(actor);
    630 
    631    activeRequest.emit("bulk-reply", packet);
    632  }
    633 
    634  /**
    635   * Called by DebuggerTransport when the underlying stream is closed.
    636   *
    637   * @param status nsresult
    638   *        The status code that corresponds to the reason for closing
    639   *        the stream.
    640   */
    641  onTransportClosed() {
    642    if (this._transportClosed) {
    643      return;
    644    }
    645    this._transportClosed = true;
    646    this.emit("closed");
    647 
    648    this.purgeRequests();
    649 
    650    // The |_pools| array on the client-side currently is used only by
    651    // protocol.js to store active fronts, mirroring the actor pools found in
    652    // the server.  So, read all usages of "pool" as "protocol.js front".
    653    //
    654    // In the normal case where we shutdown cleanly, the toolbox tells each tool
    655    // to close, and they each call |destroy| on any fronts they were using.
    656    // When |destroy| is called on a protocol.js front, it also
    657    // removes itself from the |_pools| array.  Once the toolbox has shutdown,
    658    // the connection is closed, and we reach here.  All fronts (should have
    659    // been) |destroy|ed, so |_pools| should empty.
    660    //
    661    // If the connection instead aborts unexpectedly, we may end up here with
    662    // all fronts used during the life of the connection.  So, we call |destroy|
    663    // on them clear their state, reject pending requests, and remove themselves
    664    // from |_pools|.  This saves the toolbox from hanging indefinitely, in case
    665    // it waits for some server response before shutdown that will now never
    666    // arrive.
    667    for (const pool of this._pools) {
    668      pool.destroy();
    669    }
    670  }
    671 
    672  /**
    673   * Purge pending and active requests in this client.
    674   *
    675   * @param prefix string (optional)
    676   *        If a prefix is given, only requests for actor IDs that start with the prefix
    677   *        will be cleaned up.  This is useful when forwarding of a portion of requests
    678   *        is cancelled on the server.
    679   */
    680  purgeRequests(prefix = "") {
    681    const reject = function (type, request) {
    682      // Server can send packets on its own and client only pass a callback
    683      // to expectReply, so that there is no request object.
    684      let msg;
    685      if (request.request) {
    686        msg =
    687          "'" +
    688          request.request.type +
    689          "' " +
    690          type +
    691          " request packet" +
    692          " to '" +
    693          request.actor +
    694          "' " +
    695          "can't be sent as the connection just closed.";
    696      } else {
    697        msg =
    698          "server side packet can't be received as the connection just closed.";
    699      }
    700      const packet = { error: "connectionClosed", message: msg };
    701      request.emit("json-reply", packet);
    702    };
    703 
    704    let pendingRequestsToReject = [];
    705    this._pendingRequests.forEach((requests, actor) => {
    706      if (!actor.startsWith(prefix)) {
    707        return;
    708      }
    709      this._pendingRequests.delete(actor);
    710      pendingRequestsToReject = pendingRequestsToReject.concat(requests);
    711    });
    712    pendingRequestsToReject.forEach(request => reject("pending", request));
    713 
    714    let activeRequestsToReject = [];
    715    this._activeRequests.forEach((request, actor) => {
    716      if (!actor.startsWith(prefix)) {
    717        return;
    718      }
    719      this._activeRequests.delete(actor);
    720      activeRequestsToReject = activeRequestsToReject.concat(request);
    721    });
    722    activeRequestsToReject.forEach(request => reject("active", request));
    723 
    724    // Also purge protocol.js requests
    725    const fronts = this.getAllFronts();
    726 
    727    for (const front of fronts) {
    728      if (!front.isDestroyed() && front.actorID.startsWith(prefix)) {
    729        // Call Front.baseFrontClassDestroy nstead of Front.destroy in order to flush requests
    730        // and nullify front.actorID immediately, even if Front.destroy is overloaded
    731        // by an async function which would otherwise be able to try emitting new request
    732        // after the purge.
    733        front.baseFrontClassDestroy();
    734      }
    735    }
    736  }
    737 
    738  /**
    739   * Search for all requests in process for this client, including those made via
    740   * protocol.js and wait all of them to complete.  Since the requests seen when this is
    741   * first called may in turn trigger more requests, we keep recursing through this
    742   * function until there is no more activity.
    743   *
    744   * This is a fairly heavy weight process, so it's only meant to be used in tests.
    745   *
    746   * @param {object=} options
    747   * @param {boolean=} options.ignoreOrphanedFronts
    748   *        Allow to ignore fronts which can no longer be retrieved via
    749   *        getFrontByID, as their requests can never be completed now.
    750   *        Ideally we should rather investigate and address those cases, but
    751   *        since this is a test helper, allow to bypass them here. Defaults to
    752   *        false.
    753   *
    754   * @return Promise
    755   *         Resolved when all requests have settled.
    756   */
    757  waitForRequestsToSettle({ ignoreOrphanedFronts = false } = {}) {
    758    let requests = [];
    759 
    760    // Gather all pending and active requests in this client
    761    // The request object supports a Promise API for completion (it has .then())
    762    this._pendingRequests.forEach(requestsForActor => {
    763      // Each value is an array of pending requests
    764      requests = requests.concat(requestsForActor);
    765    });
    766    this._activeRequests.forEach(requestForActor => {
    767      // Each value is a single active request
    768      requests = requests.concat(requestForActor);
    769    });
    770 
    771    // protocol.js
    772    const fronts = this.getAllFronts();
    773 
    774    // For each front, wait for its requests to settle
    775    for (const front of fronts) {
    776      if (front.hasRequests()) {
    777        if (ignoreOrphanedFronts && !this.getFrontByID(front.actorID)) {
    778          // If a front was stuck during its destroy but the pool managing it
    779          // has been already removed, ignore its pending requests, they can
    780          // never resolve.
    781          continue;
    782        }
    783        requests.push(front.waitForRequestsToSettle());
    784      }
    785    }
    786 
    787    // Abort early if there are no requests
    788    if (!requests.length) {
    789      return Promise.resolve();
    790    }
    791 
    792    return DevToolsUtils.settleAll(requests)
    793      .catch(() => {
    794        // One of the requests might have failed, but ignore that situation here and pipe
    795        // both success and failure through the same path.  The important part is just that
    796        // we waited.
    797      })
    798      .then(() => {
    799        // Repeat, more requests may have started in response to those we just waited for
    800        return this.waitForRequestsToSettle({ ignoreOrphanedFronts });
    801      });
    802  }
    803 
    804  getAllFronts() {
    805    // Use a Set because some fronts (like domwalker) seem to have multiple parents.
    806    const fronts = new Set();
    807    const poolsToVisit = [...this._pools];
    808 
    809    // With protocol.js, each front can potentially have its own pools containing child
    810    // fronts, forming a tree.  Descend through all the pools to locate all child fronts.
    811    while (poolsToVisit.length) {
    812      const pool = poolsToVisit.shift();
    813      // `_pools` contains either Fronts or Pools, we only want to collect Fronts here.
    814      // Front inherits from Pool which exposes `poolChildren`.
    815      if (pool instanceof Front) {
    816        fronts.add(pool);
    817      }
    818      for (const child of pool.poolChildren()) {
    819        poolsToVisit.push(child);
    820      }
    821    }
    822    return fronts;
    823  }
    824 
    825  /**
    826   * Actor lifetime management, echos the server's actor pools.
    827   */
    828  get _pools() {
    829    if (this.__pools) {
    830      return this.__pools;
    831    }
    832    this.__pools = new Set();
    833    return this.__pools;
    834  }
    835 
    836  addActorPool(pool) {
    837    this._pools.add(pool);
    838  }
    839  removeActorPool(pool) {
    840    this._pools.delete(pool);
    841  }
    842 
    843  /**
    844   * Return the Front for the Actor whose ID is the one passed in argument.
    845   *
    846   * @param {string} actorID: The actor ID to look for.
    847   */
    848  getFrontByID(actorID) {
    849    const pool = this.poolFor(actorID);
    850    return pool ? pool.getActorByID(actorID) : null;
    851  }
    852 
    853  poolFor(actorID) {
    854    for (const pool of this._pools) {
    855      if (pool.has(actorID)) {
    856        return pool;
    857      }
    858    }
    859    return null;
    860  }
    861 
    862  /**
    863   * Creates an object front for this DevToolsClient and the grip in parameter,
    864   *
    865   * @param {object} grip: The grip to create the ObjectFront for.
    866   * @param {ThreadFront} threadFront
    867   * @param {Front} parentFront: Optional front that will manage the object front.
    868   *                             Defaults to threadFront.
    869   * @returns {ObjectFront}
    870   */
    871  createObjectFront(grip, threadFront, parentFront) {
    872    if (!parentFront) {
    873      parentFront = threadFront;
    874    }
    875 
    876    return new ObjectFront(this, threadFront.targetFront, parentFront, grip);
    877  }
    878 
    879  get transport() {
    880    return this._transport;
    881  }
    882 
    883  /**
    884   * Boolean flag to help identify client connected to the current runtime,
    885   * via a LocalDevToolsTransport pipe.
    886   */
    887  get isLocalClient() {
    888    return !!this._transport.isLocalTransport;
    889  }
    890 
    891  dumpPools() {
    892    for (const pool of this._pools) {
    893      console.log(`%c${pool.actorID}`, "font-weight: bold;", [
    894        ...pool.__poolMap.keys(),
    895      ]);
    896    }
    897  }
    898 }
    899 
    900 class Request extends EventEmitter {
    901  constructor(request) {
    902    super();
    903    this.request = request;
    904  }
    905 
    906  get actor() {
    907    return this.request.to || this.request.actor;
    908  }
    909 }
    910 
    911 module.exports = {
    912  DevToolsClient,
    913 };