summaryrefslogtreecommitdiffstats
path: root/devtools/shared/protocol.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/protocol.js')
-rw-r--r--devtools/shared/protocol.js1517
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;
+};