toolkit/devtools/server/protocol.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:8e3f7fbf6260
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/. */
4
5 "use strict";
6
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");
14
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 }
20
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 */
35
36 let types = Object.create(null);
37 exports.types = types;
38
39 let registeredTypes = new Map();
40 let registeredLifetimes = new Map();
41
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 }
64
65 if (typeof(type) !== "string") {
66 return type;
67 }
68
69 // If already registered, we're done here.
70 let reg = registeredTypes.get(type);
71 if (reg) return reg;
72
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));
78
79 if (collection === "array") {
80 return types.addArrayType(subtype);
81 } else if (collection === "nullable") {
82 return types.addNullableType(subtype);
83 }
84
85 if (registeredLifetimes.has(collection)) {
86 return types.addLifetimeType(collection, subtype);
87 }
88
89 throw Error("Unknown collection type: " + collection);
90 }
91
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 }
97
98 // Might be a lazily-loaded type
99 if (type === "longstring") {
100 require("devtools/server/actors/string");
101 return registeredTypes.get("longstring");
102 }
103
104 throw Error("Unknown type: " + type);
105 }
106
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 }
117
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 }
145
146 let type = object.merge({
147 name: name,
148 primitive: !(typeObject.read || typeObject.write),
149 read: identityWrite,
150 write: identityWrite
151 }, typeObject);
152
153 registeredTypes.set(name, type);
154
155 if (!options.thawed) {
156 Object.freeze(type);
157 }
158
159 return type;
160 };
161
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);
173
174 let name = "array:" + subtype.name;
175
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 };
186
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 },
212
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 }
226
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 }
254
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 }
278
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 }
289
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 }
308
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 }
335
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 }
351
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 }
376
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");
383
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 */
395
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 },
410
411 write: function(arg, ctx) {
412 return this.type.write(arg, ctx);
413 },
414
415 read: function(v, ctx, outArgs) {
416 outArgs[this.index] = this.type.read(v, ctx);
417 },
418
419 describe: function() {
420 return {
421 _arg: this.index,
422 type: this.type.name,
423 }
424 }
425 });
426 exports.Arg = Arg;
427
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 },
450
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 },
470
471 describe: function() {
472 return {
473 _option: this.index,
474 type: this.type.name,
475 }
476 }
477 });
478
479 exports.Option = Option;
480
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 },
491
492 write: function(v, ctx) {
493 return this.type.write(v, ctx);
494 },
495
496 read: function(v, ctx) {
497 return this.type.read(v, ctx);
498 },
499
500 describe: function() {
501 return {
502 _retval: this.type.name
503 }
504 }
505 });
506
507 exports.RetVal = RetVal;
508
509 /* Template handling functions */
510
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 }
523
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 }
532
533 if (template instanceof constructor) {
534 placeholders.push({ placeholder: template, path: [p for (p of path)] });
535 return placeholders;
536 }
537
538 for (let name in template) {
539 path.push(name);
540 findPlaceholders(template[name], constructor, path, placeholders);
541 path.pop();
542 }
543
544 return placeholders;
545 }
546
547
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 }
556
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 },
570
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 },
589
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 },
609
610 describe: function() { return describeTemplate(this.template); }
611 });
612
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 },
633
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 },
650
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 },
666
667 describe: function() { return describeTemplate(this.template); }
668 });
669
670 /**
671 * Actor and Front implementations
672 */
673
674 /**
675 * A protocol object that can manage the lifetime of other protocol
676 * objects.
677 */
678 let Pool = Class({
679 extends: EventTarget,
680
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 },
696
697 /**
698 * Return the parent pool for this client.
699 */
700 parent: function() { return this.conn.poolFor(this.actorID) },
701
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; },
707
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 },
720
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 }
728
729 this._poolMap.set(actor.actorID, actor);
730 return actor;
731 },
732
733 /**
734 * Remove an actor as a child of this pool.
735 */
736 unmanage: function(actor) {
737 this.__poolMap.delete(actor.actorID);
738 },
739
740 // true if the given actor ID exists in the pool.
741 has: function(actorID) this.__poolMap && this._poolMap.has(actorID),
742
743 // The actor for a given actor id stored in this pool
744 actor: function(actorID) this.__poolMap ? this._poolMap.get(actorID) : null,
745
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,
749
750 // True if this pool has no children.
751 isEmpty: function() !this.__poolMap || this._poolMap.size == 0,
752
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 },
783
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;
793
794 /**
795 * An actor in the actor tree.
796 */
797 let Actor = Class({
798 extends: Pool,
799
800 // Will contain the actor's ID
801 actorID: null,
802
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);
814
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 },
826
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 },
837
838 destroy: function() {
839 Pool.prototype.destroy.call(this);
840 this.actorID = null;
841 },
842
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 },
852
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 },
864
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;
872
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 }
891
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 }
900
901 let protoSpec = {
902 methods: [],
903 };
904
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 }
911
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;
921
922 protoSpec.methods.push(spec);
923 }
924 }
925
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 }
935
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);
942
943 let ret = this[spec.name].apply(this, args);
944
945 if (spec.oneway) {
946 // No need to send a response.
947 return;
948 }
949
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 }
962
963 conn.send(response);
964 };
965
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 };
978
979 actorProto.requestTypes[spec.request.type] = handler;
980 });
981
982 actorProto._actorSpec = protoSpec;
983 return actorProto;
984 }
985
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);
1000 }
1001 let cls = Class(actorProto(proto));
1002
1003 registeredTypes.get(proto.typeName).actorSpec = proto._actorSpec;
1004 return cls;
1005 };
1006
1007 /**
1008 * Base class for client-side actor fronts.
1009 */
1010 let Front = Class({
1011 extends: Pool,
1012
1013 actorID: null,
1014
1015 /**
1016 * The base class for client-side actor fronts.
1017 *
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);
1032 }
1033 },
1034
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"));
1041 }
1042 Pool.prototype.destroy.call(this);
1043 this.actorID = null;
1044 },
1045
1046 /**
1047 * @returns a promise that will resolve to the actorID this front
1048 * represents.
1049 */
1050 actor: function() { return promise.resolve(this.actorID) },
1051
1052 toString: function() { return "[Front for " + this.typeName + "/" + this.actorID + "]" },
1053
1054 /**
1055 * Update the actor from its representation.
1056 * Subclasses should override this.
1057 */
1058 form: function(form) {},
1059
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 });
1071 }
1072 },
1073
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 },
1083
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));
1094 }
1095 events.emit.apply(null, [this, event.name].concat(args));
1096 return;
1097 }
1098
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;
1105 }
1106
1107 let deferred = this._requests.shift();
1108 if (packet.error) {
1109 deferred.reject(packet.error);
1110 } else {
1111 deferred.resolve(packet);
1112 }
1113 }
1114 });
1115 exports.Front = Front;
1116
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;
1124 }
1125
1126 /**
1127 * Mark a method as a custom front implementation, replacing the generated
1128 * front method.
1129 *
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;
1140 }
1141
1142 function prototypeOf(obj) {
1143 return typeof(obj) === "function" ? obj.prototype : obj;
1144 }
1145
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!");
1154 }
1155 proto._actorSpec = actorType._actorSpec;
1156 proto.typeName = actorType.typeName;
1157
1158 // Generate request methods.
1159 let methods = proto._actorSpec.methods;
1160 methods.forEach(spec => {
1161 let name = spec.name;
1162
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 + ".");
1169 }
1170 // If the user doesn't need the impl don't generate it.
1171 if (!custom.impl) {
1172 return;
1173 }
1174 name = custom.impl;
1175 }
1176
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");
1183 }
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;
1196 }
1197 }
1198
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;
1204 }
1205
1206 return this.request(packet).then(response => {
1207 let ret = spec.response.read(response, this);
1208
1209 if (histogram) {
1210 histogram.add(+new Date - startTime);
1211 }
1212
1213 return ret;
1214 }).then(null, promiseDone);
1215 }
1216
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 })
1225 }
1226 }
1227 });
1228
1229
1230 // Process event specifications
1231 proto._clientSpec = {};
1232
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;
1241 }
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);
1246 }
1247 let handlers = preHandlers.get(preEvent);
1248 if (!handlers) {
1249 handlers = [];
1250 preHandlers.set(preEvent, handlers);
1251 }
1252 handlers.push(desc.value);
1253 }
1254 }
1255
1256 proto._clientSpec.events = new Map();
1257
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 });
1264 }
1265 }
1266 return proto;
1267 }
1268
1269 /**
1270 * Create a front class for the given actor class, with the given prototype.
1271 *
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;
1284 }
1285
1286
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 };
1295
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 });
1304 }
1305
1306 if (actorSpec.events) {
1307 for (let [name, request] of actorSpec.events) {
1308 ret.events[name] = request.describe();
1309 }
1310 }
1311
1312
1313 JSON.stringify(ret);
1314
1315 return ret;
1316 }
1317
1318 exports.dumpProtocolSpec = function() {
1319 let ret = {
1320 types: {},
1321 };
1322
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
1331 }
1332 } else if (type.category === "actor") {
1333 ret.types[name] = exports.dumpActorSpec(type);
1334 }
1335 }
1336
1337 return ret;
1338 }

mercurial