summaryrefslogtreecommitdiffstats
path: root/devtools/shared/protocol/Actor.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/shared/protocol/Actor.js260
1 files changed, 260 insertions, 0 deletions
diff --git a/devtools/shared/protocol/Actor.js b/devtools/shared/protocol/Actor.js
new file mode 100644
index 0000000000..ea37f6fea2
--- /dev/null
+++ b/devtools/shared/protocol/Actor.js
@@ -0,0 +1,260 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { Pool } = require("resource://devtools/shared/protocol/Pool.js");
+
+/**
+ * Keep track of which actorSpecs have been created. If a replica of a spec
+ * is created, it can be caught, and specs which inherit from other specs will
+ * not overwrite eachother.
+ */
+var actorSpecs = new WeakMap();
+
+exports.actorSpecs = actorSpecs;
+
+/**
+ * An actor in the actor tree.
+ *
+ * @param optional conn
+ * Either a DevToolsServerConnection or a DevToolsClient. Must have
+ * addActorPool, removeActorPool, and poolFor.
+ * conn can be null if the subclass provides a conn property.
+ * @constructor
+ */
+
+class Actor extends Pool {
+ constructor(conn, spec) {
+ super(conn);
+
+ this.typeName = spec.typeName;
+
+ // Will contain the actor's ID
+ this.actorID = null;
+
+ // Ensure computing requestTypes only one time per class
+ const proto = Object.getPrototypeOf(this);
+ if (!proto.requestTypes) {
+ proto.requestTypes = generateRequestTypes(spec);
+ }
+
+ // Forward events to the connection.
+ if (spec.events) {
+ for (const [name, request] of spec.events.entries()) {
+ this.on(name, (...args) => {
+ this._sendEvent(name, request, ...args);
+ });
+ }
+ }
+ }
+
+ toString() {
+ return "[Actor " + this.typeName + "/" + this.actorID + "]";
+ }
+
+ _sendEvent(name, request, ...args) {
+ if (this.isDestroyed()) {
+ console.error(
+ `Tried to send a '${name}' event on an already destroyed actor` +
+ ` '${this.typeName}'`
+ );
+ return;
+ }
+ let packet;
+ try {
+ packet = request.write(args, this);
+ } catch (ex) {
+ console.error("Error sending event: " + name);
+ throw ex;
+ }
+ packet.from = packet.from || this.actorID;
+ this.conn.send(packet);
+
+ // This can really be a hot path, even computing the marker label can
+ // have some performance impact.
+ // Guard against missing `Services.profiler` because Services is mocked to
+ // an empty object in the worker loader.
+ if (Services.profiler?.IsActive()) {
+ ChromeUtils.addProfilerMarker(
+ "DevTools:RDP Actor",
+ null,
+ `${this.typeName}.${name}`
+ );
+ }
+ }
+
+ destroy() {
+ super.destroy();
+ this.actorID = null;
+ this._isDestroyed = true;
+ }
+
+ /**
+ * Override this method in subclasses to serialize the actor.
+ * @param [optional] string hint
+ * Optional string to customize the form.
+ * @returns A jsonable object.
+ */
+ form(hint) {
+ return { actor: this.actorID };
+ }
+
+ writeError(error, typeName, method) {
+ console.error(
+ `Error while calling actor '${typeName}'s method '${method}'`,
+ error.message || error
+ );
+ // Also log the error object as-is in order to log the server side stack
+ // nicely in the console, while the previous log will log the client side stack only.
+ if (error.stack) {
+ console.error(error);
+ }
+
+ // Do not try to send the error if the actor is destroyed
+ // as the connection is probably also destroyed and may throw.
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ this.conn.send({
+ from: this.actorID,
+ // error.error -> errors created using the throwError() helper
+ // error.name -> errors created using `new Error` or Components.exception
+ // typeof(error)=="string" -> a method thrown like this `throw "a string"`
+ error:
+ error.error ||
+ error.name ||
+ (typeof error == "string" ? error : "unknownError"),
+ message: error.message,
+ // error.fileName -> regular Error instances
+ // error.filename -> errors created using Components.exception
+ fileName: error.fileName || error.filename,
+ lineNumber: error.lineNumber,
+ columnNumber: error.columnNumber,
+ });
+ }
+
+ _queueResponse(create) {
+ const pending = this._pendingResponse || Promise.resolve(null);
+ const response = create(pending);
+ this._pendingResponse = response;
+ }
+
+ /**
+ * Throw an error with the passed message and attach an `error` property to the Error
+ * object so it can be consumed by the writeError function.
+ * @param {String} error: A string (usually a single word serving as an id) that will
+ * be assign to error.error.
+ * @param {String} message: The string that will be passed to the Error constructor.
+ * @throws This always throw.
+ */
+ throwError(error, message) {
+ const err = new Error(message);
+ err.error = error;
+ throw err;
+ }
+}
+
+exports.Actor = Actor;
+
+/**
+ * Generate the "requestTypes" object used by DevToolsServerConnection to implement RDP.
+ * When a RDP packet is received for calling an actor method, this lookup for
+ * the method name in this object and call the function holded on this attribute.
+ *
+ * @params {Object} actorSpec
+ * The procotol-js actor specific coming from devtools/shared/specs/*.js files
+ * This describes the types for methods and events implemented by all actors.
+ * @return {Object} requestTypes
+ * An object where attributes are actor method names
+ * and values are function implementing these methods.
+ * These methods receive a RDP Packet (JSON-serializable object) and a DevToolsServerConnection.
+ * We expect them to return a promise that reserves with the response object
+ * to send back to the client (JSON-serializable object).
+ */
+var generateRequestTypes = function (actorSpec) {
+ // Generate request handlers for each method definition
+ const requestTypes = Object.create(null);
+ actorSpec.methods.forEach(spec => {
+ const handler = function (packet, conn) {
+ try {
+ const startTime = isWorker ? null : Cu.now();
+ let args;
+ try {
+ args = spec.request.read(packet, this);
+ } catch (ex) {
+ console.error("Error reading request: " + packet.type);
+ throw ex;
+ }
+
+ if (!this[spec.name]) {
+ throw new Error(
+ `Spec for '${actorSpec.typeName}' specifies a '${spec.name}'` +
+ ` method that isn't implemented by the actor`
+ );
+ }
+ const ret = this[spec.name].apply(this, args);
+
+ const sendReturn = retToSend => {
+ if (spec.oneway) {
+ // No need to send a response.
+ return;
+ }
+ if (this.isDestroyed()) {
+ console.error(
+ `Tried to send a '${spec.name}' method reply on an already destroyed actor` +
+ ` '${this.typeName}'`
+ );
+ return;
+ }
+
+ let response;
+ try {
+ response = spec.response.write(retToSend, this);
+ } catch (ex) {
+ console.error("Error writing response to: " + spec.name);
+ throw ex;
+ }
+ response.from = this.actorID;
+ // If spec.release has been specified, destroy the object.
+ if (spec.release) {
+ try {
+ this.destroy();
+ } catch (e) {
+ this.writeError(e, actorSpec.typeName, spec.name);
+ return;
+ }
+ }
+
+ conn.send(response);
+
+ ChromeUtils.addProfilerMarker(
+ "DevTools:RDP Actor",
+ startTime,
+ `${actorSpec.typeName}:${spec.name}()`
+ );
+ };
+
+ this._queueResponse(p => {
+ return p
+ .then(() => ret)
+ .then(sendReturn)
+ .catch(e => this.writeError(e, actorSpec.typeName, spec.name));
+ });
+ } catch (e) {
+ this._queueResponse(p => {
+ return p.then(() =>
+ this.writeError(e, actorSpec.typeName, spec.name)
+ );
+ });
+ }
+ };
+
+ requestTypes[spec.request.type] = handler;
+ });
+
+ return requestTypes;
+};
+exports.generateRequestTypes = generateRequestTypes;