toolkit/devtools/server/protocol.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 let {Cu} = require("chrome");
     8 let Services = require("Services");
     9 let promise = require("devtools/toolkit/deprecated-sync-thenables");
    10 let {Class} = require("sdk/core/heritage");
    11 let {EventTarget} = require("sdk/event/target");
    12 let events = require("sdk/event/core");
    13 let object = require("sdk/util/object");
    15 // Waiting for promise.done() to be added, see bug 851321
    16 function promiseDone(err) {
    17   console.error(err);
    18   return promise.reject(err);
    19 }
    21 /**
    22  * Types: named marshallers/demarshallers.
    23  *
    24  * Types provide a 'write' function that takes a js representation and
    25  * returns a protocol representation, and a "read" function that
    26  * takes a protocol representation and returns a js representation.
    27  *
    28  * The read and write methods are also passed a context object that
    29  * represent the actor or front requesting the translation.
    30  *
    31  * Types are referred to with a typestring.  Basic types are
    32  * registered by name using addType, and more complex types can
    33  * be generated by adding detail to the type name.
    34  */
    36 let types = Object.create(null);
    37 exports.types = types;
    39 let registeredTypes = new Map();
    40 let registeredLifetimes = new Map();
    42 /**
    43  * Return the type object associated with a given typestring.
    44  * If passed a type object, it will be returned unchanged.
    45  *
    46  * Types can be registered with addType, or can be created on
    47  * the fly with typestrings.  Examples:
    48  *
    49  *   boolean
    50  *   threadActor
    51  *   threadActor#detail
    52  *   array:threadActor
    53  *   array:array:threadActor#detail
    54  *
    55  * @param [typestring|type] type
    56  *    Either a typestring naming a type or a type object.
    57  *
    58  * @returns a type object.
    59  */
    60 types.getType = function(type) {
    61   if (!type) {
    62     return types.Primitive;
    63   }
    65   if (typeof(type) !== "string") {
    66     return type;
    67   }
    69   // If already registered, we're done here.
    70   let reg = registeredTypes.get(type);
    71   if (reg) return reg;
    73   // New type, see if it's a collection/lifetime type:
    74   let sep = type.indexOf(":");
    75   if (sep >= 0) {
    76     let collection = type.substring(0, sep);
    77     let subtype = types.getType(type.substring(sep + 1));
    79     if (collection === "array") {
    80       return types.addArrayType(subtype);
    81     } else if (collection === "nullable") {
    82       return types.addNullableType(subtype);
    83     }
    85     if (registeredLifetimes.has(collection)) {
    86       return types.addLifetimeType(collection, subtype);
    87     }
    89     throw Error("Unknown collection type: " + collection);
    90   }
    92   // Not a collection, might be actor detail
    93   let pieces = type.split("#", 2);
    94   if (pieces.length > 1) {
    95     return types.addActorDetail(type, pieces[0], pieces[1]);
    96   }
    98   // Might be a lazily-loaded type
    99   if (type === "longstring") {
   100     require("devtools/server/actors/string");
   101     return registeredTypes.get("longstring");
   102   }
   104   throw Error("Unknown type: " + type);
   105 }
   107 /**
   108  * Don't allow undefined when writing primitive types to packets.  If
   109  * you want to allow undefined, use a nullable type.
   110  */
   111 function identityWrite(v) {
   112   if (v === undefined) {
   113     throw Error("undefined passed where a value is required");
   114   }
   115   return v;
   116 }
   118 /**
   119  * Add a type to the type system.
   120  *
   121  * When registering a type, you can provide `read` and `write` methods.
   122  *
   123  * The `read` method will be passed a JS object value from the JSON
   124  * packet and must return a native representation.  The `write` method will
   125  * be passed a native representation and should provide a JSONable value.
   126  *
   127  * These methods will both be passed a context.  The context is the object
   128  * performing or servicing the request - on the server side it will be
   129  * an Actor, on the client side it will be a Front.
   130  *
   131  * @param typestring name
   132  *    Name to register
   133  * @param object typeObject
   134  *    An object whose properties will be stored in the type, including
   135  *    the `read` and `write` methods.
   136  * @param object options
   137  *    Can specify `thawed` to prevent the type from being frozen.
   138  *
   139  * @returns a type object that can be used in protocol definitions.
   140  */
   141 types.addType = function(name, typeObject={}, options={}) {
   142   if (registeredTypes.has(name)) {
   143     throw Error("Type '" + name + "' already exists.");
   144   }
   146   let type = object.merge({
   147     name: name,
   148     primitive: !(typeObject.read || typeObject.write),
   149     read: identityWrite,
   150     write: identityWrite
   151   }, typeObject);
   153   registeredTypes.set(name, type);
   155   if (!options.thawed) {
   156     Object.freeze(type);
   157   }
   159   return type;
   160 };
   162 /**
   163  * Add an array type to the type system.
   164  *
   165  * getType() will call this function if provided an "array:<type>"
   166  * typestring.
   167  *
   168  * @param type subtype
   169  *    The subtype to be held by the array.
   170  */
   171 types.addArrayType = function(subtype) {
   172   subtype = types.getType(subtype);
   174   let name = "array:" + subtype.name;
   176   // Arrays of primitive types are primitive types themselves.
   177   if (subtype.primitive) {
   178     return types.addType(name);
   179   }
   180   return types.addType(name, {
   181     category: "array",
   182     read: (v, ctx) => [subtype.read(i, ctx) for (i of v)],
   183     write: (v, ctx) => [subtype.write(i, ctx) for (i of v)]
   184   });
   185 };
   187 /**
   188  * Add a dict type to the type system.  This allows you to serialize
   189  * a JS object that contains non-primitive subtypes.
   190  *
   191  * Properties of the value that aren't included in the specializations
   192  * will be serialized as primitive values.
   193  *
   194  * @param object specializations
   195  *    A dict of property names => type
   196  */
   197 types.addDictType = function(name, specializations) {
   198   return types.addType(name, {
   199     category: "dict",
   200     specializations: specializations,
   201     read: (v, ctx) => {
   202       let ret = {};
   203       for (let prop in v) {
   204         if (prop in specializations) {
   205           ret[prop] = types.getType(specializations[prop]).read(v[prop], ctx);
   206         } else {
   207           ret[prop] = v[prop];
   208         }
   209       }
   210       return ret;
   211     },
   213     write: (v, ctx) => {
   214       let ret = {};
   215       for (let prop in v) {
   216         if (prop in specializations) {
   217           ret[prop] = types.getType(specializations[prop]).write(v[prop], ctx);
   218         } else {
   219           ret[prop] = v[prop];
   220         }
   221       }
   222       return ret;
   223     }
   224   })
   225 }
   227 /**
   228  * Register an actor type with the type system.
   229  *
   230  * Types are marshalled differently when communicating server->client
   231  * than they are when communicating client->server.  The server needs
   232  * to provide useful information to the client, so uses the actor's
   233  * `form` method to get a json representation of the actor.  When
   234  * making a request from the client we only need the actor ID string.
   235  *
   236  * This function can be called before the associated actor has been
   237  * constructed, but the read and write methods won't work until
   238  * the associated addActorImpl or addActorFront methods have been
   239  * called during actor/front construction.
   240  *
   241  * @param string name
   242  *    The typestring to register.
   243  */
   244 types.addActorType = function(name) {
   245   let type = types.addType(name, {
   246     _actor: true,
   247     category: "actor",
   248     read: (v, ctx, detail) => {
   249       // If we're reading a request on the server side, just
   250       // find the actor registered with this actorID.
   251       if (ctx instanceof Actor) {
   252         return ctx.conn.getActor(v);
   253       }
   255       // Reading a response on the client side, check for an
   256       // existing front on the connection, and create the front
   257       // if it isn't found.
   258       let actorID = typeof(v) === "string" ? v : v.actor;
   259       let front = ctx.conn.getActor(actorID);
   260       if (front) {
   261         front.form(v, detail, ctx);
   262       } else {
   263         front = new type.frontClass(ctx.conn, v, detail, ctx)
   264         front.actorID = actorID;
   265         ctx.marshallPool().manage(front);
   266       }
   267       return front;
   268     },
   269     write: (v, ctx, detail) => {
   270       // If returning a response from the server side, make sure
   271       // the actor is added to a parent object and return its form.
   272       if (v instanceof Actor) {
   273         if (!v.actorID) {
   274           ctx.marshallPool().manage(v);
   275         }
   276         return v.form(detail);
   277       }
   279       // Writing a request from the client side, just send the actor id.
   280       return v.actorID;
   281     },
   282   }, {
   283     // We usually freeze types, but actor types are updated when clients are
   284     // created, so don't freeze yet.
   285     thawed: true
   286   });
   287   return type;
   288 }
   290 types.addNullableType = function(subtype) {
   291   subtype = types.getType(subtype);
   292   return types.addType("nullable:" + subtype.name, {
   293     category: "nullable",
   294     read: (value, ctx) => {
   295       if (value == null) {
   296         return value;
   297       }
   298       return subtype.read(value, ctx);
   299     },
   300     write: (value, ctx) => {
   301       if (value == null) {
   302         return value;
   303       }
   304       return subtype.write(value, ctx);
   305     }
   306   });
   307 }
   309 /**
   310  * Register an actor detail type.  This is just like an actor type, but
   311  * will pass a detail hint to the actor's form method during serialization/
   312  * deserialization.
   313  *
   314  * This is called by getType() when passed an 'actorType#detail' string.
   315  *
   316  * @param string name
   317  *   The typestring to register this type as.
   318  * @param type actorType
   319  *   The actor type you'll be detailing.
   320  * @param string detail
   321  *   The detail to pass.
   322  */
   323 types.addActorDetail = function(name, actorType, detail) {
   324   actorType = types.getType(actorType);
   325   if (!actorType._actor) {
   326     throw Error("Details only apply to actor types, tried to add detail '" + detail + "'' to " + actorType.name + "\n");
   327   }
   328   return types.addType(name, {
   329     _actor: true,
   330     category: "detail",
   331     read: (v, ctx) => actorType.read(v, ctx, detail),
   332     write: (v, ctx) => actorType.write(v, ctx, detail)
   333   });
   334 }
   336 /**
   337  * Register an actor lifetime.  This lets the type system find a parent
   338  * actor that differs from the actor fulfilling the request.
   339  *
   340  * @param string name
   341  *    The lifetime name to use in typestrings.
   342  * @param string prop
   343  *    The property of the actor that holds the parent that should be used.
   344  */
   345 types.addLifetime = function(name, prop) {
   346   if (registeredLifetimes.has(name)) {
   347     throw Error("Lifetime '" + name + "' already registered.");
   348   }
   349   registeredLifetimes.set(name, prop);
   350 }
   352 /**
   353  * Register a lifetime type.  This creates an actor type tied to the given
   354  * lifetime.
   355  *
   356  * This is called by getType() when passed a '<lifetimeType>:<actorType>'
   357  * typestring.
   358  *
   359  * @param string lifetime
   360  *    A lifetime string previously regisered with addLifetime()
   361  * @param type subtype
   362  *    An actor type
   363  */
   364 types.addLifetimeType = function(lifetime, subtype) {
   365   subtype = types.getType(subtype);
   366   if (!subtype._actor) {
   367     throw Error("Lifetimes only apply to actor types, tried to apply lifetime '" + lifetime + "'' to " + subtype.name);
   368   }
   369   let prop = registeredLifetimes.get(lifetime);
   370   return types.addType(lifetime + ":" + subtype.name, {
   371     category: "lifetime",
   372     read: (value, ctx) => subtype.read(value, ctx[prop]),
   373     write: (value, ctx) => subtype.write(value, ctx[prop])
   374   })
   375 }
   377 // Add a few named primitive types.
   378 types.Primitive = types.addType("primitive");
   379 types.String = types.addType("string");
   380 types.Number = types.addType("number");
   381 types.Boolean = types.addType("boolean");
   382 types.JSON = types.addType("json");
   384 /**
   385  * Request/Response templates and generation
   386  *
   387  * Request packets are specified as json templates with
   388  * Arg and Option placeholders where arguments should be
   389  * placed.
   390  *
   391  * Reponse packets are also specified as json templates,
   392  * with a RetVal placeholder where the return value should be
   393  * placed.
   394  */
   396 /**
   397  * Placeholder for simple arguments.
   398  *
   399  * @param number index
   400  *    The argument index to place at this position.
   401  * @param type type
   402  *    The argument should be marshalled as this type.
   403  * @constructor
   404  */
   405 let Arg = Class({
   406   initialize: function(index, type) {
   407     this.index = index;
   408     this.type = types.getType(type);
   409   },
   411   write: function(arg, ctx) {
   412     return this.type.write(arg, ctx);
   413   },
   415   read: function(v, ctx, outArgs) {
   416     outArgs[this.index] = this.type.read(v, ctx);
   417   },
   419   describe: function() {
   420     return {
   421       _arg: this.index,
   422       type: this.type.name,
   423     }
   424   }
   425 });
   426 exports.Arg = Arg;
   428 /**
   429  * Placeholder for an options argument value that should be hoisted
   430  * into the packet.
   431  *
   432  * If provided in a method specification:
   433  *
   434  *   { optionArg: Option(1)}
   435  *
   436  * Then arguments[1].optionArg will be placed in the packet in this
   437  * value's place.
   438  *
   439  * @param number index
   440  *    The argument index of the options value.
   441  * @param type type
   442  *    The argument should be marshalled as this type.
   443  * @constructor
   444  */
   445 let Option = Class({
   446   extends: Arg,
   447   initialize: function(index, type) {
   448     Arg.prototype.initialize.call(this, index, type)
   449   },
   451   write: function(arg, ctx, name) {
   452     if (!arg) {
   453       return undefined;
   454     }
   455     let v = arg[name] || undefined;
   456     if (v === undefined) {
   457       return undefined;
   458     }
   459     return this.type.write(v, ctx);
   460   },
   461   read: function(v, ctx, outArgs, name) {
   462     if (outArgs[this.index] === undefined) {
   463       outArgs[this.index] = {};
   464     }
   465     if (v === undefined) {
   466       return;
   467     }
   468     outArgs[this.index][name] = this.type.read(v, ctx);
   469   },
   471   describe: function() {
   472     return {
   473       _option: this.index,
   474       type: this.type.name,
   475     }
   476   }
   477 });
   479 exports.Option = Option;
   481 /**
   482  * Placeholder for return values in a response template.
   483  *
   484  * @param type type
   485  *    The return value should be marshalled as this type.
   486  */
   487 let RetVal = Class({
   488   initialize: function(type) {
   489     this.type = types.getType(type);
   490   },
   492   write: function(v, ctx) {
   493     return this.type.write(v, ctx);
   494   },
   496   read: function(v, ctx) {
   497     return this.type.read(v, ctx);
   498   },
   500   describe: function() {
   501     return {
   502       _retval: this.type.name
   503     }
   504   }
   505 });
   507 exports.RetVal = RetVal;
   509 /* Template handling functions */
   511 /**
   512  * Get the value at a given path, or undefined if not found.
   513  */
   514 function getPath(obj, path) {
   515   for (let name of path) {
   516     if (!(name in obj)) {
   517       return undefined;
   518     }
   519     obj = obj[name];
   520   }
   521   return obj;
   522 }
   524 /**
   525  * Find Placeholders in the template and save them along with their
   526  * paths.
   527  */
   528 function findPlaceholders(template, constructor, path=[], placeholders=[]) {
   529   if (!template || typeof(template) != "object") {
   530     return placeholders;
   531   }
   533   if (template instanceof constructor) {
   534     placeholders.push({ placeholder: template, path: [p for (p of path)] });
   535     return placeholders;
   536   }
   538   for (let name in template) {
   539     path.push(name);
   540     findPlaceholders(template[name], constructor, path, placeholders);
   541     path.pop();
   542   }
   544   return placeholders;
   545 }
   548 function describeTemplate(template) {
   549   return JSON.parse(JSON.stringify(template, (key, value) => {
   550     if (value.describe) {
   551       return value.describe();
   552     }
   553     return value;
   554   }));
   555 }
   557 /**
   558  * Manages a request template.
   559  *
   560  * @param object template
   561  *    The request template.
   562  * @construcor
   563  */
   564 let Request = Class({
   565   initialize: function(template={}) {
   566     this.type = template.type;
   567     this.template = template;
   568     this.args = findPlaceholders(template, Arg);
   569   },
   571   /**
   572    * Write a request.
   573    *
   574    * @param array fnArgs
   575    *    The function arguments to place in the request.
   576    * @param object ctx
   577    *    The object making the request.
   578    * @returns a request packet.
   579    */
   580   write: function(fnArgs, ctx) {
   581     let str = JSON.stringify(this.template, (key, value) => {
   582       if (value instanceof Arg) {
   583         return value.write(fnArgs[value.index], ctx, key);
   584       }
   585       return value;
   586     });
   587     return JSON.parse(str);
   588   },
   590   /**
   591    * Read a request.
   592    *
   593    * @param object packet
   594    *    The request packet.
   595    * @param object ctx
   596    *    The object making the request.
   597    * @returns an arguments array
   598    */
   599   read: function(packet, ctx) {
   600     let fnArgs = [];
   601     for (let templateArg of this.args) {
   602       let arg = templateArg.placeholder;
   603       let path = templateArg.path;
   604       let name = path[path.length - 1];
   605       arg.read(getPath(packet, path), ctx, fnArgs, name);
   606     }
   607     return fnArgs;
   608   },
   610   describe: function() { return describeTemplate(this.template); }
   611 });
   613 /**
   614  * Manages a response template.
   615  *
   616  * @param object template
   617  *    The response template.
   618  * @construcor
   619  */
   620 let Response = Class({
   621   initialize: function(template={}) {
   622     this.template = template;
   623     let placeholders = findPlaceholders(template, RetVal);
   624     if (placeholders.length > 1) {
   625       throw Error("More than one RetVal specified in response");
   626     }
   627     let placeholder = placeholders.shift();
   628     if (placeholder) {
   629       this.retVal = placeholder.placeholder;
   630       this.path = placeholder.path;
   631     }
   632   },
   634   /**
   635    * Write a response for the given return value.
   636    *
   637    * @param val ret
   638    *    The return value.
   639    * @param object ctx
   640    *    The object writing the response.
   641    */
   642   write: function(ret, ctx) {
   643     return JSON.parse(JSON.stringify(this.template, function(key, value) {
   644       if (value instanceof RetVal) {
   645         return value.write(ret, ctx);
   646       }
   647       return value;
   648     }));
   649   },
   651   /**
   652    * Read a return value from the given response.
   653    *
   654    * @param object packet
   655    *    The response packet.
   656    * @param object ctx
   657    *    The object reading the response.
   658    */
   659   read: function(packet, ctx) {
   660     if (!this.retVal) {
   661       return undefined;
   662     }
   663     let v = getPath(packet, this.path);
   664     return this.retVal.read(v, ctx);
   665   },
   667   describe: function() { return describeTemplate(this.template); }
   668 });
   670 /**
   671  * Actor and Front implementations
   672  */
   674 /**
   675  * A protocol object that can manage the lifetime of other protocol
   676  * objects.
   677  */
   678 let Pool = Class({
   679   extends: EventTarget,
   681   /**
   682    * Pools are used on both sides of the connection to help coordinate
   683    * lifetimes.
   684    *
   685    * @param optional conn
   686    *   Either a DebuggerServerConnection or a DebuggerClient.  Must have
   687    *   addActorPool, removeActorPool, and poolFor.
   688    *   conn can be null if the subclass provides a conn property.
   689    * @constructor
   690    */
   691   initialize: function(conn) {
   692     if (conn) {
   693       this.conn = conn;
   694     }
   695   },
   697   /**
   698    * Return the parent pool for this client.
   699    */
   700   parent: function() { return this.conn.poolFor(this.actorID) },
   702   /**
   703    * Override this if you want actors returned by this actor
   704    * to belong to a different actor by default.
   705    */
   706   marshallPool: function() { return this; },
   708   /**
   709    * Pool is the base class for all actors, even leaf nodes.
   710    * If the child map is actually referenced, go ahead and create
   711    * the stuff needed by the pool.
   712    */
   713   __poolMap: null,
   714   get _poolMap() {
   715     if (this.__poolMap) return this.__poolMap;
   716     this.__poolMap = new Map();
   717     this.conn.addActorPool(this);
   718     return this.__poolMap;
   719   },
   721   /**
   722    * Add an actor as a child of this pool.
   723    */
   724   manage: function(actor) {
   725     if (!actor.actorID) {
   726       actor.actorID = this.conn.allocID(actor.actorPrefix || actor.typeName);
   727     }
   729     this._poolMap.set(actor.actorID, actor);
   730     return actor;
   731   },
   733   /**
   734    * Remove an actor as a child of this pool.
   735    */
   736   unmanage: function(actor) {
   737     this.__poolMap.delete(actor.actorID);
   738   },
   740   // true if the given actor ID exists in the pool.
   741   has: function(actorID) this.__poolMap && this._poolMap.has(actorID),
   743   // The actor for a given actor id stored in this pool
   744   actor: function(actorID) this.__poolMap ? this._poolMap.get(actorID) : null,
   746   // Same as actor, should update debugger connection to use 'actor'
   747   // and then remove this.
   748   get: function(actorID) this.__poolMap ? this._poolMap.get(actorID) : null,
   750   // True if this pool has no children.
   751   isEmpty: function() !this.__poolMap || this._poolMap.size == 0,
   753   /**
   754    * Destroy this item, removing it from a parent if it has one,
   755    * and destroying all children if necessary.
   756    */
   757   destroy: function() {
   758     let parent = this.parent();
   759     if (parent) {
   760       parent.unmanage(this);
   761     }
   762     if (!this.__poolMap) {
   763       return;
   764     }
   765     for (let actor of this.__poolMap.values()) {
   766       // Self-owned actors are ok, but don't need destroying twice.
   767       if (actor === this) {
   768         continue;
   769       }
   770       let destroy = actor.destroy;
   771       if (destroy) {
   772         // Disconnect destroy while we're destroying in case of (misbehaving)
   773         // circular ownership.
   774         actor.destroy = null;
   775         destroy.call(actor);
   776         actor.destroy = destroy;
   777       }
   778     };
   779     this.conn.removeActorPool(this, true);
   780     this.__poolMap.clear();
   781     this.__poolMap = null;
   782   },
   784   /**
   785    * For getting along with the debugger server pools, should be removable
   786    * eventually.
   787    */
   788   cleanup: function() {
   789     this.destroy();
   790   }
   791 });
   792 exports.Pool = Pool;
   794 /**
   795  * An actor in the actor tree.
   796  */
   797 let Actor = Class({
   798   extends: Pool,
   800   // Will contain the actor's ID
   801   actorID: null,
   803   /**
   804    * Initialize an actor.
   805    *
   806    * @param optional conn
   807    *   Either a DebuggerServerConnection or a DebuggerClient.  Must have
   808    *   addActorPool, removeActorPool, and poolFor.
   809    *   conn can be null if the subclass provides a conn property.
   810    * @constructor
   811    */
   812   initialize: function(conn) {
   813     Pool.prototype.initialize.call(this, conn);
   815     // Forward events to the connection.
   816     if (this._actorSpec && this._actorSpec.events) {
   817       for (let key of this._actorSpec.events.keys()) {
   818         let name = key;
   819         let sendEvent = this._sendEvent.bind(this, name)
   820         this.on(name, (...args) => {
   821           sendEvent.apply(null, args);
   822         });
   823       }
   824     }
   825   },
   827   _sendEvent: function(name, ...args) {
   828     if (!this._actorSpec.events.has(name)) {
   829       // It's ok to emit events that don't go over the wire.
   830       return;
   831     }
   832     let request = this._actorSpec.events.get(name);
   833     let packet = request.write(args, this);
   834     packet.from = packet.from || this.actorID;
   835     this.conn.send(packet);
   836   },
   838   destroy: function() {
   839     Pool.prototype.destroy.call(this);
   840     this.actorID = null;
   841   },
   843   /**
   844    * Override this method in subclasses to serialize the actor.
   845    * @param [optional] string hint
   846    *   Optional string to customize the form.
   847    * @returns A jsonable object.
   848    */
   849   form: function(hint) {
   850     return { actor: this.actorID }
   851   },
   853   writeError: function(err) {
   854     console.error(err);
   855     if (err.stack) {
   856       dump(err.stack);
   857     }
   858     this.conn.send({
   859       from: this.actorID,
   860       error: "unknownError",
   861       message: err.toString()
   862     });
   863   },
   865   _queueResponse: function(create) {
   866     let pending = this._pendingResponse || promise.resolve(null);
   867     let response = create(pending);
   868     this._pendingResponse = response;
   869   }
   870 });
   871 exports.Actor = Actor;
   873 /**
   874  * Tags a prtotype method as an actor method implementation.
   875  *
   876  * @param function fn
   877  *    The implementation function, will be returned.
   878  * @param spec
   879  *    The method specification, with the following (optional) properties:
   880  *      request (object): a request template.
   881  *      response (object): a response template.
   882  *      oneway (bool): 'true' if no response should be sent.
   883  *      telemetry (string): Telemetry probe ID for measuring completion time.
   884  */
   885 exports.method = function(fn, spec={}) {
   886   fn._methodSpec = Object.freeze(spec);
   887   if (spec.request) Object.freeze(spec.request);
   888   if (spec.response) Object.freeze(spec.response);
   889   return fn;
   890 }
   892 /**
   893  * Process an actor definition from its prototype and generate
   894  * request handlers.
   895  */
   896 let actorProto = function(actorProto) {
   897   if (actorProto._actorSpec) {
   898     throw new Error("actorProto called twice on the same actor prototype!");
   899   }
   901   let protoSpec = {
   902     methods: [],
   903   };
   905   // Find method specifications attached to prototype properties.
   906   for (let name of Object.getOwnPropertyNames(actorProto)) {
   907     let desc = Object.getOwnPropertyDescriptor(actorProto, name);
   908     if (!desc.value) {
   909       continue;
   910     }
   912     if (desc.value._methodSpec) {
   913       let frozenSpec = desc.value._methodSpec;
   914       let spec = {};
   915       spec.name = frozenSpec.name || name;
   916       spec.request = Request(object.merge({type: spec.name}, frozenSpec.request || undefined));
   917       spec.response = Response(frozenSpec.response || undefined);
   918       spec.telemetry = frozenSpec.telemetry;
   919       spec.release = frozenSpec.release;
   920       spec.oneway = frozenSpec.oneway;
   922       protoSpec.methods.push(spec);
   923     }
   924   }
   926   // Find event specifications
   927   if (actorProto.events) {
   928     protoSpec.events = new Map();
   929     for (let name in actorProto.events) {
   930       let eventRequest = actorProto.events[name];
   931       Object.freeze(eventRequest);
   932       protoSpec.events.set(name, Request(object.merge({type: name}, eventRequest)));
   933     }
   934   }
   936   // Generate request handlers for each method definition
   937   actorProto.requestTypes = Object.create(null);
   938   protoSpec.methods.forEach(spec => {
   939     let handler = function(packet, conn) {
   940       try {
   941         let args = spec.request.read(packet, this);
   943         let ret = this[spec.name].apply(this, args);
   945         if (spec.oneway) {
   946           // No need to send a response.
   947           return;
   948         }
   950         let sendReturn = (ret) => {
   951           let response = spec.response.write(ret, this);
   952           response.from = this.actorID;
   953           // If spec.release has been specified, destroy the object.
   954           if (spec.release) {
   955             try {
   956               this.destroy();
   957             } catch(e) {
   958               this.writeError(e);
   959               return;
   960             }
   961           }
   963           conn.send(response);
   964         };
   966         this._queueResponse(p => {
   967           return p
   968             .then(() => ret)
   969             .then(sendReturn)
   970             .then(null, this.writeError.bind(this));
   971         })
   972       } catch(e) {
   973         this._queueResponse(p => {
   974           return p.then(() => this.writeError(e));
   975         });
   976       }
   977     };
   979     actorProto.requestTypes[spec.request.type] = handler;
   980   });
   982   actorProto._actorSpec = protoSpec;
   983   return actorProto;
   984 }
   986 /**
   987  * Create an actor class for the given actor prototype.
   988  *
   989  * @param object proto
   990  *    The object prototype.  Must have a 'typeName' property,
   991  *    should have method definitions, can have event definitions.
   992  */
   993 exports.ActorClass = function(proto) {
   994   if (!proto.typeName) {
   995     throw Error("Actor prototype must have a typeName member.");
   996   }
   997   proto.extends = Actor;
   998   if (!registeredTypes.has(proto.typeName)) {
   999     types.addActorType(proto.typeName);
  1001   let cls = Class(actorProto(proto));
  1003   registeredTypes.get(proto.typeName).actorSpec = proto._actorSpec;
  1004   return cls;
  1005 };
  1007 /**
  1008  * Base class for client-side actor fronts.
  1009  */
  1010 let Front = Class({
  1011   extends: Pool,
  1013   actorID: null,
  1015   /**
  1016    * The base class for client-side actor fronts.
  1018    * @param optional conn
  1019    *   Either a DebuggerServerConnection or a DebuggerClient.  Must have
  1020    *   addActorPool, removeActorPool, and poolFor.
  1021    *   conn can be null if the subclass provides a conn property.
  1022    * @param optional form
  1023    *   The json form provided by the server.
  1024    * @constructor
  1025    */
  1026   initialize: function(conn=null, form=null, detail=null, context=null) {
  1027     Pool.prototype.initialize.call(this, conn);
  1028     this._requests = [];
  1029     if (form) {
  1030       this.actorID = form.actor;
  1031       this.form(form, detail, context);
  1033   },
  1035   destroy: function() {
  1036     // Reject all outstanding requests, they won't make sense after
  1037     // the front is destroyed.
  1038     while (this._requests && this._requests.length > 0) {
  1039       let deferred = this._requests.shift();
  1040       deferred.reject(new Error("Connection closed"));
  1042     Pool.prototype.destroy.call(this);
  1043     this.actorID = null;
  1044   },
  1046   /**
  1047    * @returns a promise that will resolve to the actorID this front
  1048    * represents.
  1049    */
  1050   actor: function() { return promise.resolve(this.actorID) },
  1052   toString: function() { return "[Front for " + this.typeName + "/" + this.actorID + "]" },
  1054   /**
  1055    * Update the actor from its representation.
  1056    * Subclasses should override this.
  1057    */
  1058   form: function(form) {},
  1060   /**
  1061    * Send a packet on the connection.
  1062    */
  1063   send: function(packet) {
  1064     if (packet.to) {
  1065       this.conn._transport.send(packet);
  1066     } else {
  1067       this.actor().then(actorID => {
  1068         packet.to = actorID;
  1069         this.conn._transport.send(packet);
  1070       });
  1072   },
  1074   /**
  1075    * Send a two-way request on the connection.
  1076    */
  1077   request: function(packet) {
  1078     let deferred = promise.defer();
  1079     this._requests.push(deferred);
  1080     this.send(packet);
  1081     return deferred.promise;
  1082   },
  1084   /**
  1085    * Handler for incoming packets from the client's actor.
  1086    */
  1087   onPacket: function(packet) {
  1088     // Pick off event packets
  1089     if (this._clientSpec.events && this._clientSpec.events.has(packet.type)) {
  1090       let event = this._clientSpec.events.get(packet.type);
  1091       let args = event.request.read(packet, this);
  1092       if (event.pre) {
  1093         event.pre.forEach((pre) => pre.apply(this, args));
  1095       events.emit.apply(null, [this, event.name].concat(args));
  1096       return;
  1099     // Remaining packets must be responses.
  1100     if (this._requests.length === 0) {
  1101       let msg = "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
  1102       let err = Error(msg);
  1103       console.error(err);
  1104       throw err;
  1107     let deferred = this._requests.shift();
  1108     if (packet.error) {
  1109       deferred.reject(packet.error);
  1110     } else {
  1111       deferred.resolve(packet);
  1114 });
  1115 exports.Front = Front;
  1117 /**
  1118  * A method tagged with preEvent will be called after recieving a packet
  1119  * for that event, and before the front emits the event.
  1120  */
  1121 exports.preEvent = function(eventName, fn) {
  1122   fn._preEvent = eventName;
  1123   return fn;
  1126 /**
  1127  * Mark a method as a custom front implementation, replacing the generated
  1128  * front method.
  1130  * @param function fn
  1131  *    The front implementation, will be returned.
  1132  * @param object options
  1133  *    Options object:
  1134  *      impl (string): If provided, the generated front method will be
  1135  *        stored as this property on the prototype.
  1136  */
  1137 exports.custom = function(fn, options={}) {
  1138   fn._customFront = options;
  1139   return fn;
  1142 function prototypeOf(obj) {
  1143   return typeof(obj) === "function" ? obj.prototype : obj;
  1146 /**
  1147  * Process a front definition from its prototype and generate
  1148  * request methods.
  1149  */
  1150 let frontProto = function(proto) {
  1151   let actorType = prototypeOf(proto.actorType);
  1152   if (proto._actorSpec) {
  1153     throw new Error("frontProto called twice on the same front prototype!");
  1155   proto._actorSpec = actorType._actorSpec;
  1156   proto.typeName = actorType.typeName;
  1158   // Generate request methods.
  1159   let methods = proto._actorSpec.methods;
  1160   methods.forEach(spec => {
  1161     let name = spec.name;
  1163     // If there's already a property by this name in the front, it must
  1164     // be a custom front method.
  1165     if (name in proto) {
  1166       let custom = proto[spec.name]._customFront;
  1167       if (custom === undefined) {
  1168         throw Error("Existing method for " + spec.name + " not marked customFront while processing " + actorType.typeName + ".");
  1170       // If the user doesn't need the impl don't generate it.
  1171       if (!custom.impl) {
  1172         return;
  1174       name = custom.impl;
  1177     proto[name] = function(...args) {
  1178       let histogram, startTime;
  1179       if (spec.telemetry) {
  1180         if (spec.oneway) {
  1181           // That just doesn't make sense.
  1182           throw Error("Telemetry specified for a oneway request");
  1184         let transportType = this.conn.localTransport
  1185           ? "LOCAL_"
  1186           : "REMOTE_";
  1187         let histogramId = "DEVTOOLS_DEBUGGER_RDP_"
  1188           + transportType + spec.telemetry + "_MS";
  1189         try {
  1190           histogram = Services.telemetry.getHistogramById(histogramId);
  1191           startTime = new Date();
  1192         } catch(ex) {
  1193           // XXX: Is this expected in xpcshell tests?
  1194           console.error(ex);
  1195           spec.telemetry = false;
  1199       let packet = spec.request.write(args, this);
  1200       if (spec.oneway) {
  1201         // Fire-and-forget oneway packets.
  1202         this.send(packet);
  1203         return undefined;
  1206       return this.request(packet).then(response => {
  1207         let ret = spec.response.read(response, this);
  1209         if (histogram) {
  1210           histogram.add(+new Date - startTime);
  1213         return ret;
  1214       }).then(null, promiseDone);
  1217     // Release methods should call the destroy function on return.
  1218     if (spec.release) {
  1219       let fn = proto[name];
  1220       proto[name] = function(...args) {
  1221         return fn.apply(this, args).then(result => {
  1222           this.destroy();
  1223           return result;
  1224         })
  1227   });
  1230   // Process event specifications
  1231   proto._clientSpec = {};
  1233   let events = proto._actorSpec.events;
  1234   if (events) {
  1235     // This actor has events, scan the prototype for preEvent handlers...
  1236     let preHandlers = new Map();
  1237     for (let name of Object.getOwnPropertyNames(proto)) {
  1238       let desc = Object.getOwnPropertyDescriptor(proto, name);
  1239       if (!desc.value) {
  1240         continue;
  1242       if (desc.value._preEvent) {
  1243         let preEvent = desc.value._preEvent;
  1244         if (!events.has(preEvent)) {
  1245           throw Error("preEvent for event that doesn't exist: " + preEvent);
  1247         let handlers = preHandlers.get(preEvent);
  1248         if (!handlers) {
  1249           handlers = [];
  1250           preHandlers.set(preEvent, handlers);
  1252         handlers.push(desc.value);
  1256     proto._clientSpec.events = new Map();
  1258     for (let [name, request] of events) {
  1259       proto._clientSpec.events.set(request.type, {
  1260         name: name,
  1261         request: request,
  1262         pre: preHandlers.get(name)
  1263       });
  1266   return proto;
  1269 /**
  1270  * Create a front class for the given actor class, with the given prototype.
  1272  * @param ActorClass actorType
  1273  *    The actor class you're creating a front for.
  1274  * @param object proto
  1275  *    The object prototype.  Must have a 'typeName' property,
  1276  *    should have method definitions, can have event definitions.
  1277  */
  1278 exports.FrontClass = function(actorType, proto) {
  1279   proto.actorType = actorType;
  1280   proto.extends = Front;
  1281   let cls = Class(frontProto(proto));
  1282   registeredTypes.get(cls.prototype.typeName).frontClass = cls;
  1283   return cls;
  1287 exports.dumpActorSpec = function(type) {
  1288   let actorSpec = type.actorSpec;
  1289   let ret = {
  1290     category: "actor",
  1291     typeName: type.name,
  1292     methods: [],
  1293     events: {}
  1294   };
  1296   for (let method of actorSpec.methods) {
  1297     ret.methods.push({
  1298       name: method.name,
  1299       release: method.release || undefined,
  1300       oneway: method.oneway || undefined,
  1301       request: method.request.describe(),
  1302       response: method.response.describe()
  1303     });
  1306   if (actorSpec.events) {
  1307     for (let [name, request] of actorSpec.events) {
  1308       ret.events[name] = request.describe();
  1313   JSON.stringify(ret);
  1315   return ret;
  1318 exports.dumpProtocolSpec = function() {
  1319   let ret = {
  1320     types: {},
  1321   };
  1323   for (let [name, type] of registeredTypes) {
  1324     // Force lazy instantiation if needed.
  1325     type = types.getType(name);
  1326     if (type.category === "dict") {
  1327       ret.types[name] = {
  1328         category: "dict",
  1329         typeName: name,
  1330         specializations: type.specializations
  1332     } else if (type.category === "actor") {
  1333       ret.types[name] = exports.dumpActorSpec(type);
  1337   return ret;

mercurial