Bug 983331 - The debugger server protocol should describe itself. r=gozala
authorDave Camp <dcamp@mozilla.com>
Thu, 17 Apr 2014 16:25:07 -0700
changeset 179606 98d6d4c38fcc3115d56fc7f4f05452d1e077b72e
parent 179605 87dd13ef285c9b7ecb608d4b7fc34cff097d212a
child 179607 ec728bfdbb79cb5ef6a847d042bda0cbad1207d0
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersgozala
bugs983331
milestone31.0a1
Bug 983331 - The debugger server protocol should describe itself. r=gozala
toolkit/devtools/server/actors/root.js
toolkit/devtools/server/main.js
toolkit/devtools/server/protocol.js
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -354,16 +354,20 @@ RootActor.prototype = {
   onEcho: function (aRequest) {
     /*
      * Request packets are frozen. Copy aRequest, so that
      * DebuggerServerConnection.onPacket can attach a 'from' property.
      */
     return JSON.parse(JSON.stringify(aRequest));
   },
 
+  onProtocolDescription: function (aRequest) {
+    return protocol.dumpProtocolSpec()
+  },
+
   /* Support for DebuggerServer.addGlobalActor. */
   _createExtraActors: CommonCreateExtraActors,
   _appendExtraActors: CommonAppendExtraActors,
 
   /* ThreadActor hooks. */
 
   /**
    * Prepare to enter a nested event loop by disabling debuggee events.
@@ -394,10 +398,11 @@ RootActor.prototype = {
       windowUtils.suppressEventHandling(false);
     }
   }
 };
 
 RootActor.prototype.requestTypes = {
   "listTabs": RootActor.prototype.onListTabs,
   "listAddons": RootActor.prototype.onListAddons,
-  "echo": RootActor.prototype.onEcho
+  "echo": RootActor.prototype.onEcho,
+  "protocolDescription": RootActor.prototype.onProtocolDescription
 };
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -218,16 +218,18 @@ var DebuggerServer = {
 
     this.xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
     this.initTransport(aAllowConnectionCallback);
     this.addActors("resource://gre/modules/devtools/server/actors/root.js");
 
     this._initialized = true;
   },
 
+  protocol: require("devtools/server/protocol"),
+
   /**
    * Initialize the debugger server's transport variables.  This can be
    * in place of init() for cases where the jsdebugger isn't needed.
    *
    * @param function aAllowConnectionCallback
    *        The embedder-provider callback, that decides whether an incoming
    *        remote protocol conection should be allowed or refused.
    */
--- a/toolkit/devtools/server/protocol.js
+++ b/toolkit/devtools/server/protocol.js
@@ -173,16 +173,17 @@ types.addArrayType = function(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) => [subtype.read(i, ctx) for (i of v)],
     write: (v, ctx) => [subtype.write(i, ctx) for (i of v)]
   });
 };
 
 /**
  * Add a dict type to the type system.  This allows you to serialize
  * a JS object that contains non-primitive subtypes.
@@ -190,16 +191,18 @@ types.addArrayType = function(subtype) {
  * 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];
         }
@@ -236,16 +239,17 @@ types.addDictType = function(name, speci
  * 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
@@ -281,16 +285,17 @@ types.addActorType = function(name) {
     thawed: true
   });
   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) {
@@ -317,16 +322,17 @@ types.addNullableType = function(subtype
  */
 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.
@@ -357,16 +363,17 @@ types.addLifetime = function(name, prop)
  */
 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");
@@ -402,16 +409,23 @@ let Arg = Class({
   },
 
   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.
  *
@@ -447,16 +461,23 @@ let Option = Class({
   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.
  *
@@ -469,16 +490,22 @@ let RetVal = Class({
   },
 
   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 */
 
 /**
@@ -513,16 +540,25 @@ function findPlaceholders(template, cons
     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
  */
 let Request = Class({
@@ -565,16 +601,18 @@ let Request = Class({
     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
@@ -619,17 +657,19 @@ let Response = Class({
    *    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
@@ -953,17 +993,20 @@ let actorProto = function(actorProto) {
 exports.ActorClass = function(proto) {
   if (!proto.typeName) {
     throw Error("Actor prototype must have a typeName member.");
   }
   proto.extends = Actor;
   if (!registeredTypes.has(proto.typeName)) {
     types.addActorType(proto.typeName);
   }
-  return Class(actorProto(proto));
+  let cls = Class(actorProto(proto));
+
+  registeredTypes.get(proto.typeName).actorSpec = proto._actorSpec;
+  return cls;
 };
 
 /**
  * Base class for client-side actor fronts.
  */
 let Front = Class({
   extends: Pool,
 
@@ -1234,8 +1277,62 @@ let frontProto = function(proto) {
  */
 exports.FrontClass = function(actorType, proto) {
   proto.actorType = actorType;
   proto.extends = Front;
   let cls = Class(frontProto(proto));
   registeredTypes.get(cls.prototype.typeName).frontClass = cls;
   return cls;
 }
+
+
+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);
+    if (type.category === "dict") {
+      ret.types[name] = {
+        category: "dict",
+        typeName: name,
+        specializations: type.specializations
+      }
+    } else if (type.category === "actor") {
+      ret.types[name] = exports.dumpActorSpec(type);
+    }
+  }
+
+  return ret;
+}