Bug 1072080 - Add the ability to define a marshaller for form data. r=jryans,jsantell
--- 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]