diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /devtools/shared/protocol/Actor.js | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared/protocol/Actor.js')
-rw-r--r-- | devtools/shared/protocol/Actor.js | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/devtools/shared/protocol/Actor.js b/devtools/shared/protocol/Actor.js new file mode 100644 index 0000000000..9ce009b7c1 --- /dev/null +++ b/devtools/shared/protocol/Actor.js @@ -0,0 +1,280 @@ +/* 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("resource://devtools/shared/extend.js"); +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 { + // 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); + + // 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; + +/** + * 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 { + 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 '${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); + + ChromeUtils.addProfilerMarker( + "DevTools:RDP Actor", + startTime, + `${actorSpec.typeName}:${spec.name}()` + ); + }; + + 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; |