michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: let {Cu} = require("chrome"); michael@0: let Services = require("Services"); michael@0: let promise = require("devtools/toolkit/deprecated-sync-thenables"); michael@0: let {Class} = require("sdk/core/heritage"); michael@0: let {EventTarget} = require("sdk/event/target"); michael@0: let events = require("sdk/event/core"); michael@0: let object = require("sdk/util/object"); michael@0: michael@0: // Waiting for promise.done() to be added, see bug 851321 michael@0: function promiseDone(err) { michael@0: console.error(err); michael@0: return promise.reject(err); michael@0: } michael@0: michael@0: /** michael@0: * Types: named marshallers/demarshallers. michael@0: * michael@0: * Types provide a 'write' function that takes a js representation and michael@0: * returns a protocol representation, and a "read" function that michael@0: * takes a protocol representation and returns a js representation. michael@0: * michael@0: * The read and write methods are also passed a context object that michael@0: * represent the actor or front requesting the translation. michael@0: * michael@0: * Types are referred to with a typestring. Basic types are michael@0: * registered by name using addType, and more complex types can michael@0: * be generated by adding detail to the type name. michael@0: */ michael@0: michael@0: let types = Object.create(null); michael@0: exports.types = types; michael@0: michael@0: let registeredTypes = new Map(); michael@0: let registeredLifetimes = new Map(); michael@0: michael@0: /** michael@0: * Return the type object associated with a given typestring. michael@0: * If passed a type object, it will be returned unchanged. michael@0: * michael@0: * Types can be registered with addType, or can be created on michael@0: * the fly with typestrings. Examples: michael@0: * michael@0: * boolean michael@0: * threadActor michael@0: * threadActor#detail michael@0: * array:threadActor michael@0: * array:array:threadActor#detail michael@0: * michael@0: * @param [typestring|type] type michael@0: * Either a typestring naming a type or a type object. michael@0: * michael@0: * @returns a type object. michael@0: */ michael@0: types.getType = function(type) { michael@0: if (!type) { michael@0: return types.Primitive; michael@0: } michael@0: michael@0: if (typeof(type) !== "string") { michael@0: return type; michael@0: } michael@0: michael@0: // If already registered, we're done here. michael@0: let reg = registeredTypes.get(type); michael@0: if (reg) return reg; michael@0: michael@0: // New type, see if it's a collection/lifetime type: michael@0: let sep = type.indexOf(":"); michael@0: if (sep >= 0) { michael@0: let collection = type.substring(0, sep); michael@0: let subtype = types.getType(type.substring(sep + 1)); michael@0: michael@0: if (collection === "array") { michael@0: return types.addArrayType(subtype); michael@0: } else if (collection === "nullable") { michael@0: return types.addNullableType(subtype); michael@0: } michael@0: michael@0: if (registeredLifetimes.has(collection)) { michael@0: return types.addLifetimeType(collection, subtype); michael@0: } michael@0: michael@0: throw Error("Unknown collection type: " + collection); michael@0: } michael@0: michael@0: // Not a collection, might be actor detail michael@0: let pieces = type.split("#", 2); michael@0: if (pieces.length > 1) { michael@0: return types.addActorDetail(type, pieces[0], pieces[1]); michael@0: } michael@0: michael@0: // Might be a lazily-loaded type michael@0: if (type === "longstring") { michael@0: require("devtools/server/actors/string"); michael@0: return registeredTypes.get("longstring"); michael@0: } michael@0: michael@0: throw Error("Unknown type: " + type); michael@0: } michael@0: michael@0: /** michael@0: * Don't allow undefined when writing primitive types to packets. If michael@0: * you want to allow undefined, use a nullable type. michael@0: */ michael@0: function identityWrite(v) { michael@0: if (v === undefined) { michael@0: throw Error("undefined passed where a value is required"); michael@0: } michael@0: return v; michael@0: } michael@0: michael@0: /** michael@0: * Add a type to the type system. michael@0: * michael@0: * When registering a type, you can provide `read` and `write` methods. michael@0: * michael@0: * The `read` method will be passed a JS object value from the JSON michael@0: * packet and must return a native representation. The `write` method will michael@0: * be passed a native representation and should provide a JSONable value. michael@0: * michael@0: * These methods will both be passed a context. The context is the object michael@0: * performing or servicing the request - on the server side it will be michael@0: * an Actor, on the client side it will be a Front. michael@0: * michael@0: * @param typestring name michael@0: * Name to register michael@0: * @param object typeObject michael@0: * An object whose properties will be stored in the type, including michael@0: * the `read` and `write` methods. michael@0: * @param object options michael@0: * Can specify `thawed` to prevent the type from being frozen. michael@0: * michael@0: * @returns a type object that can be used in protocol definitions. michael@0: */ michael@0: types.addType = function(name, typeObject={}, options={}) { michael@0: if (registeredTypes.has(name)) { michael@0: throw Error("Type '" + name + "' already exists."); michael@0: } michael@0: michael@0: let type = object.merge({ michael@0: name: name, michael@0: primitive: !(typeObject.read || typeObject.write), michael@0: read: identityWrite, michael@0: write: identityWrite michael@0: }, typeObject); michael@0: michael@0: registeredTypes.set(name, type); michael@0: michael@0: if (!options.thawed) { michael@0: Object.freeze(type); michael@0: } michael@0: michael@0: return type; michael@0: }; michael@0: michael@0: /** michael@0: * Add an array type to the type system. michael@0: * michael@0: * getType() will call this function if provided an "array:" michael@0: * typestring. michael@0: * michael@0: * @param type subtype michael@0: * The subtype to be held by the array. michael@0: */ michael@0: types.addArrayType = function(subtype) { michael@0: subtype = types.getType(subtype); michael@0: michael@0: let name = "array:" + subtype.name; michael@0: michael@0: // Arrays of primitive types are primitive types themselves. michael@0: if (subtype.primitive) { michael@0: return types.addType(name); michael@0: } michael@0: return types.addType(name, { michael@0: category: "array", michael@0: read: (v, ctx) => [subtype.read(i, ctx) for (i of v)], michael@0: write: (v, ctx) => [subtype.write(i, ctx) for (i of v)] michael@0: }); michael@0: }; michael@0: michael@0: /** michael@0: * Add a dict type to the type system. This allows you to serialize michael@0: * a JS object that contains non-primitive subtypes. michael@0: * michael@0: * Properties of the value that aren't included in the specializations michael@0: * will be serialized as primitive values. michael@0: * michael@0: * @param object specializations michael@0: * A dict of property names => type michael@0: */ michael@0: types.addDictType = function(name, specializations) { michael@0: return types.addType(name, { michael@0: category: "dict", michael@0: specializations: specializations, michael@0: read: (v, ctx) => { michael@0: let ret = {}; michael@0: for (let prop in v) { michael@0: if (prop in specializations) { michael@0: ret[prop] = types.getType(specializations[prop]).read(v[prop], ctx); michael@0: } else { michael@0: ret[prop] = v[prop]; michael@0: } michael@0: } michael@0: return ret; michael@0: }, michael@0: michael@0: write: (v, ctx) => { michael@0: let ret = {}; michael@0: for (let prop in v) { michael@0: if (prop in specializations) { michael@0: ret[prop] = types.getType(specializations[prop]).write(v[prop], ctx); michael@0: } else { michael@0: ret[prop] = v[prop]; michael@0: } michael@0: } michael@0: return ret; michael@0: } michael@0: }) michael@0: } michael@0: michael@0: /** michael@0: * Register an actor type with the type system. michael@0: * michael@0: * Types are marshalled differently when communicating server->client michael@0: * than they are when communicating client->server. The server needs michael@0: * to provide useful information to the client, so uses the actor's michael@0: * `form` method to get a json representation of the actor. When michael@0: * making a request from the client we only need the actor ID string. michael@0: * michael@0: * This function can be called before the associated actor has been michael@0: * constructed, but the read and write methods won't work until michael@0: * the associated addActorImpl or addActorFront methods have been michael@0: * called during actor/front construction. michael@0: * michael@0: * @param string name michael@0: * The typestring to register. michael@0: */ michael@0: types.addActorType = function(name) { michael@0: let type = types.addType(name, { michael@0: _actor: true, michael@0: category: "actor", michael@0: read: (v, ctx, detail) => { michael@0: // If we're reading a request on the server side, just michael@0: // find the actor registered with this actorID. michael@0: if (ctx instanceof Actor) { michael@0: return ctx.conn.getActor(v); michael@0: } michael@0: michael@0: // Reading a response on the client side, check for an michael@0: // existing front on the connection, and create the front michael@0: // if it isn't found. michael@0: let actorID = typeof(v) === "string" ? v : v.actor; michael@0: let front = ctx.conn.getActor(actorID); michael@0: if (front) { michael@0: front.form(v, detail, ctx); michael@0: } else { michael@0: front = new type.frontClass(ctx.conn, v, detail, ctx) michael@0: front.actorID = actorID; michael@0: ctx.marshallPool().manage(front); michael@0: } michael@0: return front; michael@0: }, michael@0: write: (v, ctx, detail) => { michael@0: // If returning a response from the server side, make sure michael@0: // the actor is added to a parent object and return its form. michael@0: if (v instanceof Actor) { michael@0: if (!v.actorID) { michael@0: ctx.marshallPool().manage(v); michael@0: } michael@0: return v.form(detail); michael@0: } michael@0: michael@0: // Writing a request from the client side, just send the actor id. michael@0: return v.actorID; michael@0: }, michael@0: }, { michael@0: // We usually freeze types, but actor types are updated when clients are michael@0: // created, so don't freeze yet. michael@0: thawed: true michael@0: }); michael@0: return type; michael@0: } michael@0: michael@0: types.addNullableType = function(subtype) { michael@0: subtype = types.getType(subtype); michael@0: return types.addType("nullable:" + subtype.name, { michael@0: category: "nullable", michael@0: read: (value, ctx) => { michael@0: if (value == null) { michael@0: return value; michael@0: } michael@0: return subtype.read(value, ctx); michael@0: }, michael@0: write: (value, ctx) => { michael@0: if (value == null) { michael@0: return value; michael@0: } michael@0: return subtype.write(value, ctx); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Register an actor detail type. This is just like an actor type, but michael@0: * will pass a detail hint to the actor's form method during serialization/ michael@0: * deserialization. michael@0: * michael@0: * This is called by getType() when passed an 'actorType#detail' string. michael@0: * michael@0: * @param string name michael@0: * The typestring to register this type as. michael@0: * @param type actorType michael@0: * The actor type you'll be detailing. michael@0: * @param string detail michael@0: * The detail to pass. michael@0: */ michael@0: types.addActorDetail = function(name, actorType, detail) { michael@0: actorType = types.getType(actorType); michael@0: if (!actorType._actor) { michael@0: throw Error("Details only apply to actor types, tried to add detail '" + detail + "'' to " + actorType.name + "\n"); michael@0: } michael@0: return types.addType(name, { michael@0: _actor: true, michael@0: category: "detail", michael@0: read: (v, ctx) => actorType.read(v, ctx, detail), michael@0: write: (v, ctx) => actorType.write(v, ctx, detail) michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Register an actor lifetime. This lets the type system find a parent michael@0: * actor that differs from the actor fulfilling the request. michael@0: * michael@0: * @param string name michael@0: * The lifetime name to use in typestrings. michael@0: * @param string prop michael@0: * The property of the actor that holds the parent that should be used. michael@0: */ michael@0: types.addLifetime = function(name, prop) { michael@0: if (registeredLifetimes.has(name)) { michael@0: throw Error("Lifetime '" + name + "' already registered."); michael@0: } michael@0: registeredLifetimes.set(name, prop); michael@0: } michael@0: michael@0: /** michael@0: * Register a lifetime type. This creates an actor type tied to the given michael@0: * lifetime. michael@0: * michael@0: * This is called by getType() when passed a ':' michael@0: * typestring. michael@0: * michael@0: * @param string lifetime michael@0: * A lifetime string previously regisered with addLifetime() michael@0: * @param type subtype michael@0: * An actor type michael@0: */ michael@0: types.addLifetimeType = function(lifetime, subtype) { michael@0: subtype = types.getType(subtype); michael@0: if (!subtype._actor) { michael@0: throw Error("Lifetimes only apply to actor types, tried to apply lifetime '" + lifetime + "'' to " + subtype.name); michael@0: } michael@0: let prop = registeredLifetimes.get(lifetime); michael@0: return types.addType(lifetime + ":" + subtype.name, { michael@0: category: "lifetime", michael@0: read: (value, ctx) => subtype.read(value, ctx[prop]), michael@0: write: (value, ctx) => subtype.write(value, ctx[prop]) michael@0: }) michael@0: } michael@0: michael@0: // Add a few named primitive types. michael@0: types.Primitive = types.addType("primitive"); michael@0: types.String = types.addType("string"); michael@0: types.Number = types.addType("number"); michael@0: types.Boolean = types.addType("boolean"); michael@0: types.JSON = types.addType("json"); michael@0: michael@0: /** michael@0: * Request/Response templates and generation michael@0: * michael@0: * Request packets are specified as json templates with michael@0: * Arg and Option placeholders where arguments should be michael@0: * placed. michael@0: * michael@0: * Reponse packets are also specified as json templates, michael@0: * with a RetVal placeholder where the return value should be michael@0: * placed. michael@0: */ michael@0: michael@0: /** michael@0: * Placeholder for simple arguments. michael@0: * michael@0: * @param number index michael@0: * The argument index to place at this position. michael@0: * @param type type michael@0: * The argument should be marshalled as this type. michael@0: * @constructor michael@0: */ michael@0: let Arg = Class({ michael@0: initialize: function(index, type) { michael@0: this.index = index; michael@0: this.type = types.getType(type); michael@0: }, michael@0: michael@0: write: function(arg, ctx) { michael@0: return this.type.write(arg, ctx); michael@0: }, michael@0: michael@0: read: function(v, ctx, outArgs) { michael@0: outArgs[this.index] = this.type.read(v, ctx); michael@0: }, michael@0: michael@0: describe: function() { michael@0: return { michael@0: _arg: this.index, michael@0: type: this.type.name, michael@0: } michael@0: } michael@0: }); michael@0: exports.Arg = Arg; michael@0: michael@0: /** michael@0: * Placeholder for an options argument value that should be hoisted michael@0: * into the packet. michael@0: * michael@0: * If provided in a method specification: michael@0: * michael@0: * { optionArg: Option(1)} michael@0: * michael@0: * Then arguments[1].optionArg will be placed in the packet in this michael@0: * value's place. michael@0: * michael@0: * @param number index michael@0: * The argument index of the options value. michael@0: * @param type type michael@0: * The argument should be marshalled as this type. michael@0: * @constructor michael@0: */ michael@0: let Option = Class({ michael@0: extends: Arg, michael@0: initialize: function(index, type) { michael@0: Arg.prototype.initialize.call(this, index, type) michael@0: }, michael@0: michael@0: write: function(arg, ctx, name) { michael@0: if (!arg) { michael@0: return undefined; michael@0: } michael@0: let v = arg[name] || undefined; michael@0: if (v === undefined) { michael@0: return undefined; michael@0: } michael@0: return this.type.write(v, ctx); michael@0: }, michael@0: read: function(v, ctx, outArgs, name) { michael@0: if (outArgs[this.index] === undefined) { michael@0: outArgs[this.index] = {}; michael@0: } michael@0: if (v === undefined) { michael@0: return; michael@0: } michael@0: outArgs[this.index][name] = this.type.read(v, ctx); michael@0: }, michael@0: michael@0: describe: function() { michael@0: return { michael@0: _option: this.index, michael@0: type: this.type.name, michael@0: } michael@0: } michael@0: }); michael@0: michael@0: exports.Option = Option; michael@0: michael@0: /** michael@0: * Placeholder for return values in a response template. michael@0: * michael@0: * @param type type michael@0: * The return value should be marshalled as this type. michael@0: */ michael@0: let RetVal = Class({ michael@0: initialize: function(type) { michael@0: this.type = types.getType(type); michael@0: }, michael@0: michael@0: write: function(v, ctx) { michael@0: return this.type.write(v, ctx); michael@0: }, michael@0: michael@0: read: function(v, ctx) { michael@0: return this.type.read(v, ctx); michael@0: }, michael@0: michael@0: describe: function() { michael@0: return { michael@0: _retval: this.type.name michael@0: } michael@0: } michael@0: }); michael@0: michael@0: exports.RetVal = RetVal; michael@0: michael@0: /* Template handling functions */ michael@0: michael@0: /** michael@0: * Get the value at a given path, or undefined if not found. michael@0: */ michael@0: function getPath(obj, path) { michael@0: for (let name of path) { michael@0: if (!(name in obj)) { michael@0: return undefined; michael@0: } michael@0: obj = obj[name]; michael@0: } michael@0: return obj; michael@0: } michael@0: michael@0: /** michael@0: * Find Placeholders in the template and save them along with their michael@0: * paths. michael@0: */ michael@0: function findPlaceholders(template, constructor, path=[], placeholders=[]) { michael@0: if (!template || typeof(template) != "object") { michael@0: return placeholders; michael@0: } michael@0: michael@0: if (template instanceof constructor) { michael@0: placeholders.push({ placeholder: template, path: [p for (p of path)] }); michael@0: return placeholders; michael@0: } michael@0: michael@0: for (let name in template) { michael@0: path.push(name); michael@0: findPlaceholders(template[name], constructor, path, placeholders); michael@0: path.pop(); michael@0: } michael@0: michael@0: return placeholders; michael@0: } michael@0: michael@0: michael@0: function describeTemplate(template) { michael@0: return JSON.parse(JSON.stringify(template, (key, value) => { michael@0: if (value.describe) { michael@0: return value.describe(); michael@0: } michael@0: return value; michael@0: })); michael@0: } michael@0: michael@0: /** michael@0: * Manages a request template. michael@0: * michael@0: * @param object template michael@0: * The request template. michael@0: * @construcor michael@0: */ michael@0: let Request = Class({ michael@0: initialize: function(template={}) { michael@0: this.type = template.type; michael@0: this.template = template; michael@0: this.args = findPlaceholders(template, Arg); michael@0: }, michael@0: michael@0: /** michael@0: * Write a request. michael@0: * michael@0: * @param array fnArgs michael@0: * The function arguments to place in the request. michael@0: * @param object ctx michael@0: * The object making the request. michael@0: * @returns a request packet. michael@0: */ michael@0: write: function(fnArgs, ctx) { michael@0: let str = JSON.stringify(this.template, (key, value) => { michael@0: if (value instanceof Arg) { michael@0: return value.write(fnArgs[value.index], ctx, key); michael@0: } michael@0: return value; michael@0: }); michael@0: return JSON.parse(str); michael@0: }, michael@0: michael@0: /** michael@0: * Read a request. michael@0: * michael@0: * @param object packet michael@0: * The request packet. michael@0: * @param object ctx michael@0: * The object making the request. michael@0: * @returns an arguments array michael@0: */ michael@0: read: function(packet, ctx) { michael@0: let fnArgs = []; michael@0: for (let templateArg of this.args) { michael@0: let arg = templateArg.placeholder; michael@0: let path = templateArg.path; michael@0: let name = path[path.length - 1]; michael@0: arg.read(getPath(packet, path), ctx, fnArgs, name); michael@0: } michael@0: return fnArgs; michael@0: }, michael@0: michael@0: describe: function() { return describeTemplate(this.template); } michael@0: }); michael@0: michael@0: /** michael@0: * Manages a response template. michael@0: * michael@0: * @param object template michael@0: * The response template. michael@0: * @construcor michael@0: */ michael@0: let Response = Class({ michael@0: initialize: function(template={}) { michael@0: this.template = template; michael@0: let placeholders = findPlaceholders(template, RetVal); michael@0: if (placeholders.length > 1) { michael@0: throw Error("More than one RetVal specified in response"); michael@0: } michael@0: let placeholder = placeholders.shift(); michael@0: if (placeholder) { michael@0: this.retVal = placeholder.placeholder; michael@0: this.path = placeholder.path; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Write a response for the given return value. michael@0: * michael@0: * @param val ret michael@0: * The return value. michael@0: * @param object ctx michael@0: * The object writing the response. michael@0: */ michael@0: write: function(ret, ctx) { michael@0: return JSON.parse(JSON.stringify(this.template, function(key, value) { michael@0: if (value instanceof RetVal) { michael@0: return value.write(ret, ctx); michael@0: } michael@0: return value; michael@0: })); michael@0: }, michael@0: michael@0: /** michael@0: * Read a return value from the given response. michael@0: * michael@0: * @param object packet michael@0: * The response packet. michael@0: * @param object ctx michael@0: * The object reading the response. michael@0: */ michael@0: read: function(packet, ctx) { michael@0: if (!this.retVal) { michael@0: return undefined; michael@0: } michael@0: let v = getPath(packet, this.path); michael@0: return this.retVal.read(v, ctx); michael@0: }, michael@0: michael@0: describe: function() { return describeTemplate(this.template); } michael@0: }); michael@0: michael@0: /** michael@0: * Actor and Front implementations michael@0: */ michael@0: michael@0: /** michael@0: * A protocol object that can manage the lifetime of other protocol michael@0: * objects. michael@0: */ michael@0: let Pool = Class({ michael@0: extends: EventTarget, michael@0: michael@0: /** michael@0: * Pools are used on both sides of the connection to help coordinate michael@0: * lifetimes. michael@0: * michael@0: * @param optional conn michael@0: * Either a DebuggerServerConnection or a DebuggerClient. Must have michael@0: * addActorPool, removeActorPool, and poolFor. michael@0: * conn can be null if the subclass provides a conn property. michael@0: * @constructor michael@0: */ michael@0: initialize: function(conn) { michael@0: if (conn) { michael@0: this.conn = conn; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Return the parent pool for this client. michael@0: */ michael@0: parent: function() { return this.conn.poolFor(this.actorID) }, michael@0: michael@0: /** michael@0: * Override this if you want actors returned by this actor michael@0: * to belong to a different actor by default. michael@0: */ michael@0: marshallPool: function() { return this; }, michael@0: michael@0: /** michael@0: * Pool is the base class for all actors, even leaf nodes. michael@0: * If the child map is actually referenced, go ahead and create michael@0: * the stuff needed by the pool. michael@0: */ michael@0: __poolMap: null, michael@0: get _poolMap() { michael@0: if (this.__poolMap) return this.__poolMap; michael@0: this.__poolMap = new Map(); michael@0: this.conn.addActorPool(this); michael@0: return this.__poolMap; michael@0: }, michael@0: michael@0: /** michael@0: * Add an actor as a child of this pool. michael@0: */ michael@0: manage: function(actor) { michael@0: if (!actor.actorID) { michael@0: actor.actorID = this.conn.allocID(actor.actorPrefix || actor.typeName); michael@0: } michael@0: michael@0: this._poolMap.set(actor.actorID, actor); michael@0: return actor; michael@0: }, michael@0: michael@0: /** michael@0: * Remove an actor as a child of this pool. michael@0: */ michael@0: unmanage: function(actor) { michael@0: this.__poolMap.delete(actor.actorID); michael@0: }, michael@0: michael@0: // true if the given actor ID exists in the pool. michael@0: has: function(actorID) this.__poolMap && this._poolMap.has(actorID), michael@0: michael@0: // The actor for a given actor id stored in this pool michael@0: actor: function(actorID) this.__poolMap ? this._poolMap.get(actorID) : null, michael@0: michael@0: // Same as actor, should update debugger connection to use 'actor' michael@0: // and then remove this. michael@0: get: function(actorID) this.__poolMap ? this._poolMap.get(actorID) : null, michael@0: michael@0: // True if this pool has no children. michael@0: isEmpty: function() !this.__poolMap || this._poolMap.size == 0, michael@0: michael@0: /** michael@0: * Destroy this item, removing it from a parent if it has one, michael@0: * and destroying all children if necessary. michael@0: */ michael@0: destroy: function() { michael@0: let parent = this.parent(); michael@0: if (parent) { michael@0: parent.unmanage(this); michael@0: } michael@0: if (!this.__poolMap) { michael@0: return; michael@0: } michael@0: for (let actor of this.__poolMap.values()) { michael@0: // Self-owned actors are ok, but don't need destroying twice. michael@0: if (actor === this) { michael@0: continue; michael@0: } michael@0: let destroy = actor.destroy; michael@0: if (destroy) { michael@0: // Disconnect destroy while we're destroying in case of (misbehaving) michael@0: // circular ownership. michael@0: actor.destroy = null; michael@0: destroy.call(actor); michael@0: actor.destroy = destroy; michael@0: } michael@0: }; michael@0: this.conn.removeActorPool(this, true); michael@0: this.__poolMap.clear(); michael@0: this.__poolMap = null; michael@0: }, michael@0: michael@0: /** michael@0: * For getting along with the debugger server pools, should be removable michael@0: * eventually. michael@0: */ michael@0: cleanup: function() { michael@0: this.destroy(); michael@0: } michael@0: }); michael@0: exports.Pool = Pool; michael@0: michael@0: /** michael@0: * An actor in the actor tree. michael@0: */ michael@0: let Actor = Class({ michael@0: extends: Pool, michael@0: michael@0: // Will contain the actor's ID michael@0: actorID: null, michael@0: michael@0: /** michael@0: * Initialize an actor. michael@0: * michael@0: * @param optional conn michael@0: * Either a DebuggerServerConnection or a DebuggerClient. Must have michael@0: * addActorPool, removeActorPool, and poolFor. michael@0: * conn can be null if the subclass provides a conn property. michael@0: * @constructor michael@0: */ michael@0: initialize: function(conn) { michael@0: Pool.prototype.initialize.call(this, conn); michael@0: michael@0: // Forward events to the connection. michael@0: if (this._actorSpec && this._actorSpec.events) { michael@0: for (let key of this._actorSpec.events.keys()) { michael@0: let name = key; michael@0: let sendEvent = this._sendEvent.bind(this, name) michael@0: this.on(name, (...args) => { michael@0: sendEvent.apply(null, args); michael@0: }); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _sendEvent: function(name, ...args) { michael@0: if (!this._actorSpec.events.has(name)) { michael@0: // It's ok to emit events that don't go over the wire. michael@0: return; michael@0: } michael@0: let request = this._actorSpec.events.get(name); michael@0: let packet = request.write(args, this); michael@0: packet.from = packet.from || this.actorID; michael@0: this.conn.send(packet); michael@0: }, michael@0: michael@0: destroy: function() { michael@0: Pool.prototype.destroy.call(this); michael@0: this.actorID = null; michael@0: }, michael@0: michael@0: /** michael@0: * Override this method in subclasses to serialize the actor. michael@0: * @param [optional] string hint michael@0: * Optional string to customize the form. michael@0: * @returns A jsonable object. michael@0: */ michael@0: form: function(hint) { michael@0: return { actor: this.actorID } michael@0: }, michael@0: michael@0: writeError: function(err) { michael@0: console.error(err); michael@0: if (err.stack) { michael@0: dump(err.stack); michael@0: } michael@0: this.conn.send({ michael@0: from: this.actorID, michael@0: error: "unknownError", michael@0: message: err.toString() michael@0: }); michael@0: }, michael@0: michael@0: _queueResponse: function(create) { michael@0: let pending = this._pendingResponse || promise.resolve(null); michael@0: let response = create(pending); michael@0: this._pendingResponse = response; michael@0: } michael@0: }); michael@0: exports.Actor = Actor; michael@0: michael@0: /** michael@0: * Tags a prtotype method as an actor method implementation. michael@0: * michael@0: * @param function fn michael@0: * The implementation function, will be returned. michael@0: * @param spec michael@0: * The method specification, with the following (optional) properties: michael@0: * request (object): a request template. michael@0: * response (object): a response template. michael@0: * oneway (bool): 'true' if no response should be sent. michael@0: * telemetry (string): Telemetry probe ID for measuring completion time. michael@0: */ michael@0: exports.method = function(fn, spec={}) { michael@0: fn._methodSpec = Object.freeze(spec); michael@0: if (spec.request) Object.freeze(spec.request); michael@0: if (spec.response) Object.freeze(spec.response); michael@0: return fn; michael@0: } michael@0: michael@0: /** michael@0: * Process an actor definition from its prototype and generate michael@0: * request handlers. michael@0: */ michael@0: let actorProto = function(actorProto) { michael@0: if (actorProto._actorSpec) { michael@0: throw new Error("actorProto called twice on the same actor prototype!"); michael@0: } michael@0: michael@0: let protoSpec = { michael@0: methods: [], michael@0: }; michael@0: michael@0: // Find method specifications attached to prototype properties. michael@0: for (let name of Object.getOwnPropertyNames(actorProto)) { michael@0: let desc = Object.getOwnPropertyDescriptor(actorProto, name); michael@0: if (!desc.value) { michael@0: continue; michael@0: } michael@0: michael@0: if (desc.value._methodSpec) { michael@0: let frozenSpec = desc.value._methodSpec; michael@0: let spec = {}; michael@0: spec.name = frozenSpec.name || name; michael@0: spec.request = Request(object.merge({type: spec.name}, frozenSpec.request || undefined)); michael@0: spec.response = Response(frozenSpec.response || undefined); michael@0: spec.telemetry = frozenSpec.telemetry; michael@0: spec.release = frozenSpec.release; michael@0: spec.oneway = frozenSpec.oneway; michael@0: michael@0: protoSpec.methods.push(spec); michael@0: } michael@0: } michael@0: michael@0: // Find event specifications michael@0: if (actorProto.events) { michael@0: protoSpec.events = new Map(); michael@0: for (let name in actorProto.events) { michael@0: let eventRequest = actorProto.events[name]; michael@0: Object.freeze(eventRequest); michael@0: protoSpec.events.set(name, Request(object.merge({type: name}, eventRequest))); michael@0: } michael@0: } michael@0: michael@0: // Generate request handlers for each method definition michael@0: actorProto.requestTypes = Object.create(null); michael@0: protoSpec.methods.forEach(spec => { michael@0: let handler = function(packet, conn) { michael@0: try { michael@0: let args = spec.request.read(packet, this); michael@0: michael@0: let ret = this[spec.name].apply(this, args); michael@0: michael@0: if (spec.oneway) { michael@0: // No need to send a response. michael@0: return; michael@0: } michael@0: michael@0: let sendReturn = (ret) => { michael@0: let response = spec.response.write(ret, this); michael@0: response.from = this.actorID; michael@0: // If spec.release has been specified, destroy the object. michael@0: if (spec.release) { michael@0: try { michael@0: this.destroy(); michael@0: } catch(e) { michael@0: this.writeError(e); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: conn.send(response); michael@0: }; michael@0: michael@0: this._queueResponse(p => { michael@0: return p michael@0: .then(() => ret) michael@0: .then(sendReturn) michael@0: .then(null, this.writeError.bind(this)); michael@0: }) michael@0: } catch(e) { michael@0: this._queueResponse(p => { michael@0: return p.then(() => this.writeError(e)); michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: actorProto.requestTypes[spec.request.type] = handler; michael@0: }); michael@0: michael@0: actorProto._actorSpec = protoSpec; michael@0: return actorProto; michael@0: } michael@0: michael@0: /** michael@0: * Create an actor class for the given actor prototype. michael@0: * michael@0: * @param object proto michael@0: * The object prototype. Must have a 'typeName' property, michael@0: * should have method definitions, can have event definitions. michael@0: */ michael@0: exports.ActorClass = function(proto) { michael@0: if (!proto.typeName) { michael@0: throw Error("Actor prototype must have a typeName member."); michael@0: } michael@0: proto.extends = Actor; michael@0: if (!registeredTypes.has(proto.typeName)) { michael@0: types.addActorType(proto.typeName); michael@0: } michael@0: let cls = Class(actorProto(proto)); michael@0: michael@0: registeredTypes.get(proto.typeName).actorSpec = proto._actorSpec; michael@0: return cls; michael@0: }; michael@0: michael@0: /** michael@0: * Base class for client-side actor fronts. michael@0: */ michael@0: let Front = Class({ michael@0: extends: Pool, michael@0: michael@0: actorID: null, michael@0: michael@0: /** michael@0: * The base class for client-side actor fronts. michael@0: * michael@0: * @param optional conn michael@0: * Either a DebuggerServerConnection or a DebuggerClient. Must have michael@0: * addActorPool, removeActorPool, and poolFor. michael@0: * conn can be null if the subclass provides a conn property. michael@0: * @param optional form michael@0: * The json form provided by the server. michael@0: * @constructor michael@0: */ michael@0: initialize: function(conn=null, form=null, detail=null, context=null) { michael@0: Pool.prototype.initialize.call(this, conn); michael@0: this._requests = []; michael@0: if (form) { michael@0: this.actorID = form.actor; michael@0: this.form(form, detail, context); michael@0: } michael@0: }, michael@0: michael@0: destroy: function() { michael@0: // Reject all outstanding requests, they won't make sense after michael@0: // the front is destroyed. michael@0: while (this._requests && this._requests.length > 0) { michael@0: let deferred = this._requests.shift(); michael@0: deferred.reject(new Error("Connection closed")); michael@0: } michael@0: Pool.prototype.destroy.call(this); michael@0: this.actorID = null; michael@0: }, michael@0: michael@0: /** michael@0: * @returns a promise that will resolve to the actorID this front michael@0: * represents. michael@0: */ michael@0: actor: function() { return promise.resolve(this.actorID) }, michael@0: michael@0: toString: function() { return "[Front for " + this.typeName + "/" + this.actorID + "]" }, michael@0: michael@0: /** michael@0: * Update the actor from its representation. michael@0: * Subclasses should override this. michael@0: */ michael@0: form: function(form) {}, michael@0: michael@0: /** michael@0: * Send a packet on the connection. michael@0: */ michael@0: send: function(packet) { michael@0: if (packet.to) { michael@0: this.conn._transport.send(packet); michael@0: } else { michael@0: this.actor().then(actorID => { michael@0: packet.to = actorID; michael@0: this.conn._transport.send(packet); michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Send a two-way request on the connection. michael@0: */ michael@0: request: function(packet) { michael@0: let deferred = promise.defer(); michael@0: this._requests.push(deferred); michael@0: this.send(packet); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Handler for incoming packets from the client's actor. michael@0: */ michael@0: onPacket: function(packet) { michael@0: // Pick off event packets michael@0: if (this._clientSpec.events && this._clientSpec.events.has(packet.type)) { michael@0: let event = this._clientSpec.events.get(packet.type); michael@0: let args = event.request.read(packet, this); michael@0: if (event.pre) { michael@0: event.pre.forEach((pre) => pre.apply(this, args)); michael@0: } michael@0: events.emit.apply(null, [this, event.name].concat(args)); michael@0: return; michael@0: } michael@0: michael@0: // Remaining packets must be responses. michael@0: if (this._requests.length === 0) { michael@0: let msg = "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet); michael@0: let err = Error(msg); michael@0: console.error(err); michael@0: throw err; michael@0: } michael@0: michael@0: let deferred = this._requests.shift(); michael@0: if (packet.error) { michael@0: deferred.reject(packet.error); michael@0: } else { michael@0: deferred.resolve(packet); michael@0: } michael@0: } michael@0: }); michael@0: exports.Front = Front; michael@0: michael@0: /** michael@0: * A method tagged with preEvent will be called after recieving a packet michael@0: * for that event, and before the front emits the event. michael@0: */ michael@0: exports.preEvent = function(eventName, fn) { michael@0: fn._preEvent = eventName; michael@0: return fn; michael@0: } michael@0: michael@0: /** michael@0: * Mark a method as a custom front implementation, replacing the generated michael@0: * front method. michael@0: * michael@0: * @param function fn michael@0: * The front implementation, will be returned. michael@0: * @param object options michael@0: * Options object: michael@0: * impl (string): If provided, the generated front method will be michael@0: * stored as this property on the prototype. michael@0: */ michael@0: exports.custom = function(fn, options={}) { michael@0: fn._customFront = options; michael@0: return fn; michael@0: } michael@0: michael@0: function prototypeOf(obj) { michael@0: return typeof(obj) === "function" ? obj.prototype : obj; michael@0: } michael@0: michael@0: /** michael@0: * Process a front definition from its prototype and generate michael@0: * request methods. michael@0: */ michael@0: let frontProto = function(proto) { michael@0: let actorType = prototypeOf(proto.actorType); michael@0: if (proto._actorSpec) { michael@0: throw new Error("frontProto called twice on the same front prototype!"); michael@0: } michael@0: proto._actorSpec = actorType._actorSpec; michael@0: proto.typeName = actorType.typeName; michael@0: michael@0: // Generate request methods. michael@0: let methods = proto._actorSpec.methods; michael@0: methods.forEach(spec => { michael@0: let name = spec.name; michael@0: michael@0: // If there's already a property by this name in the front, it must michael@0: // be a custom front method. michael@0: if (name in proto) { michael@0: let custom = proto[spec.name]._customFront; michael@0: if (custom === undefined) { michael@0: throw Error("Existing method for " + spec.name + " not marked customFront while processing " + actorType.typeName + "."); michael@0: } michael@0: // If the user doesn't need the impl don't generate it. michael@0: if (!custom.impl) { michael@0: return; michael@0: } michael@0: name = custom.impl; michael@0: } michael@0: michael@0: proto[name] = function(...args) { michael@0: let histogram, startTime; michael@0: if (spec.telemetry) { michael@0: if (spec.oneway) { michael@0: // That just doesn't make sense. michael@0: throw Error("Telemetry specified for a oneway request"); michael@0: } michael@0: let transportType = this.conn.localTransport michael@0: ? "LOCAL_" michael@0: : "REMOTE_"; michael@0: let histogramId = "DEVTOOLS_DEBUGGER_RDP_" michael@0: + transportType + spec.telemetry + "_MS"; michael@0: try { michael@0: histogram = Services.telemetry.getHistogramById(histogramId); michael@0: startTime = new Date(); michael@0: } catch(ex) { michael@0: // XXX: Is this expected in xpcshell tests? michael@0: console.error(ex); michael@0: spec.telemetry = false; michael@0: } michael@0: } michael@0: michael@0: let packet = spec.request.write(args, this); michael@0: if (spec.oneway) { michael@0: // Fire-and-forget oneway packets. michael@0: this.send(packet); michael@0: return undefined; michael@0: } michael@0: michael@0: return this.request(packet).then(response => { michael@0: let ret = spec.response.read(response, this); michael@0: michael@0: if (histogram) { michael@0: histogram.add(+new Date - startTime); michael@0: } michael@0: michael@0: return ret; michael@0: }).then(null, promiseDone); michael@0: } michael@0: michael@0: // Release methods should call the destroy function on return. michael@0: if (spec.release) { michael@0: let fn = proto[name]; michael@0: proto[name] = function(...args) { michael@0: return fn.apply(this, args).then(result => { michael@0: this.destroy(); michael@0: return result; michael@0: }) michael@0: } michael@0: } michael@0: }); michael@0: michael@0: michael@0: // Process event specifications michael@0: proto._clientSpec = {}; michael@0: michael@0: let events = proto._actorSpec.events; michael@0: if (events) { michael@0: // This actor has events, scan the prototype for preEvent handlers... michael@0: let preHandlers = new Map(); michael@0: for (let name of Object.getOwnPropertyNames(proto)) { michael@0: let desc = Object.getOwnPropertyDescriptor(proto, name); michael@0: if (!desc.value) { michael@0: continue; michael@0: } michael@0: if (desc.value._preEvent) { michael@0: let preEvent = desc.value._preEvent; michael@0: if (!events.has(preEvent)) { michael@0: throw Error("preEvent for event that doesn't exist: " + preEvent); michael@0: } michael@0: let handlers = preHandlers.get(preEvent); michael@0: if (!handlers) { michael@0: handlers = []; michael@0: preHandlers.set(preEvent, handlers); michael@0: } michael@0: handlers.push(desc.value); michael@0: } michael@0: } michael@0: michael@0: proto._clientSpec.events = new Map(); michael@0: michael@0: for (let [name, request] of events) { michael@0: proto._clientSpec.events.set(request.type, { michael@0: name: name, michael@0: request: request, michael@0: pre: preHandlers.get(name) michael@0: }); michael@0: } michael@0: } michael@0: return proto; michael@0: } michael@0: michael@0: /** michael@0: * Create a front class for the given actor class, with the given prototype. michael@0: * michael@0: * @param ActorClass actorType michael@0: * The actor class you're creating a front for. michael@0: * @param object proto michael@0: * The object prototype. Must have a 'typeName' property, michael@0: * should have method definitions, can have event definitions. michael@0: */ michael@0: exports.FrontClass = function(actorType, proto) { michael@0: proto.actorType = actorType; michael@0: proto.extends = Front; michael@0: let cls = Class(frontProto(proto)); michael@0: registeredTypes.get(cls.prototype.typeName).frontClass = cls; michael@0: return cls; michael@0: } michael@0: michael@0: michael@0: exports.dumpActorSpec = function(type) { michael@0: let actorSpec = type.actorSpec; michael@0: let ret = { michael@0: category: "actor", michael@0: typeName: type.name, michael@0: methods: [], michael@0: events: {} michael@0: }; michael@0: michael@0: for (let method of actorSpec.methods) { michael@0: ret.methods.push({ michael@0: name: method.name, michael@0: release: method.release || undefined, michael@0: oneway: method.oneway || undefined, michael@0: request: method.request.describe(), michael@0: response: method.response.describe() michael@0: }); michael@0: } michael@0: michael@0: if (actorSpec.events) { michael@0: for (let [name, request] of actorSpec.events) { michael@0: ret.events[name] = request.describe(); michael@0: } michael@0: } michael@0: michael@0: michael@0: JSON.stringify(ret); michael@0: michael@0: return ret; michael@0: } michael@0: michael@0: exports.dumpProtocolSpec = function() { michael@0: let ret = { michael@0: types: {}, michael@0: }; michael@0: michael@0: for (let [name, type] of registeredTypes) { michael@0: // Force lazy instantiation if needed. michael@0: type = types.getType(name); michael@0: if (type.category === "dict") { michael@0: ret.types[name] = { michael@0: category: "dict", michael@0: typeName: name, michael@0: specializations: type.specializations michael@0: } michael@0: } else if (type.category === "actor") { michael@0: ret.types[name] = exports.dumpActorSpec(type); michael@0: } michael@0: } michael@0: michael@0: return ret; michael@0: }