Bug 1072080 - Add the ability to define a marshaller for form data. r=jryans,jsantell
authorDave Camp <dcamp@mozilla.com>
Sat, 11 Oct 2014 13:08:07 -0700
changeset 209982 4cf592b066efc4a56977a181d6aa4d81f78d3155
parent 209931 2878bf6cbc1f03db66ca60d665cd4a8667ffdfd9
child 209983 44cb72be622dc5581a944eab309738232ab104c3
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersjryans, jsantell
bugs1072080
milestone35.0a1
Bug 1072080 - Add the ability to define a marshaller for form data. r=jryans,jsantell
toolkit/devtools/server/actors/string.js
toolkit/devtools/server/actors/webaudio.js
toolkit/devtools/server/protocol.js
toolkit/devtools/server/tests/unit/test_protocol_children.js
toolkit/devtools/server/tests/unit/test_protocol_formtype.js
toolkit/devtools/server/tests/unit/xpcshell.ini
--- a/toolkit/devtools/server/actors/string.js
+++ b/toolkit/devtools/server/actors/string.js
@@ -74,32 +74,29 @@ exports.ShortLongString = Class({
 
   release: function() {
     this.str = null;
     return promise.resolve(undefined);
   }
 })
 
 exports.LongStringFront = protocol.FrontClass(exports.LongStringActor, {
-  initialize: function(client, form) {
-    // Don't give the form by default, because we're being tricky and it might just
-    // be a string.
-    protocol.Front.prototype.initialize.call(this, client, null);
-    this.form(form);
+  initialize: function(client) {
+    protocol.Front.prototype.initialize.call(this, client);
   },
 
   destroy: function() {
     this.initial = null;
     this.length = null;
     this.strPromise = null;
     protocol.Front.prototype.destroy.call(this);
   },
 
   form: function(form) {
-    this.actorID = form.actorID;
+    this.actorID = form.actor;
     this.initial = form.initial;
     this.length = form.length;
   },
 
   string: function() {
     if (!this.strPromise) {
       let promiseRest = (thusFar) => {
         if (thusFar.length === this.length)
--- a/toolkit/devtools/server/actors/webaudio.js
+++ b/toolkit/devtools/server/actors/webaudio.js
@@ -293,17 +293,21 @@ let AudioNodeActor = exports.AudioNodeAc
 });
 
 /**
  * The corresponding Front object for the AudioNodeActor.
  */
 let AudioNodeFront = protocol.FrontClass(AudioNodeActor, {
   initialize: function (client, form) {
     protocol.Front.prototype.initialize.call(this, client, form);
-    this.manage(this);
+    // if we were manually passed a form, this was created manually and
+    // needs to own itself for now.
+    if (form) {
+      this.manage(this);
+    }
   }
 });
 
 /**
  * The Web Audio Actor handles simple interaction with an AudioContext
  * high-level methods. After instantiating this actor, you'll need to set it
  * up by calling setup().
  */
@@ -430,16 +434,18 @@ let WebAudioActor = exports.WebAudioActo
    * to hibernation. This method is called automatically just before the
    * actor is destroyed.
    */
   finalize: method(function() {
     if (!this._initialized) {
       return;
     }
     this._initialized = false;
+    systemOff("webaudio-node-demise", this._onDestroyNode);
+
     off(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
     off(this.tabActor, "window-ready", this._onGlobalCreated);
     this.tabActor = null;
     this._nativeToActorID = null;
     this._callWatcher.eraseRecording();
     this._callWatcher.finalize();
     this._callWatcher = null;
   }, {
--- a/toolkit/devtools/server/protocol.js
+++ b/toolkit/devtools/server/protocol.js
@@ -140,16 +140,17 @@ function identityWrite(v) {
  * @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);
 
@@ -253,38 +254,56 @@ types.addActorType = function(name) {
         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.form(v, detail, ctx);
-      } else {
-        front = new type.frontClass(ctx.conn, v, detail, ctx)
+      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 v.form(detail);
+        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];
+    }
   }, {
     // We usually freeze types, but actor types are updated when clients are
     // created, so don't freeze yet.
     thawed: true
   });
   return type;
 }
 
@@ -819,16 +838,18 @@ let Actor = Class({
         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 {
@@ -903,23 +924,34 @@ let actorProto = function(actorProto) {
   if (actorProto._actorSpec) {
     throw new Error("actorProto called twice on the same actor prototype!");
   }
 
   let protoSpec = {
     methods: [],
   };
 
-  // Find method specifications attached to prototype properties.
+  // Find method and form specifications attached to prototype properties.
   for (let name of Object.getOwnPropertyNames(actorProto)) {
     let desc = Object.getOwnPropertyDescriptor(actorProto, name);
     if (!desc.value) {
       continue;
     }
 
+    if (name.startsWith("formType")) {
+      if (typeof(desc.value) === "string") {
+        protoSpec[name] = types.getType(desc.value);
+      } else if (desc.value.name && registeredTypes.has(desc.value.name)) {
+        protoSpec[name] = desc.value;
+      } else {
+        // Shorthand for a newly-registered DictType.
+        protoSpec[name] = types.addDictType(actorProto.typeName + "__" + name, desc.value);
+      }
+    }
+
     if (desc.value._methodSpec) {
       let frozenSpec = desc.value._methodSpec;
       let spec = {};
       spec.name = frozenSpec.name || name;
       spec.request = Request(object.merge({type: spec.name}, frozenSpec.request || undefined));
       spec.response = Response(frozenSpec.response || undefined);
       spec.telemetry = frozenSpec.telemetry;
       spec.release = frozenSpec.release;
@@ -1039,18 +1071,24 @@ let Front = Class({
    *   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) {
@@ -1115,16 +1153,17 @@ let Front = Class({
     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) {
         event.pre.forEach((pre) => pre.apply(this, args));
       }
       events.emit.apply(null, [this, event.name].concat(args));
       return;
     }
--- a/toolkit/devtools/server/tests/unit/test_protocol_children.js
+++ b/toolkit/devtools/server/tests/unit/test_protocol_children.js
@@ -229,17 +229,19 @@ let RootFront = protocol.FrontClass(Root
     this.actorID = "root";
     protocol.Front.prototype.initialize.call(this, client);
     // Root actor owns itself.
     this.manage(this);
   },
 
   getTemporaryChild: protocol.custom(function(id) {
     if (!this._temporaryHolder) {
-      this._temporaryHolder = this.manage(new protocol.Front(this.conn, {actor: this.actorID + "_temp"}));
+      this._temporaryHolder = protocol.Front(this.conn);
+      this._temporaryHolder.actorID = this.actorID + "_temp";
+      this._temporaryHolder = this.manage(this._temporaryHolder);
     }
     return this._getTemporaryChild(id);
   },{
    impl: "_getTemporaryChild"
   }),
 
   clearTemporaryChildren: protocol.custom(function() {
     if (!this._temporaryHolder) {
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_protocol_formtype.js
@@ -0,0 +1,160 @@
+let protocol = devtools.require("devtools/server/protocol");
+let {method, Arg, Option, RetVal} = protocol;
+
+protocol.types.addActorType("child");
+protocol.types.addActorType("root");
+
+// The child actor doesn't provide a form description
+let ChildActor = protocol.ActorClass({
+  typeName: "child",
+  initialize(conn) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+  },
+
+  form(detail) {
+    return {
+      actor: this.actorID,
+      extra: "extra"
+    }
+  },
+
+  getChild: method(function() {
+    return this;
+  }, {
+    response: RetVal("child")
+  }),
+});
+
+let ChildFront = protocol.FrontClass(ChildActor, {
+  initialize(client) {
+    protocol.Front.prototype.initialize.call(this, client);
+  },
+
+  form(v, ctx, detail) {
+    this.extra = v.extra;
+  }
+});
+
+// The root actor does provide a form description.
+let RootActor = protocol.ActorClass({
+  typeName: "root",
+  initialize(conn) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    this.manage(this);
+    this.child = new ChildActor();
+  },
+
+  // Basic form type, relies on implicit DictType creation
+  formType: {
+    childActor: "child"
+  },
+
+  sayHello() {
+    return {
+      from: "root",
+      applicationType: "xpcshell-tests",
+      traits: []
+    }
+  },
+
+  // This detail uses explicit DictType creation
+  "formType#detail1": protocol.types.addDictType("RootActorFormTypeDetail1", {
+    detailItem: "child"
+  }),
+
+  // This detail a string type.
+  "formType#actorid": "string",
+
+  form(detail) {
+    if (detail === "detail1") {
+      return {
+        actor: this.actorID,
+        detailItem: this.child
+      }
+    } else if (detail === "actorid") {
+      return this.actorID;
+    }
+
+    return {
+      actor: this.actorID,
+      childActor: this.child
+    }
+  },
+
+  getDefault: method(function() {
+    return this;
+  }, {
+    response: RetVal("root")
+  }),
+
+  getDetail1: method(function() {
+    return this;
+  }, {
+    response: RetVal("root#detail1")
+  }),
+
+  getDetail2: method(function() {
+    return this;
+  }, {
+    response: {
+      item: RetVal("root#actorid")
+    }
+  }),
+
+  getUnknownDetail: method(function() {
+    return this;
+  }, {
+    response: RetVal("root#unknownDetail")
+  }),
+});
+
+let RootFront = protocol.FrontClass(RootActor, {
+  initialize(client) {
+    this.actorID = "root";
+    protocol.Front.prototype.initialize.call(this, client);
+
+    // Root owns itself.
+    this.manage(this);
+  },
+
+  form(v, ctx, detail) {
+    this.lastForm = v;
+  }
+});
+
+const run_test = Test(function*() {
+  DebuggerServer.createRootActor = (conn => {
+    return RootActor(conn);
+  });
+  DebuggerServer.init(() => true);
+
+  const connection = DebuggerServer.connectPipe();
+  const conn = new DebuggerClient(connection);
+  const client = Async(conn);
+
+  yield client.connect();
+
+  let rootFront = RootFront(conn);
+
+  // Trigger some methods that return forms.
+  let retval = yield rootFront.getDefault();
+  do_check_true(retval instanceof RootFront);
+  do_check_true(rootFront.lastForm.childActor instanceof ChildFront);
+
+  retval = yield rootFront.getDetail1();
+  do_check_true(retval instanceof RootFront);
+  do_check_true(rootFront.lastForm.detailItem instanceof ChildFront);
+
+  retval = yield rootFront.getDetail2();
+  do_check_true(retval instanceof RootFront);
+  do_check_true(typeof(rootFront.lastForm) === "string");
+
+  // getUnknownDetail should fail, since no typeName is specified.
+  try {
+    yield rootFront.getUnknownDetail();
+    do_check_true(false);
+  } catch(ex) {
+  }
+
+  yield client.close();
+});
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -60,16 +60,17 @@ support-files =
 [test_eval-02.js]
 [test_eval-03.js]
 [test_eval-04.js]
 [test_eval-05.js]
 [test_protocol_async.js]
 [test_protocol_simple.js]
 [test_protocol_longstring.js]
 [test_protocol_children.js]
+[test_protocol_formtype.js]
 [test_breakpoint-01.js]
 [test_register_actor.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_breakpoint-02.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_breakpoint-03.js]