290 lines
8.9 KiB
JavaScript
290 lines
8.9 KiB
JavaScript
/* 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");
|
|
|
|
// Lazy load this symbol in order to prevent a dependency cycle between Actor and types.
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"BULK_RESPONSE",
|
|
"resource://devtools/shared/protocol/types.js",
|
|
true
|
|
);
|
|
|
|
/**
|
|
* 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.
|
|
* @returns A jsonable object.
|
|
*/
|
|
form() {
|
|
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,
|
|
// Also pass the whole stack as string
|
|
stack: error.stack,
|
|
});
|
|
}
|
|
|
|
_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`
|
|
);
|
|
}
|
|
|
|
// If this method is flaged to be returning a bulk response,
|
|
// expose a method as last argument which will initiate the bulk response
|
|
// and return a promise resolving to a StreamCopier instance.
|
|
const isBulkResponse = spec.response.template === BULK_RESPONSE;
|
|
if (isBulkResponse) {
|
|
args.push(length => {
|
|
return this.conn.startBulkSend({
|
|
actor: this.actorID,
|
|
length,
|
|
});
|
|
});
|
|
}
|
|
const ret = this[spec.name].apply(this, args);
|
|
|
|
const sendReturn = retToSend => {
|
|
if (spec.oneway) {
|
|
// No need to send a response.
|
|
return;
|
|
}
|
|
if (isBulkResponse) {
|
|
if (retToSend) {
|
|
throw new Actor(
|
|
`Actor method '${this.typeName}.${spec.name}' is supposed to return a bulk response, but returned some value.`
|
|
);
|
|
}
|
|
// Bulk response are one-way requests and are not replying any JSON packet.
|
|
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;
|