123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518 |
- /* 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;
- };
|