diff options
Diffstat (limited to 'devtools/shared/protocol.js')
-rw-r--r-- | devtools/shared/protocol.js | 1517 |
1 files changed, 1517 insertions, 0 deletions
diff --git a/devtools/shared/protocol.js b/devtools/shared/protocol.js new file mode 100644 index 000000000..4035d5016 --- /dev/null +++ b/devtools/shared/protocol.js @@ -0,0 +1,1517 @@ +/* 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 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"); +var {getStack, callFunctionWithAsyncStack} = require("devtools/shared/platform/stack"); + +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; +}; + +/** + * THIS METHOD IS DEPRECATED, AND PRESERVED ONLY FOR ADD-ONS. IT SHOULD NOT BE + * USED INSIDE THE TREE. + * + * Create an actor class for the given actor prototype. + * + * @param object actorProto + * The actor prototype. Must have a 'typeName' property, + * should have method definitions, can have event definitions. + */ +exports.ActorClass = function (actorProto) { + return ActorClassWithSpec(generateActorSpec(actorProto), actorProto); +}; + +/** + * THIS METHOD IS DEPRECATED, AND PRESERVED ONLY FOR ADD-ONS. IT SHOULD NOT BE + * USED INSIDE THE TREE. + * + * 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."); + } + + actorProto.extends = Actor; + let cls = Class(generateRequestHandlers(actorSpec, actorProto)); + + return cls; +}; +exports.ActorClassWithSpec = ActorClassWithSpec; + +/** + * 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: getStack(), + }); + 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(); + callFunctionWithAsyncStack(() => { + if (packet.error) { + // "Protocol error" is here to avoid TBPL heuristics. See also + // https://dxr.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 class and front prototype. + * + * @param ActorClass actorType + * The actor class you're creating a front for. + * @param object frontProto + * The front prototype. Must have a 'typeName' property, + * should have method definitions, can have event definitions. + */ +exports.FrontClass = function (actorType, frontProto) { + return FrontClassWithSpec(prototypeOf(actorType)._actorSpec, 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 FrontClassWithSpec = 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.FrontClassWithSpec = FrontClassWithSpec; + +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; +}; |