|
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 } |