gecko-dev/devtools/shared/protocol.js

1489 lines
40 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 { Cu, components } = require("chrome");
var Services = require("Services");
var promise = require("promise");
var defer = require("devtools/shared/defer");
var {Class} = require("sdk/core/heritage");
var {EventTarget} = require("sdk/event/target");
var events = require("sdk/event/core");
var object = require("sdk/util/object");
exports.emit = events.emit;
/**
* Types: named marshallers/demarshallers.
*
* Types provide a 'write' function that takes a js representation and
* returns a protocol representation, and a "read" function that
* takes a protocol representation and returns a js representation.
*
* The read and write methods are also passed a context object that
* represent the actor or front requesting the translation.
*
* Types are referred to with a typestring. Basic types are
* registered by name using addType, and more complex types can
* be generated by adding detail to the type name.
*/
var types = Object.create(null);
exports.types = types;
var registeredTypes = types.registeredTypes = new Map();
var registeredLifetimes = types.registeredLifetimes = new Map();
/**
* Return the type object associated with a given typestring.
* If passed a type object, it will be returned unchanged.
*
* Types can be registered with addType, or can be created on
* the fly with typestrings. Examples:
*
* boolean
* threadActor
* threadActor#detail
* array:threadActor
* array:array:threadActor#detail
*
* @param [typestring|type] type
* Either a typestring naming a type or a type object.
*
* @returns a type object.
*/
types.getType = function (type) {
if (!type) {
return types.Primitive;
}
if (typeof (type) !== "string") {
return type;
}
// If already registered, we're done here.
let reg = registeredTypes.get(type);
if (reg) return reg;
// New type, see if it's a collection/lifetime type:
let sep = type.indexOf(":");
if (sep >= 0) {
let collection = type.substring(0, sep);
let subtype = types.getType(type.substring(sep + 1));
if (collection === "array") {
return types.addArrayType(subtype);
} else if (collection === "nullable") {
return types.addNullableType(subtype);
}
if (registeredLifetimes.has(collection)) {
return types.addLifetimeType(collection, subtype);
}
throw Error("Unknown collection type: " + collection);
}
// Not a collection, might be actor detail
let pieces = type.split("#", 2);
if (pieces.length > 1) {
return types.addActorDetail(type, pieces[0], pieces[1]);
}
// Might be a lazily-loaded type
if (type === "longstring") {
require("devtools/shared/specs/string");
return registeredTypes.get("longstring");
}
throw Error("Unknown type: " + type);
};
/**
* Don't allow undefined when writing primitive types to packets. If
* you want to allow undefined, use a nullable type.
*/
function identityWrite(v) {
if (v === undefined) {
throw Error("undefined passed where a value is required");
}
// This has to handle iterator->array conversion because arrays of
// primitive types pass through here.
if (v && typeof (v) === "object" && Symbol.iterator in v) {
return [...v];
}
return v;
}
/**
* Add a type to the type system.
*
* When registering a type, you can provide `read` and `write` methods.
*
* The `read` method will be passed a JS object value from the JSON
* packet and must return a native representation. The `write` method will
* be passed a native representation and should provide a JSONable value.
*
* These methods will both be passed a context. The context is the object
* performing or servicing the request - on the server side it will be
* an Actor, on the client side it will be a Front.
*
* @param typestring name
* Name to register
* @param object typeObject
* An object whose properties will be stored in the type, including
* the `read` and `write` methods.
* @param object options
* Can specify `thawed` to prevent the type from being frozen.
*
* @returns a type object that can be used in protocol definitions.
*/
types.addType = function (name, typeObject = {}, options = {}) {
if (registeredTypes.has(name)) {
throw Error("Type '" + name + "' already exists.");
}
let type = object.merge({
toString() { return "[protocol type:" + name + "]";},
name: name,
primitive: !(typeObject.read || typeObject.write),
read: identityWrite,
write: identityWrite
}, typeObject);
registeredTypes.set(name, type);
return type;
};
/**
* Remove a type previously registered with the system.
* Primarily useful for types registered by addons.
*/
types.removeType = function (name) {
// This type may still be referenced by other types, make sure
// those references don't work.
let type = registeredTypes.get(name);
type.name = "DEFUNCT:" + name;
type.category = "defunct";
type.primitive = false;
type.read = type.write = function () { throw new Error("Using defunct type: " + name); };
registeredTypes.delete(name);
};
/**
* Add an array type to the type system.
*
* getType() will call this function if provided an "array:<type>"
* typestring.
*
* @param type subtype
* The subtype to be held by the array.
*/
types.addArrayType = function (subtype) {
subtype = types.getType(subtype);
let name = "array:" + subtype.name;
// Arrays of primitive types are primitive types themselves.
if (subtype.primitive) {
return types.addType(name);
}
return types.addType(name, {
category: "array",
read: (v, ctx) => [...v].map(i => subtype.read(i, ctx)),
write: (v, ctx) => [...v].map(i => subtype.write(i, ctx))
});
};
/**
* Add a dict type to the type system. This allows you to serialize
* a JS object that contains non-primitive subtypes.
*
* Properties of the value that aren't included in the specializations
* will be serialized as primitive values.
*
* @param object specializations
* A dict of property names => type
*/
types.addDictType = function (name, specializations) {
return types.addType(name, {
category: "dict",
specializations: specializations,
read: (v, ctx) => {
let ret = {};
for (let prop in v) {
if (prop in specializations) {
ret[prop] = types.getType(specializations[prop]).read(v[prop], ctx);
} else {
ret[prop] = v[prop];
}
}
return ret;
},
write: (v, ctx) => {
let ret = {};
for (let prop in v) {
if (prop in specializations) {
ret[prop] = types.getType(specializations[prop]).write(v[prop], ctx);
} else {
ret[prop] = v[prop];
}
}
return ret;
}
});
};
/**
* Register an actor type with the type system.
*
* Types are marshalled differently when communicating server->client
* than they are when communicating client->server. The server needs
* to provide useful information to the client, so uses the actor's
* `form` method to get a json representation of the actor. When
* making a request from the client we only need the actor ID string.
*
* This function can be called before the associated actor has been
* constructed, but the read and write methods won't work until
* the associated addActorImpl or addActorFront methods have been
* called during actor/front construction.
*
* @param string name
* The typestring to register.
*/
types.addActorType = function (name) {
let type = types.addType(name, {
_actor: true,
category: "actor",
read: (v, ctx, detail) => {
// If we're reading a request on the server side, just
// find the actor registered with this actorID.
if (ctx instanceof Actor) {
return ctx.conn.getActor(v);
}
// Reading a response on the client side, check for an
// existing front on the connection, and create the front
// if it isn't found.
let actorID = typeof (v) === "string" ? v : v.actor;
let front = ctx.conn.getActor(actorID);
if (!front) {
front = new type.frontClass(ctx.conn);
front.actorID = actorID;
ctx.marshallPool().manage(front);
}
v = type.formType(detail).read(v, front, detail);
front.form(v, detail, ctx);
return front;
},
write: (v, ctx, detail) => {
// If returning a response from the server side, make sure
// the actor is added to a parent object and return its form.
if (v instanceof Actor) {
if (!v.actorID) {
ctx.marshallPool().manage(v);
}
return type.formType(detail).write(v.form(detail), ctx, detail);
}
// Writing a request from the client side, just send the actor id.
return v.actorID;
},
formType: (detail) => {
if (!("formType" in type.actorSpec)) {
return types.Primitive;
}
let formAttr = "formType";
if (detail) {
formAttr += "#" + detail;
}
if (!(formAttr in type.actorSpec)) {
throw new Error("No type defined for " + formAttr);
}
return type.actorSpec[formAttr];
}
});
return type;
};
types.addNullableType = function (subtype) {
subtype = types.getType(subtype);
return types.addType("nullable:" + subtype.name, {
category: "nullable",
read: (value, ctx) => {
if (value == null) {
return value;
}
return subtype.read(value, ctx);
},
write: (value, ctx) => {
if (value == null) {
return value;
}
return subtype.write(value, ctx);
}
});
};
/**
* Register an actor detail type. This is just like an actor type, but
* will pass a detail hint to the actor's form method during serialization/
* deserialization.
*
* This is called by getType() when passed an 'actorType#detail' string.
*
* @param string name
* The typestring to register this type as.
* @param type actorType
* The actor type you'll be detailing.
* @param string detail
* The detail to pass.
*/
types.addActorDetail = function (name, actorType, detail) {
actorType = types.getType(actorType);
if (!actorType._actor) {
throw Error("Details only apply to actor types, tried to add detail '" + detail + "'' to " + actorType.name + "\n");
}
return types.addType(name, {
_actor: true,
category: "detail",
read: (v, ctx) => actorType.read(v, ctx, detail),
write: (v, ctx) => actorType.write(v, ctx, detail)
});
};
/**
* Register an actor lifetime. This lets the type system find a parent
* actor that differs from the actor fulfilling the request.
*
* @param string name
* The lifetime name to use in typestrings.
* @param string prop
* The property of the actor that holds the parent that should be used.
*/
types.addLifetime = function (name, prop) {
if (registeredLifetimes.has(name)) {
throw Error("Lifetime '" + name + "' already registered.");
}
registeredLifetimes.set(name, prop);
};
/**
* Remove a previously-registered lifetime. Useful for lifetimes registered
* in addons.
*/
types.removeLifetime = function (name) {
registeredLifetimes.delete(name);
};
/**
* Register a lifetime type. This creates an actor type tied to the given
* lifetime.
*
* This is called by getType() when passed a '<lifetimeType>:<actorType>'
* typestring.
*
* @param string lifetime
* A lifetime string previously regisered with addLifetime()
* @param type subtype
* An actor type
*/
types.addLifetimeType = function (lifetime, subtype) {
subtype = types.getType(subtype);
if (!subtype._actor) {
throw Error("Lifetimes only apply to actor types, tried to apply lifetime '" + lifetime + "'' to " + subtype.name);
}
let prop = registeredLifetimes.get(lifetime);
return types.addType(lifetime + ":" + subtype.name, {
category: "lifetime",
read: (value, ctx) => subtype.read(value, ctx[prop]),
write: (value, ctx) => subtype.write(value, ctx[prop])
});
};
// Add a few named primitive types.
types.Primitive = types.addType("primitive");
types.String = types.addType("string");
types.Number = types.addType("number");
types.Boolean = types.addType("boolean");
types.JSON = types.addType("json");
/**
* Request/Response templates and generation
*
* Request packets are specified as json templates with
* Arg and Option placeholders where arguments should be
* placed.
*
* Reponse packets are also specified as json templates,
* with a RetVal placeholder where the return value should be
* placed.
*/
/**
* Placeholder for simple arguments.
*
* @param number index
* The argument index to place at this position.
* @param type type
* The argument should be marshalled as this type.
* @constructor
*/
var Arg = Class({
initialize: function (index, type) {
this.index = index;
this.type = types.getType(type);
},
write: function (arg, ctx) {
return this.type.write(arg, ctx);
},
read: function (v, ctx, outArgs) {
outArgs[this.index] = this.type.read(v, ctx);
},
describe: function () {
return {
_arg: this.index,
type: this.type.name,
};
}
});
exports.Arg = Arg;
/**
* Placeholder for an options argument value that should be hoisted
* into the packet.
*
* If provided in a method specification:
*
* { optionArg: Option(1)}
*
* Then arguments[1].optionArg will be placed in the packet in this
* value's place.
*
* @param number index
* The argument index of the options value.
* @param type type
* The argument should be marshalled as this type.
* @constructor
*/
var Option = Class({
extends: Arg,
initialize: function (index, type) {
Arg.prototype.initialize.call(this, index, type);
},
write: function (arg, ctx, name) {
// Ignore if arg is undefined or null; allow other falsy values
if (arg == undefined || arg[name] == undefined) {
return undefined;
}
let v = arg[name];
return this.type.write(v, ctx);
},
read: function (v, ctx, outArgs, name) {
if (outArgs[this.index] === undefined) {
outArgs[this.index] = {};
}
if (v === undefined) {
return;
}
outArgs[this.index][name] = this.type.read(v, ctx);
},
describe: function () {
return {
_option: this.index,
type: this.type.name,
};
}
});
exports.Option = Option;
/**
* Placeholder for return values in a response template.
*
* @param type type
* The return value should be marshalled as this type.
*/
var RetVal = Class({
initialize: function (type) {
this.type = types.getType(type);
},
write: function (v, ctx) {
return this.type.write(v, ctx);
},
read: function (v, ctx) {
return this.type.read(v, ctx);
},
describe: function () {
return {
_retval: this.type.name
};
}
});
exports.RetVal = RetVal;
/* Template handling functions */
/**
* Get the value at a given path, or undefined if not found.
*/
function getPath(obj, path) {
for (let name of path) {
if (!(name in obj)) {
return undefined;
}
obj = obj[name];
}
return obj;
}
/**
* Find Placeholders in the template and save them along with their
* paths.
*/
function findPlaceholders(template, constructor, path = [], placeholders = []) {
if (!template || typeof (template) != "object") {
return placeholders;
}
if (template instanceof constructor) {
placeholders.push({ placeholder: template, path: [...path] });
return placeholders;
}
for (let name in template) {
path.push(name);
findPlaceholders(template[name], constructor, path, placeholders);
path.pop();
}
return placeholders;
}
function describeTemplate(template) {
return JSON.parse(JSON.stringify(template, (key, value) => {
if (value.describe) {
return value.describe();
}
return value;
}));
}
/**
* Manages a request template.
*
* @param object template
* The request template.
* @construcor
*/
var Request = Class({
initialize: function (template = {}) {
this.type = template.type;
this.template = template;
this.args = findPlaceholders(template, Arg);
},
/**
* Write a request.
*
* @param array fnArgs
* The function arguments to place in the request.
* @param object ctx
* The object making the request.
* @returns a request packet.
*/
write: function (fnArgs, ctx) {
let str = JSON.stringify(this.template, (key, value) => {
if (value instanceof Arg) {
return value.write(value.index in fnArgs ? fnArgs[value.index] : undefined,
ctx, key);
}
return value;
});
return JSON.parse(str);
},
/**
* Read a request.
*
* @param object packet
* The request packet.
* @param object ctx
* The object making the request.
* @returns an arguments array
*/
read: function (packet, ctx) {
let fnArgs = [];
for (let templateArg of this.args) {
let arg = templateArg.placeholder;
let path = templateArg.path;
let name = path[path.length - 1];
arg.read(getPath(packet, path), ctx, fnArgs, name);
}
return fnArgs;
},
describe: function () { return describeTemplate(this.template); }
});
/**
* Manages a response template.
*
* @param object template
* The response template.
* @construcor
*/
var Response = Class({
initialize: function (template = {}) {
this.template = template;
let placeholders = findPlaceholders(template, RetVal);
if (placeholders.length > 1) {
throw Error("More than one RetVal specified in response");
}
let placeholder = placeholders.shift();
if (placeholder) {
this.retVal = placeholder.placeholder;
this.path = placeholder.path;
}
},
/**
* Write a response for the given return value.
*
* @param val ret
* The return value.
* @param object ctx
* The object writing the response.
*/
write: function (ret, ctx) {
return JSON.parse(JSON.stringify(this.template, function (key, value) {
if (value instanceof RetVal) {
return value.write(ret, ctx);
}
return value;
}));
},
/**
* Read a return value from the given response.
*
* @param object packet
* The response packet.
* @param object ctx
* The object reading the response.
*/
read: function (packet, ctx) {
if (!this.retVal) {
return undefined;
}
let v = getPath(packet, this.path);
return this.retVal.read(v, ctx);
},
describe: function () { return describeTemplate(this.template); }
});
/**
* Actor and Front implementations
*/
/**
* A protocol object that can manage the lifetime of other protocol
* objects.
*/
var Pool = Class({
extends: EventTarget,
/**
* Pools are used on both sides of the connection to help coordinate
* lifetimes.
*
* @param optional conn
* Either a DebuggerServerConnection or a DebuggerClient. Must have
* addActorPool, removeActorPool, and poolFor.
* conn can be null if the subclass provides a conn property.
* @constructor
*/
initialize: function (conn) {
if (conn) {
this.conn = conn;
}
},
/**
* Return the parent pool for this client.
*/
parent: function () { return this.conn.poolFor(this.actorID); },
/**
* Override this if you want actors returned by this actor
* to belong to a different actor by default.
*/
marshallPool: function () { return this; },
/**
* Pool is the base class for all actors, even leaf nodes.
* If the child map is actually referenced, go ahead and create
* the stuff needed by the pool.
*/
__poolMap: null,
get _poolMap() {
if (this.__poolMap) return this.__poolMap;
this.__poolMap = new Map();
this.conn.addActorPool(this);
return this.__poolMap;
},
/**
* Add an actor as a child of this pool.
*/
manage: function (actor) {
if (!actor.actorID) {
actor.actorID = this.conn.allocID(actor.actorPrefix || actor.typeName);
}
this._poolMap.set(actor.actorID, actor);
return actor;
},
/**
* Remove an actor as a child of this pool.
*/
unmanage: function (actor) {
this.__poolMap && this.__poolMap.delete(actor.actorID);
},
// true if the given actor ID exists in the pool.
has: function (actorID) {
return this.__poolMap && this._poolMap.has(actorID);
},
// The actor for a given actor id stored in this pool
actor: function (actorID) {
return this.__poolMap ? this._poolMap.get(actorID) : null;
},
// Same as actor, should update debugger connection to use 'actor'
// and then remove this.
get: function (actorID) {
return this.__poolMap ? this._poolMap.get(actorID) : null;
},
// True if this pool has no children.
isEmpty: function () {
return !this.__poolMap || this._poolMap.size == 0;
},
/**
* Destroy this item, removing it from a parent if it has one,
* and destroying all children if necessary.
*/
destroy: function () {
let parent = this.parent();
if (parent) {
parent.unmanage(this);
}
if (!this.__poolMap) {
return;
}
for (let actor of this.__poolMap.values()) {
// Self-owned actors are ok, but don't need destroying twice.
if (actor === this) {
continue;
}
let destroy = actor.destroy;
if (destroy) {
// Disconnect destroy while we're destroying in case of (misbehaving)
// circular ownership.
actor.destroy = null;
destroy.call(actor);
actor.destroy = destroy;
}
}
this.conn.removeActorPool(this, true);
this.__poolMap.clear();
this.__poolMap = null;
},
/**
* For getting along with the debugger server pools, should be removable
* eventually.
*/
cleanup: function () {
this.destroy();
}
});
exports.Pool = Pool;
/**
* An actor in the actor tree.
*/
var Actor = Class({
extends: Pool,
// Will contain the actor's ID
actorID: null,
/**
* Initialize an actor.
*
* @param optional conn
* Either a DebuggerServerConnection or a DebuggerClient. Must have
* addActorPool, removeActorPool, and poolFor.
* conn can be null if the subclass provides a conn property.
* @constructor
*/
initialize: function (conn) {
Pool.prototype.initialize.call(this, conn);
// Forward events to the connection.
if (this._actorSpec && this._actorSpec.events) {
for (let key of this._actorSpec.events.keys()) {
let name = key;
let sendEvent = this._sendEvent.bind(this, name);
this.on(name, (...args) => {
sendEvent.apply(null, args);
});
}
}
},
toString: function () { return "[Actor " + this.typeName + "/" + this.actorID + "]"; },
_sendEvent: function (name, ...args) {
if (!this._actorSpec.events.has(name)) {
// It's ok to emit events that don't go over the wire.
return;
}
let request = this._actorSpec.events.get(name);
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: function () {
Pool.prototype.destroy.call(this);
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: function (hint) {
return { actor: this.actorID };
},
writeError: function (error) {
console.error(error);
if (error.stack) {
dump(error.stack);
}
this.conn.send({
from: this.actorID,
error: error.error || "unknownError",
message: error.message
});
},
_queueResponse: function (create) {
let pending = this._pendingResponse || promise.resolve(null);
let response = create(pending);
this._pendingResponse = response;
}
});
exports.Actor = Actor;
/**
* Tags a prtotype method as an actor method implementation.
*
* @param function fn
* The implementation function, will be returned.
* @param spec
* The method specification, with the following (optional) properties:
* request (object): a request template.
* response (object): a response template.
* oneway (bool): 'true' if no response should be sent.
*/
exports.method = function (fn, spec = {}) {
fn._methodSpec = Object.freeze(spec);
if (spec.request) Object.freeze(spec.request);
if (spec.response) Object.freeze(spec.response);
return fn;
};
/**
* Generates an actor specification from an actor description.
*/
var generateActorSpec = function (actorDesc) {
let actorSpec = {
typeName: actorDesc.typeName,
methods: []
};
// Find method and form specifications attached to properties.
for (let name of Object.getOwnPropertyNames(actorDesc)) {
let desc = Object.getOwnPropertyDescriptor(actorDesc, name);
if (!desc.value) {
continue;
}
if (name.startsWith("formType")) {
if (typeof (desc.value) === "string") {
actorSpec[name] = types.getType(desc.value);
} else if (desc.value.name && registeredTypes.has(desc.value.name)) {
actorSpec[name] = desc.value;
} else {
// Shorthand for a newly-registered DictType.
actorSpec[name] = types.addDictType(actorDesc.typeName + "__" + name, desc.value);
}
}
if (desc.value._methodSpec) {
let methodSpec = desc.value._methodSpec;
let spec = {};
spec.name = methodSpec.name || name;
spec.request = Request(object.merge({type: spec.name}, methodSpec.request || undefined));
spec.response = Response(methodSpec.response || undefined);
spec.release = methodSpec.release;
spec.oneway = methodSpec.oneway;
actorSpec.methods.push(spec);
}
}
// Find additional method specifications
if (actorDesc.methods) {
for (let name in actorDesc.methods) {
let methodSpec = actorDesc.methods[name];
let spec = {};
spec.name = methodSpec.name || name;
spec.request = Request(object.merge({type: spec.name}, methodSpec.request || undefined));
spec.response = Response(methodSpec.response || undefined);
spec.release = methodSpec.release;
spec.oneway = methodSpec.oneway;
actorSpec.methods.push(spec);
}
}
// Find event specifications
if (actorDesc.events) {
actorSpec.events = new Map();
for (let name in actorDesc.events) {
let eventRequest = actorDesc.events[name];
Object.freeze(eventRequest);
actorSpec.events.set(name, Request(object.merge({type: name}, eventRequest)));
}
}
if (!registeredTypes.has(actorSpec.typeName)) {
types.addActorType(actorSpec.typeName);
}
registeredTypes.get(actorSpec.typeName).actorSpec = actorSpec;
return actorSpec;
};
exports.generateActorSpec = generateActorSpec;
/**
* Generates request handlers as described by the given actor specification on
* the given actor prototype. Returns the actor prototype.
*/
var generateRequestHandlers = function (actorSpec, actorProto) {
if (actorProto._actorSpec) {
throw new Error("actorProto called twice on the same actor prototype!");
}
actorProto.typeName = actorSpec.typeName;
// Generate request handlers for each method definition
actorProto.requestTypes = Object.create(null);
actorSpec.methods.forEach(spec => {
let 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;
}
let ret = this[spec.name].apply(this, args);
let sendReturn = (ret) => {
if (spec.oneway) {
// No need to send a response.
return;
}
let response;
try {
response = spec.response.write(ret, 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);
return;
}
}
conn.send(response);
};
this._queueResponse(p => {
return p
.then(() => ret)
.then(sendReturn)
.then(null, this.writeError.bind(this));
});
} catch (e) {
this._queueResponse(p => {
return p.then(() => this.writeError(e));
});
}
};
actorProto.requestTypes[spec.request.type] = handler;
});
actorProto._actorSpec = actorSpec;
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 ActorClass = function (actorSpec, actorProto) {
if (!actorSpec.typeName) {
throw Error("Actor specification must have a typeName member.");
}
actorProto.extends = Actor;
let cls = Class(generateRequestHandlers(actorSpec, actorProto));
return cls;
};
exports.ActorClass = ActorClass;
/**
* Base class for client-side actor fronts.
*/
var Front = Class({
extends: Pool,
actorID: null,
/**
* The base class for client-side actor fronts.
*
* @param optional conn
* Either a DebuggerServerConnection or a DebuggerClient. Must have
* addActorPool, removeActorPool, and poolFor.
* conn can be null if the subclass provides a conn property.
* @param optional form
* The json form provided by the server.
* @constructor
*/
initialize: function (conn = null, form = null, detail = null, context = null) {
Pool.prototype.initialize.call(this, conn);
this._requests = [];
// protocol.js no longer uses this data in the constructor, only external
// uses do. External usage of manually-constructed fronts will be
// drastically reduced if we convert the root and tab actors to
// protocol.js, in which case this can probably go away.
if (form) {
this.actorID = form.actor;
form = types.getType(this.typeName).formType(detail).read(form, this, detail);
this.form(form, detail, context);
}
},
destroy: function () {
// Reject all outstanding requests, they won't make sense after
// the front is destroyed.
while (this._requests && this._requests.length > 0) {
let { deferred, to, type, stack } = this._requests.shift();
let msg = "Connection closed, pending request to " + to +
", type " + type + " failed" +
"\n\nRequest stack:\n" + stack.formattedStack;
deferred.reject(new Error(msg));
}
Pool.prototype.destroy.call(this);
this.actorID = null;
},
manage: function (front) {
if (!front.actorID) {
throw new Error("Can't manage front without an actor ID.\n" +
"Ensure server supports " + front.typeName + ".");
}
return Pool.prototype.manage.call(this, front);
},
/**
* @returns a promise that will resolve to the actorID this front
* represents.
*/
actor: function () { return promise.resolve(this.actorID); },
toString: function () { return "[Front for " + this.typeName + "/" + this.actorID + "]"; },
/**
* Update the actor from its representation.
* Subclasses should override this.
*/
form: function (form) {},
/**
* Send a packet on the connection.
*/
send: function (packet) {
if (packet.to) {
this.conn._transport.send(packet);
} else {
this.actor().then(actorID => {
packet.to = actorID;
this.conn._transport.send(packet);
}).then(null, e => console.error(e));
}
},
/**
* Send a two-way request on the connection.
*/
request: function (packet) {
let deferred = defer();
// Save packet basics for debugging
let { to, type } = packet;
this._requests.push({
deferred,
to: to || this.actorID,
type,
stack: components.stack,
});
this.send(packet);
return deferred.promise;
},
/**
* Handler for incoming packets from the client's actor.
*/
onPacket: function (packet) {
// Pick off event packets
let type = packet.type || undefined;
if (this._clientSpec.events && this._clientSpec.events.has(type)) {
let event = this._clientSpec.events.get(packet.type);
let args;
try {
args = event.request.read(packet, this);
} catch (ex) {
console.error("Error reading event: " + packet.type);
console.exception(ex);
throw ex;
}
if (event.pre) {
let results = event.pre.map(pre => pre.apply(this, args));
// Check to see if any of the preEvents returned a promise -- if so,
// wait for their resolution before emitting. Otherwise, emit synchronously.
if (results.some(result => result && typeof result.then === "function")) {
promise.all(results).then(() => events.emit.apply(null, [this, event.name].concat(args)));
return;
}
}
events.emit.apply(null, [this, event.name].concat(args));
return;
}
// Remaining packets must be responses.
if (this._requests.length === 0) {
let msg = "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
let err = Error(msg);
console.error(err);
throw err;
}
let { deferred, stack } = this._requests.shift();
Cu.callFunctionWithAsyncStack(() => {
if (packet.error) {
// "Protocol error" is here to avoid TBPL heuristics. See also
// https://mxr.mozilla.org/webtools-central/source/tbpl/php/inc/GeneralErrorFilter.php
let message;
if (packet.error && packet.message) {
message = "Protocol error (" + packet.error + "): " + packet.message;
} else {
message = packet.error;
}
deferred.reject(message);
} else {
deferred.resolve(packet);
}
}, stack, "DevTools RDP");
}
});
exports.Front = Front;
/**
* A method tagged with preEvent will be called after recieving a packet
* for that event, and before the front emits the event.
*/
exports.preEvent = function (eventName, fn) {
fn._preEvent = eventName;
return fn;
};
/**
* Mark a method as a custom front implementation, replacing the generated
* front method.
*
* @param function fn
* The front implementation, will be returned.
* @param object options
* Options object:
* impl (string): If provided, the generated front method will be
* stored as this property on the prototype.
*/
exports.custom = function (fn, options = {}) {
fn._customFront = options;
return fn;
};
function prototypeOf(obj) {
return typeof (obj) === "function" ? obj.prototype : obj;
}
/**
* Generates request methods as described by the given actor specification on
* the given front prototype. Returns the front prototype.
*/
var generateRequestMethods = function (actorSpec, frontProto) {
if (frontProto._actorSpec) {
throw new Error("frontProto called twice on the same front prototype!");
}
frontProto.typeName = actorSpec.typeName;
// Generate request methods.
let methods = actorSpec.methods;
methods.forEach(spec => {
let name = spec.name;
// If there's already a property by this name in the front, it must
// be a custom front method.
if (name in frontProto) {
let custom = frontProto[spec.name]._customFront;
if (custom === undefined) {
throw Error("Existing method for " + spec.name + " not marked customFront while processing " + actorType.typeName + ".");
}
// If the user doesn't need the impl don't generate it.
if (!custom.impl) {
return;
}
name = custom.impl;
}
frontProto[name] = function (...args) {
let packet;
try {
packet = spec.request.write(args, this);
} catch (ex) {
console.error("Error writing request: " + name);
throw ex;
}
if (spec.oneway) {
// Fire-and-forget oneway packets.
this.send(packet);
return undefined;
}
return this.request(packet).then(response => {
let ret;
try {
ret = spec.response.read(response, this);
} catch (ex) {
console.error("Error reading response to: " + name);
throw ex;
}
return ret;
});
};
// Release methods should call the destroy function on return.
if (spec.release) {
let fn = frontProto[name];
frontProto[name] = function (...args) {
return fn.apply(this, args).then(result => {
this.destroy();
return result;
});
};
}
});
// Process event specifications
frontProto._clientSpec = {};
let events = actorSpec.events;
if (events) {
// This actor has events, scan the prototype for preEvent handlers...
let preHandlers = new Map();
for (let name of Object.getOwnPropertyNames(frontProto)) {
let desc = Object.getOwnPropertyDescriptor(frontProto, name);
if (!desc.value) {
continue;
}
if (desc.value._preEvent) {
let preEvent = desc.value._preEvent;
if (!events.has(preEvent)) {
throw Error("preEvent for event that doesn't exist: " + preEvent);
}
let handlers = preHandlers.get(preEvent);
if (!handlers) {
handlers = [];
preHandlers.set(preEvent, handlers);
}
handlers.push(desc.value);
}
}
frontProto._clientSpec.events = new Map();
for (let [name, request] of events) {
frontProto._clientSpec.events.set(request.type, {
name: name,
request: request,
pre: preHandlers.get(name)
});
}
}
frontProto._actorSpec = actorSpec;
return frontProto;
};
/**
* Create a front class for the given actor specification and front prototype.
*
* @param object actorSpec
* The actor specification you're creating a front for.
* @param object proto
* The object prototype. Must have a 'typeName' property,
* should have method definitions, can have event definitions.
*/
var FrontClass = function (actorSpec, frontProto) {
frontProto.extends = Front;
let cls = Class(generateRequestMethods(actorSpec, frontProto));
if (!registeredTypes.has(actorSpec.typeName)) {
types.addActorType(actorSpec.typeName);
}
registeredTypes.get(actorSpec.typeName).frontClass = cls;
return cls;
};
exports.FrontClass = FrontClass;
exports.dumpActorSpec = function (type) {
let actorSpec = type.actorSpec;
let ret = {
category: "actor",
typeName: type.name,
methods: [],
events: {}
};
for (let method of actorSpec.methods) {
ret.methods.push({
name: method.name,
release: method.release || undefined,
oneway: method.oneway || undefined,
request: method.request.describe(),
response: method.response.describe()
});
}
if (actorSpec.events) {
for (let [name, request] of actorSpec.events) {
ret.events[name] = request.describe();
}
}
JSON.stringify(ret);
return ret;
};
exports.dumpProtocolSpec = function () {
let ret = {
types: {},
};
for (let [name, type] of registeredTypes) {
// Force lazy instantiation if needed.
type = types.getType(name);
let category = type.category || undefined;
if (category === "dict") {
ret.types[name] = {
category: "dict",
typeName: name,
specializations: type.specializations
};
} else if (category === "actor") {
ret.types[name] = exports.dumpActorSpec(type);
}
}
return ret;
};