diff options
Diffstat (limited to 'devtools/shared/protocol/Actor.js')
-rw-r--r-- | devtools/shared/protocol/Actor.js | 260 |
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; |