tor-browser

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

Actor.js (9493B)


      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 var { Pool } = require("resource://devtools/shared/protocol/Pool.js");
      8 
      9 // Lazy load this symbol in order to prevent a dependency cycle between Actor and types.
     10 loader.lazyRequireGetter(
     11  this,
     12  "BULK_RESPONSE",
     13  "resource://devtools/shared/protocol/types.js",
     14  true
     15 );
     16 
     17 /**
     18 * Keep track of which actorSpecs have been created. If a replica of a spec
     19 * is created, it can be caught, and specs which inherit from other specs will
     20 * not overwrite eachother.
     21 */
     22 var actorSpecs = new WeakMap();
     23 
     24 exports.actorSpecs = actorSpecs;
     25 
     26 /**
     27 * An actor in the actor tree.
     28 *
     29 * @param optional conn
     30 *   Either a DevToolsServerConnection or a DevToolsClient.  Must have
     31 *   addActorPool, removeActorPool, and poolFor.
     32 *   conn can be null if the subclass provides a conn property.
     33 * @class
     34 */
     35 
     36 class Actor extends Pool {
     37  constructor(conn, spec) {
     38    super(conn);
     39 
     40    this.typeName = spec.typeName;
     41 
     42    // Will contain the actor's ID
     43    this.actorID = null;
     44 
     45    // Ensure computing requestTypes only one time per class
     46    const proto = Object.getPrototypeOf(this);
     47    if (!proto.requestTypes) {
     48      proto.requestTypes = generateRequestTypes(spec);
     49    }
     50 
     51    // Forward events to the connection.
     52    if (spec.events) {
     53      for (const [name, request] of spec.events.entries()) {
     54        this.on(name, (...args) => {
     55          this._sendEvent(name, request, ...args);
     56        });
     57      }
     58    }
     59  }
     60 
     61  toString() {
     62    return "[Actor " + this.typeName + "/" + this.actorID + "]";
     63  }
     64 
     65  _sendEvent(name, request, ...args) {
     66    if (this.isDestroyed()) {
     67      console.error(
     68        `Tried to send a '${name}' event on an already destroyed actor` +
     69          ` '${this.typeName}'`
     70      );
     71      return;
     72    }
     73    let packet;
     74    try {
     75      packet = request.write(args, this);
     76    } catch (ex) {
     77      console.error("Error sending event: " + name);
     78      throw ex;
     79    }
     80    packet.from = packet.from || this.actorID;
     81    this.conn.send(packet);
     82 
     83    // This can really be a hot path, even computing the marker label can
     84    // have some performance impact.
     85    // Guard against missing `Services.profiler` because Services is mocked to
     86    // an empty object in the worker loader.
     87    if (Services.profiler?.IsActive()) {
     88      ChromeUtils.addProfilerMarker(
     89        "DevTools:RDP Actor",
     90        null,
     91        `${this.typeName}.${name}`
     92      );
     93    }
     94  }
     95 
     96  destroy() {
     97    super.destroy();
     98    this.actorID = null;
     99    this._isDestroyed = true;
    100  }
    101 
    102  /**
    103   * Override this method in subclasses to serialize the actor.
    104   *
    105   * @returns A jsonable object.
    106   */
    107  form() {
    108    return { actor: this.actorID };
    109  }
    110 
    111  writeError(error, typeName, method) {
    112    console.error(
    113      `Error while calling actor '${typeName}'s method '${method}'`,
    114      error.message || error
    115    );
    116    // Also log the error object as-is in order to log the server side stack
    117    // nicely in the console, while the previous log will log the client side stack only.
    118    if (error.stack) {
    119      console.error(error);
    120    }
    121 
    122    // Do not try to send the error if the actor is destroyed
    123    // as the connection is probably also destroyed and may throw.
    124    if (this.isDestroyed()) {
    125      return;
    126    }
    127 
    128    this.conn.send({
    129      from: this.actorID,
    130      // error.error -> errors created using the throwError() helper
    131      // error.name -> errors created using `new Error` or Components.exception
    132      // typeof(error)=="string" -> a method thrown like this `throw "a string"`
    133      error:
    134        error.error ||
    135        error.name ||
    136        (typeof error == "string" ? error : "unknownError"),
    137      message: error.message,
    138      // error.fileName -> regular Error instances
    139      // error.filename -> errors created using Components.exception
    140      fileName: error.fileName || error.filename,
    141      lineNumber: error.lineNumber,
    142      columnNumber: error.columnNumber,
    143      // Also pass the whole stack as string.
    144      //
    145      // "out of memory" string may be thrown by SpiderMonkey,
    146      // in which case getLastOOMStackTrace can return a last resort stack as a string.
    147      // https://searchfox.org/firefox-main/rev/33bba5cfe4a89dda0ee07fa9fbac578353713fd3/js/src/vm/JSContext.cpp#296-297
    148      stack:
    149        error == "out of memory"
    150          ? ChromeUtils.getLastOOMStackTrace()
    151          : error.stack,
    152    });
    153  }
    154 
    155  _queueResponse(create) {
    156    const pending = this._pendingResponse || Promise.resolve(null);
    157    const response = create(pending);
    158    this._pendingResponse = response;
    159  }
    160 
    161  /**
    162   * Throw an error with the passed message and attach an `error` property to the Error
    163   * object so it can be consumed by the writeError function.
    164   *
    165   * @param {string} error: A string (usually a single word serving as an id) that will
    166   *                        be assign to error.error.
    167   * @param {string} message: The string that will be passed to the Error constructor.
    168   * @throws This always throw.
    169   */
    170  throwError(error, message) {
    171    const err = new Error(message);
    172    err.error = error;
    173    throw err;
    174  }
    175 }
    176 
    177 exports.Actor = Actor;
    178 
    179 /**
    180 * Generate the "requestTypes" object used by DevToolsServerConnection to implement RDP.
    181 * When a RDP packet is received for calling an actor method, this lookup for
    182 * the method name in this object and call the function holded on this attribute.
    183 *
    184 * @param {object} actorSpec
    185 *         The procotol-js actor specific coming from devtools/shared/specs/*.js files
    186 *         This describes the types for methods and events implemented by all actors.
    187 * @return {object} requestTypes
    188 *         An object where attributes are actor method names
    189 *         and values are function implementing these methods.
    190 *         These methods receive a RDP Packet (JSON-serializable object) and a DevToolsServerConnection.
    191 *         We expect them to return a promise that reserves with the response object
    192 *         to send back to the client (JSON-serializable object).
    193 */
    194 var generateRequestTypes = function (actorSpec) {
    195  // Generate request handlers for each method definition
    196  const requestTypes = Object.create(null);
    197  actorSpec.methods.forEach(spec => {
    198    const handler = function (packet, conn) {
    199      try {
    200        const startTime = isWorker ? null : ChromeUtils.now();
    201        let args;
    202        try {
    203          args = spec.request.read(packet, this);
    204        } catch (ex) {
    205          console.error("Error reading request: " + packet.type);
    206          throw ex;
    207        }
    208 
    209        if (!this[spec.name]) {
    210          throw new Error(
    211            `Spec for '${actorSpec.typeName}' specifies a '${spec.name}'` +
    212              ` method that isn't implemented by the actor`
    213          );
    214        }
    215 
    216        // If this method is flaged to be returning a bulk response,
    217        // expose a method as last argument which will initiate the bulk response
    218        // and return a promise resolving to a StreamCopier instance.
    219        const isBulkResponse = spec.response.template === BULK_RESPONSE;
    220        if (isBulkResponse) {
    221          args.push(length => {
    222            return this.conn.startBulkSend({
    223              actor: this.actorID,
    224              length,
    225            });
    226          });
    227        }
    228        const ret = this[spec.name].apply(this, args);
    229 
    230        const sendReturn = retToSend => {
    231          if (spec.oneway) {
    232            // No need to send a response.
    233            return;
    234          }
    235          if (isBulkResponse) {
    236            if (retToSend) {
    237              throw new Actor(
    238                `Actor method '${this.typeName}.${spec.name}' is supposed to return a bulk response, but returned some value.`
    239              );
    240            }
    241            // Bulk response are one-way requests and are not replying any JSON packet.
    242            return;
    243          }
    244          if (this.isDestroyed()) {
    245            console.error(
    246              `Tried to send a '${spec.name}' method reply on an already destroyed actor` +
    247                ` '${this.typeName}'`
    248            );
    249            return;
    250          }
    251 
    252          let response;
    253          try {
    254            response = spec.response.write(retToSend, this);
    255          } catch (ex) {
    256            console.error("Error writing response to: " + spec.name);
    257            throw ex;
    258          }
    259          response.from = this.actorID;
    260          // If spec.release has been specified, destroy the object.
    261          if (spec.release) {
    262            try {
    263              this.destroy();
    264            } catch (e) {
    265              this.writeError(e, actorSpec.typeName, spec.name);
    266              return;
    267            }
    268          }
    269 
    270          conn.send(response);
    271 
    272          ChromeUtils.addProfilerMarker(
    273            "DevTools:RDP Actor",
    274            startTime,
    275            `${actorSpec.typeName}:${spec.name}()`
    276          );
    277        };
    278 
    279        this._queueResponse(p => {
    280          return p
    281            .then(() => ret)
    282            .then(sendReturn)
    283            .catch(e => this.writeError(e, actorSpec.typeName, spec.name));
    284        });
    285      } catch (e) {
    286        this._queueResponse(p => {
    287          return p.then(() =>
    288            this.writeError(e, actorSpec.typeName, spec.name)
    289          );
    290        });
    291      }
    292    };
    293 
    294    requestTypes[spec.request.type] = handler;
    295  });
    296 
    297  return requestTypes;
    298 };
    299 exports.generateRequestTypes = generateRequestTypes;