/* 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"; const { extend } = require("devtools/shared/extend"); var { Pool } = require("devtools/shared/protocol/Pool"); /** * 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 { // Existing Actors extending this class expect initialize to contain constructor logic. initialize(conn) { // Repeat Pool.constructor here as we can't call it from initialize // This is to be removed once actors switch to es classes and are able to call // Actor's contructor. if (conn) { this.conn = conn; } // Will contain the actor's ID this.actorID = null; this._actorSpec = actorSpecs.get(Object.getPrototypeOf(this)); // Forward events to the connection. if (this._actorSpec && this._actorSpec.events) { for (const [name, request] of this._actorSpec.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); } destroy() { super.destroy(); this.actorID = null; } isDestroyed() { return this.actorID === null; } /** * 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 ); if (error.stack) { console.error(error.stack); } // 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 error: error.error || error.name || "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; /** * Generates request handlers as described by the given actor specification on * the given actor prototype. Returns the actor prototype. */ var generateRequestHandlers = function(actorSpec, actorProto) { actorProto.typeName = actorSpec.typeName; // Generate request handlers for each method definition actorProto.requestTypes = Object.create(null); actorSpec.methods.forEach(spec => { const handler = function(packet, conn) { try { 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 '${actorProto.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, actorProto.typeName, spec.name); return; } } conn.send(response); }; this._queueResponse(p => { return p .then(() => ret) .then(sendReturn) .catch(e => this.writeError(e, actorProto.typeName, spec.name)); }); } catch (e) { this._queueResponse(p => { return p.then(() => this.writeError(e, actorProto.typeName, spec.name) ); }); } }; actorProto.requestTypes[spec.request.type] = handler; }); return actorProto; }; /** * Create an actor class for the given actor specification and prototype. * * @param object actorSpec * The actor specification. Must have a 'typeName' property. * @param object actorProto * The actor prototype. Should have method definitions, can have event * definitions. */ var ActorClassWithSpec = function(actorSpec, actorProto) { if (!actorSpec.typeName) { throw Error("Actor specification must have a typeName member."); } // Existing Actors are relying on the initialize instead of constructor methods. const cls = function() { const instance = Object.create(cls.prototype); instance.initialize.apply(instance, arguments); return instance; }; cls.prototype = extend( Actor.prototype, generateRequestHandlers(actorSpec, actorProto) ); actorSpecs.set(cls.prototype, actorSpec); return cls; }; exports.ActorClassWithSpec = ActorClassWithSpec;