1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/server/protocol.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1338 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +let {Cu} = require("chrome"); 1.11 +let Services = require("Services"); 1.12 +let promise = require("devtools/toolkit/deprecated-sync-thenables"); 1.13 +let {Class} = require("sdk/core/heritage"); 1.14 +let {EventTarget} = require("sdk/event/target"); 1.15 +let events = require("sdk/event/core"); 1.16 +let object = require("sdk/util/object"); 1.17 + 1.18 +// Waiting for promise.done() to be added, see bug 851321 1.19 +function promiseDone(err) { 1.20 + console.error(err); 1.21 + return promise.reject(err); 1.22 +} 1.23 + 1.24 +/** 1.25 + * Types: named marshallers/demarshallers. 1.26 + * 1.27 + * Types provide a 'write' function that takes a js representation and 1.28 + * returns a protocol representation, and a "read" function that 1.29 + * takes a protocol representation and returns a js representation. 1.30 + * 1.31 + * The read and write methods are also passed a context object that 1.32 + * represent the actor or front requesting the translation. 1.33 + * 1.34 + * Types are referred to with a typestring. Basic types are 1.35 + * registered by name using addType, and more complex types can 1.36 + * be generated by adding detail to the type name. 1.37 + */ 1.38 + 1.39 +let types = Object.create(null); 1.40 +exports.types = types; 1.41 + 1.42 +let registeredTypes = new Map(); 1.43 +let registeredLifetimes = new Map(); 1.44 + 1.45 +/** 1.46 + * Return the type object associated with a given typestring. 1.47 + * If passed a type object, it will be returned unchanged. 1.48 + * 1.49 + * Types can be registered with addType, or can be created on 1.50 + * the fly with typestrings. Examples: 1.51 + * 1.52 + * boolean 1.53 + * threadActor 1.54 + * threadActor#detail 1.55 + * array:threadActor 1.56 + * array:array:threadActor#detail 1.57 + * 1.58 + * @param [typestring|type] type 1.59 + * Either a typestring naming a type or a type object. 1.60 + * 1.61 + * @returns a type object. 1.62 + */ 1.63 +types.getType = function(type) { 1.64 + if (!type) { 1.65 + return types.Primitive; 1.66 + } 1.67 + 1.68 + if (typeof(type) !== "string") { 1.69 + return type; 1.70 + } 1.71 + 1.72 + // If already registered, we're done here. 1.73 + let reg = registeredTypes.get(type); 1.74 + if (reg) return reg; 1.75 + 1.76 + // New type, see if it's a collection/lifetime type: 1.77 + let sep = type.indexOf(":"); 1.78 + if (sep >= 0) { 1.79 + let collection = type.substring(0, sep); 1.80 + let subtype = types.getType(type.substring(sep + 1)); 1.81 + 1.82 + if (collection === "array") { 1.83 + return types.addArrayType(subtype); 1.84 + } else if (collection === "nullable") { 1.85 + return types.addNullableType(subtype); 1.86 + } 1.87 + 1.88 + if (registeredLifetimes.has(collection)) { 1.89 + return types.addLifetimeType(collection, subtype); 1.90 + } 1.91 + 1.92 + throw Error("Unknown collection type: " + collection); 1.93 + } 1.94 + 1.95 + // Not a collection, might be actor detail 1.96 + let pieces = type.split("#", 2); 1.97 + if (pieces.length > 1) { 1.98 + return types.addActorDetail(type, pieces[0], pieces[1]); 1.99 + } 1.100 + 1.101 + // Might be a lazily-loaded type 1.102 + if (type === "longstring") { 1.103 + require("devtools/server/actors/string"); 1.104 + return registeredTypes.get("longstring"); 1.105 + } 1.106 + 1.107 + throw Error("Unknown type: " + type); 1.108 +} 1.109 + 1.110 +/** 1.111 + * Don't allow undefined when writing primitive types to packets. If 1.112 + * you want to allow undefined, use a nullable type. 1.113 + */ 1.114 +function identityWrite(v) { 1.115 + if (v === undefined) { 1.116 + throw Error("undefined passed where a value is required"); 1.117 + } 1.118 + return v; 1.119 +} 1.120 + 1.121 +/** 1.122 + * Add a type to the type system. 1.123 + * 1.124 + * When registering a type, you can provide `read` and `write` methods. 1.125 + * 1.126 + * The `read` method will be passed a JS object value from the JSON 1.127 + * packet and must return a native representation. The `write` method will 1.128 + * be passed a native representation and should provide a JSONable value. 1.129 + * 1.130 + * These methods will both be passed a context. The context is the object 1.131 + * performing or servicing the request - on the server side it will be 1.132 + * an Actor, on the client side it will be a Front. 1.133 + * 1.134 + * @param typestring name 1.135 + * Name to register 1.136 + * @param object typeObject 1.137 + * An object whose properties will be stored in the type, including 1.138 + * the `read` and `write` methods. 1.139 + * @param object options 1.140 + * Can specify `thawed` to prevent the type from being frozen. 1.141 + * 1.142 + * @returns a type object that can be used in protocol definitions. 1.143 + */ 1.144 +types.addType = function(name, typeObject={}, options={}) { 1.145 + if (registeredTypes.has(name)) { 1.146 + throw Error("Type '" + name + "' already exists."); 1.147 + } 1.148 + 1.149 + let type = object.merge({ 1.150 + name: name, 1.151 + primitive: !(typeObject.read || typeObject.write), 1.152 + read: identityWrite, 1.153 + write: identityWrite 1.154 + }, typeObject); 1.155 + 1.156 + registeredTypes.set(name, type); 1.157 + 1.158 + if (!options.thawed) { 1.159 + Object.freeze(type); 1.160 + } 1.161 + 1.162 + return type; 1.163 +}; 1.164 + 1.165 +/** 1.166 + * Add an array type to the type system. 1.167 + * 1.168 + * getType() will call this function if provided an "array:<type>" 1.169 + * typestring. 1.170 + * 1.171 + * @param type subtype 1.172 + * The subtype to be held by the array. 1.173 + */ 1.174 +types.addArrayType = function(subtype) { 1.175 + subtype = types.getType(subtype); 1.176 + 1.177 + let name = "array:" + subtype.name; 1.178 + 1.179 + // Arrays of primitive types are primitive types themselves. 1.180 + if (subtype.primitive) { 1.181 + return types.addType(name); 1.182 + } 1.183 + return types.addType(name, { 1.184 + category: "array", 1.185 + read: (v, ctx) => [subtype.read(i, ctx) for (i of v)], 1.186 + write: (v, ctx) => [subtype.write(i, ctx) for (i of v)] 1.187 + }); 1.188 +}; 1.189 + 1.190 +/** 1.191 + * Add a dict type to the type system. This allows you to serialize 1.192 + * a JS object that contains non-primitive subtypes. 1.193 + * 1.194 + * Properties of the value that aren't included in the specializations 1.195 + * will be serialized as primitive values. 1.196 + * 1.197 + * @param object specializations 1.198 + * A dict of property names => type 1.199 + */ 1.200 +types.addDictType = function(name, specializations) { 1.201 + return types.addType(name, { 1.202 + category: "dict", 1.203 + specializations: specializations, 1.204 + read: (v, ctx) => { 1.205 + let ret = {}; 1.206 + for (let prop in v) { 1.207 + if (prop in specializations) { 1.208 + ret[prop] = types.getType(specializations[prop]).read(v[prop], ctx); 1.209 + } else { 1.210 + ret[prop] = v[prop]; 1.211 + } 1.212 + } 1.213 + return ret; 1.214 + }, 1.215 + 1.216 + write: (v, ctx) => { 1.217 + let ret = {}; 1.218 + for (let prop in v) { 1.219 + if (prop in specializations) { 1.220 + ret[prop] = types.getType(specializations[prop]).write(v[prop], ctx); 1.221 + } else { 1.222 + ret[prop] = v[prop]; 1.223 + } 1.224 + } 1.225 + return ret; 1.226 + } 1.227 + }) 1.228 +} 1.229 + 1.230 +/** 1.231 + * Register an actor type with the type system. 1.232 + * 1.233 + * Types are marshalled differently when communicating server->client 1.234 + * than they are when communicating client->server. The server needs 1.235 + * to provide useful information to the client, so uses the actor's 1.236 + * `form` method to get a json representation of the actor. When 1.237 + * making a request from the client we only need the actor ID string. 1.238 + * 1.239 + * This function can be called before the associated actor has been 1.240 + * constructed, but the read and write methods won't work until 1.241 + * the associated addActorImpl or addActorFront methods have been 1.242 + * called during actor/front construction. 1.243 + * 1.244 + * @param string name 1.245 + * The typestring to register. 1.246 + */ 1.247 +types.addActorType = function(name) { 1.248 + let type = types.addType(name, { 1.249 + _actor: true, 1.250 + category: "actor", 1.251 + read: (v, ctx, detail) => { 1.252 + // If we're reading a request on the server side, just 1.253 + // find the actor registered with this actorID. 1.254 + if (ctx instanceof Actor) { 1.255 + return ctx.conn.getActor(v); 1.256 + } 1.257 + 1.258 + // Reading a response on the client side, check for an 1.259 + // existing front on the connection, and create the front 1.260 + // if it isn't found. 1.261 + let actorID = typeof(v) === "string" ? v : v.actor; 1.262 + let front = ctx.conn.getActor(actorID); 1.263 + if (front) { 1.264 + front.form(v, detail, ctx); 1.265 + } else { 1.266 + front = new type.frontClass(ctx.conn, v, detail, ctx) 1.267 + front.actorID = actorID; 1.268 + ctx.marshallPool().manage(front); 1.269 + } 1.270 + return front; 1.271 + }, 1.272 + write: (v, ctx, detail) => { 1.273 + // If returning a response from the server side, make sure 1.274 + // the actor is added to a parent object and return its form. 1.275 + if (v instanceof Actor) { 1.276 + if (!v.actorID) { 1.277 + ctx.marshallPool().manage(v); 1.278 + } 1.279 + return v.form(detail); 1.280 + } 1.281 + 1.282 + // Writing a request from the client side, just send the actor id. 1.283 + return v.actorID; 1.284 + }, 1.285 + }, { 1.286 + // We usually freeze types, but actor types are updated when clients are 1.287 + // created, so don't freeze yet. 1.288 + thawed: true 1.289 + }); 1.290 + return type; 1.291 +} 1.292 + 1.293 +types.addNullableType = function(subtype) { 1.294 + subtype = types.getType(subtype); 1.295 + return types.addType("nullable:" + subtype.name, { 1.296 + category: "nullable", 1.297 + read: (value, ctx) => { 1.298 + if (value == null) { 1.299 + return value; 1.300 + } 1.301 + return subtype.read(value, ctx); 1.302 + }, 1.303 + write: (value, ctx) => { 1.304 + if (value == null) { 1.305 + return value; 1.306 + } 1.307 + return subtype.write(value, ctx); 1.308 + } 1.309 + }); 1.310 +} 1.311 + 1.312 +/** 1.313 + * Register an actor detail type. This is just like an actor type, but 1.314 + * will pass a detail hint to the actor's form method during serialization/ 1.315 + * deserialization. 1.316 + * 1.317 + * This is called by getType() when passed an 'actorType#detail' string. 1.318 + * 1.319 + * @param string name 1.320 + * The typestring to register this type as. 1.321 + * @param type actorType 1.322 + * The actor type you'll be detailing. 1.323 + * @param string detail 1.324 + * The detail to pass. 1.325 + */ 1.326 +types.addActorDetail = function(name, actorType, detail) { 1.327 + actorType = types.getType(actorType); 1.328 + if (!actorType._actor) { 1.329 + throw Error("Details only apply to actor types, tried to add detail '" + detail + "'' to " + actorType.name + "\n"); 1.330 + } 1.331 + return types.addType(name, { 1.332 + _actor: true, 1.333 + category: "detail", 1.334 + read: (v, ctx) => actorType.read(v, ctx, detail), 1.335 + write: (v, ctx) => actorType.write(v, ctx, detail) 1.336 + }); 1.337 +} 1.338 + 1.339 +/** 1.340 + * Register an actor lifetime. This lets the type system find a parent 1.341 + * actor that differs from the actor fulfilling the request. 1.342 + * 1.343 + * @param string name 1.344 + * The lifetime name to use in typestrings. 1.345 + * @param string prop 1.346 + * The property of the actor that holds the parent that should be used. 1.347 + */ 1.348 +types.addLifetime = function(name, prop) { 1.349 + if (registeredLifetimes.has(name)) { 1.350 + throw Error("Lifetime '" + name + "' already registered."); 1.351 + } 1.352 + registeredLifetimes.set(name, prop); 1.353 +} 1.354 + 1.355 +/** 1.356 + * Register a lifetime type. This creates an actor type tied to the given 1.357 + * lifetime. 1.358 + * 1.359 + * This is called by getType() when passed a '<lifetimeType>:<actorType>' 1.360 + * typestring. 1.361 + * 1.362 + * @param string lifetime 1.363 + * A lifetime string previously regisered with addLifetime() 1.364 + * @param type subtype 1.365 + * An actor type 1.366 + */ 1.367 +types.addLifetimeType = function(lifetime, subtype) { 1.368 + subtype = types.getType(subtype); 1.369 + if (!subtype._actor) { 1.370 + throw Error("Lifetimes only apply to actor types, tried to apply lifetime '" + lifetime + "'' to " + subtype.name); 1.371 + } 1.372 + let prop = registeredLifetimes.get(lifetime); 1.373 + return types.addType(lifetime + ":" + subtype.name, { 1.374 + category: "lifetime", 1.375 + read: (value, ctx) => subtype.read(value, ctx[prop]), 1.376 + write: (value, ctx) => subtype.write(value, ctx[prop]) 1.377 + }) 1.378 +} 1.379 + 1.380 +// Add a few named primitive types. 1.381 +types.Primitive = types.addType("primitive"); 1.382 +types.String = types.addType("string"); 1.383 +types.Number = types.addType("number"); 1.384 +types.Boolean = types.addType("boolean"); 1.385 +types.JSON = types.addType("json"); 1.386 + 1.387 +/** 1.388 + * Request/Response templates and generation 1.389 + * 1.390 + * Request packets are specified as json templates with 1.391 + * Arg and Option placeholders where arguments should be 1.392 + * placed. 1.393 + * 1.394 + * Reponse packets are also specified as json templates, 1.395 + * with a RetVal placeholder where the return value should be 1.396 + * placed. 1.397 + */ 1.398 + 1.399 +/** 1.400 + * Placeholder for simple arguments. 1.401 + * 1.402 + * @param number index 1.403 + * The argument index to place at this position. 1.404 + * @param type type 1.405 + * The argument should be marshalled as this type. 1.406 + * @constructor 1.407 + */ 1.408 +let Arg = Class({ 1.409 + initialize: function(index, type) { 1.410 + this.index = index; 1.411 + this.type = types.getType(type); 1.412 + }, 1.413 + 1.414 + write: function(arg, ctx) { 1.415 + return this.type.write(arg, ctx); 1.416 + }, 1.417 + 1.418 + read: function(v, ctx, outArgs) { 1.419 + outArgs[this.index] = this.type.read(v, ctx); 1.420 + }, 1.421 + 1.422 + describe: function() { 1.423 + return { 1.424 + _arg: this.index, 1.425 + type: this.type.name, 1.426 + } 1.427 + } 1.428 +}); 1.429 +exports.Arg = Arg; 1.430 + 1.431 +/** 1.432 + * Placeholder for an options argument value that should be hoisted 1.433 + * into the packet. 1.434 + * 1.435 + * If provided in a method specification: 1.436 + * 1.437 + * { optionArg: Option(1)} 1.438 + * 1.439 + * Then arguments[1].optionArg will be placed in the packet in this 1.440 + * value's place. 1.441 + * 1.442 + * @param number index 1.443 + * The argument index of the options value. 1.444 + * @param type type 1.445 + * The argument should be marshalled as this type. 1.446 + * @constructor 1.447 + */ 1.448 +let Option = Class({ 1.449 + extends: Arg, 1.450 + initialize: function(index, type) { 1.451 + Arg.prototype.initialize.call(this, index, type) 1.452 + }, 1.453 + 1.454 + write: function(arg, ctx, name) { 1.455 + if (!arg) { 1.456 + return undefined; 1.457 + } 1.458 + let v = arg[name] || undefined; 1.459 + if (v === undefined) { 1.460 + return undefined; 1.461 + } 1.462 + return this.type.write(v, ctx); 1.463 + }, 1.464 + read: function(v, ctx, outArgs, name) { 1.465 + if (outArgs[this.index] === undefined) { 1.466 + outArgs[this.index] = {}; 1.467 + } 1.468 + if (v === undefined) { 1.469 + return; 1.470 + } 1.471 + outArgs[this.index][name] = this.type.read(v, ctx); 1.472 + }, 1.473 + 1.474 + describe: function() { 1.475 + return { 1.476 + _option: this.index, 1.477 + type: this.type.name, 1.478 + } 1.479 + } 1.480 +}); 1.481 + 1.482 +exports.Option = Option; 1.483 + 1.484 +/** 1.485 + * Placeholder for return values in a response template. 1.486 + * 1.487 + * @param type type 1.488 + * The return value should be marshalled as this type. 1.489 + */ 1.490 +let RetVal = Class({ 1.491 + initialize: function(type) { 1.492 + this.type = types.getType(type); 1.493 + }, 1.494 + 1.495 + write: function(v, ctx) { 1.496 + return this.type.write(v, ctx); 1.497 + }, 1.498 + 1.499 + read: function(v, ctx) { 1.500 + return this.type.read(v, ctx); 1.501 + }, 1.502 + 1.503 + describe: function() { 1.504 + return { 1.505 + _retval: this.type.name 1.506 + } 1.507 + } 1.508 +}); 1.509 + 1.510 +exports.RetVal = RetVal; 1.511 + 1.512 +/* Template handling functions */ 1.513 + 1.514 +/** 1.515 + * Get the value at a given path, or undefined if not found. 1.516 + */ 1.517 +function getPath(obj, path) { 1.518 + for (let name of path) { 1.519 + if (!(name in obj)) { 1.520 + return undefined; 1.521 + } 1.522 + obj = obj[name]; 1.523 + } 1.524 + return obj; 1.525 +} 1.526 + 1.527 +/** 1.528 + * Find Placeholders in the template and save them along with their 1.529 + * paths. 1.530 + */ 1.531 +function findPlaceholders(template, constructor, path=[], placeholders=[]) { 1.532 + if (!template || typeof(template) != "object") { 1.533 + return placeholders; 1.534 + } 1.535 + 1.536 + if (template instanceof constructor) { 1.537 + placeholders.push({ placeholder: template, path: [p for (p of path)] }); 1.538 + return placeholders; 1.539 + } 1.540 + 1.541 + for (let name in template) { 1.542 + path.push(name); 1.543 + findPlaceholders(template[name], constructor, path, placeholders); 1.544 + path.pop(); 1.545 + } 1.546 + 1.547 + return placeholders; 1.548 +} 1.549 + 1.550 + 1.551 +function describeTemplate(template) { 1.552 + return JSON.parse(JSON.stringify(template, (key, value) => { 1.553 + if (value.describe) { 1.554 + return value.describe(); 1.555 + } 1.556 + return value; 1.557 + })); 1.558 +} 1.559 + 1.560 +/** 1.561 + * Manages a request template. 1.562 + * 1.563 + * @param object template 1.564 + * The request template. 1.565 + * @construcor 1.566 + */ 1.567 +let Request = Class({ 1.568 + initialize: function(template={}) { 1.569 + this.type = template.type; 1.570 + this.template = template; 1.571 + this.args = findPlaceholders(template, Arg); 1.572 + }, 1.573 + 1.574 + /** 1.575 + * Write a request. 1.576 + * 1.577 + * @param array fnArgs 1.578 + * The function arguments to place in the request. 1.579 + * @param object ctx 1.580 + * The object making the request. 1.581 + * @returns a request packet. 1.582 + */ 1.583 + write: function(fnArgs, ctx) { 1.584 + let str = JSON.stringify(this.template, (key, value) => { 1.585 + if (value instanceof Arg) { 1.586 + return value.write(fnArgs[value.index], ctx, key); 1.587 + } 1.588 + return value; 1.589 + }); 1.590 + return JSON.parse(str); 1.591 + }, 1.592 + 1.593 + /** 1.594 + * Read a request. 1.595 + * 1.596 + * @param object packet 1.597 + * The request packet. 1.598 + * @param object ctx 1.599 + * The object making the request. 1.600 + * @returns an arguments array 1.601 + */ 1.602 + read: function(packet, ctx) { 1.603 + let fnArgs = []; 1.604 + for (let templateArg of this.args) { 1.605 + let arg = templateArg.placeholder; 1.606 + let path = templateArg.path; 1.607 + let name = path[path.length - 1]; 1.608 + arg.read(getPath(packet, path), ctx, fnArgs, name); 1.609 + } 1.610 + return fnArgs; 1.611 + }, 1.612 + 1.613 + describe: function() { return describeTemplate(this.template); } 1.614 +}); 1.615 + 1.616 +/** 1.617 + * Manages a response template. 1.618 + * 1.619 + * @param object template 1.620 + * The response template. 1.621 + * @construcor 1.622 + */ 1.623 +let Response = Class({ 1.624 + initialize: function(template={}) { 1.625 + this.template = template; 1.626 + let placeholders = findPlaceholders(template, RetVal); 1.627 + if (placeholders.length > 1) { 1.628 + throw Error("More than one RetVal specified in response"); 1.629 + } 1.630 + let placeholder = placeholders.shift(); 1.631 + if (placeholder) { 1.632 + this.retVal = placeholder.placeholder; 1.633 + this.path = placeholder.path; 1.634 + } 1.635 + }, 1.636 + 1.637 + /** 1.638 + * Write a response for the given return value. 1.639 + * 1.640 + * @param val ret 1.641 + * The return value. 1.642 + * @param object ctx 1.643 + * The object writing the response. 1.644 + */ 1.645 + write: function(ret, ctx) { 1.646 + return JSON.parse(JSON.stringify(this.template, function(key, value) { 1.647 + if (value instanceof RetVal) { 1.648 + return value.write(ret, ctx); 1.649 + } 1.650 + return value; 1.651 + })); 1.652 + }, 1.653 + 1.654 + /** 1.655 + * Read a return value from the given response. 1.656 + * 1.657 + * @param object packet 1.658 + * The response packet. 1.659 + * @param object ctx 1.660 + * The object reading the response. 1.661 + */ 1.662 + read: function(packet, ctx) { 1.663 + if (!this.retVal) { 1.664 + return undefined; 1.665 + } 1.666 + let v = getPath(packet, this.path); 1.667 + return this.retVal.read(v, ctx); 1.668 + }, 1.669 + 1.670 + describe: function() { return describeTemplate(this.template); } 1.671 +}); 1.672 + 1.673 +/** 1.674 + * Actor and Front implementations 1.675 + */ 1.676 + 1.677 +/** 1.678 + * A protocol object that can manage the lifetime of other protocol 1.679 + * objects. 1.680 + */ 1.681 +let Pool = Class({ 1.682 + extends: EventTarget, 1.683 + 1.684 + /** 1.685 + * Pools are used on both sides of the connection to help coordinate 1.686 + * lifetimes. 1.687 + * 1.688 + * @param optional conn 1.689 + * Either a DebuggerServerConnection or a DebuggerClient. Must have 1.690 + * addActorPool, removeActorPool, and poolFor. 1.691 + * conn can be null if the subclass provides a conn property. 1.692 + * @constructor 1.693 + */ 1.694 + initialize: function(conn) { 1.695 + if (conn) { 1.696 + this.conn = conn; 1.697 + } 1.698 + }, 1.699 + 1.700 + /** 1.701 + * Return the parent pool for this client. 1.702 + */ 1.703 + parent: function() { return this.conn.poolFor(this.actorID) }, 1.704 + 1.705 + /** 1.706 + * Override this if you want actors returned by this actor 1.707 + * to belong to a different actor by default. 1.708 + */ 1.709 + marshallPool: function() { return this; }, 1.710 + 1.711 + /** 1.712 + * Pool is the base class for all actors, even leaf nodes. 1.713 + * If the child map is actually referenced, go ahead and create 1.714 + * the stuff needed by the pool. 1.715 + */ 1.716 + __poolMap: null, 1.717 + get _poolMap() { 1.718 + if (this.__poolMap) return this.__poolMap; 1.719 + this.__poolMap = new Map(); 1.720 + this.conn.addActorPool(this); 1.721 + return this.__poolMap; 1.722 + }, 1.723 + 1.724 + /** 1.725 + * Add an actor as a child of this pool. 1.726 + */ 1.727 + manage: function(actor) { 1.728 + if (!actor.actorID) { 1.729 + actor.actorID = this.conn.allocID(actor.actorPrefix || actor.typeName); 1.730 + } 1.731 + 1.732 + this._poolMap.set(actor.actorID, actor); 1.733 + return actor; 1.734 + }, 1.735 + 1.736 + /** 1.737 + * Remove an actor as a child of this pool. 1.738 + */ 1.739 + unmanage: function(actor) { 1.740 + this.__poolMap.delete(actor.actorID); 1.741 + }, 1.742 + 1.743 + // true if the given actor ID exists in the pool. 1.744 + has: function(actorID) this.__poolMap && this._poolMap.has(actorID), 1.745 + 1.746 + // The actor for a given actor id stored in this pool 1.747 + actor: function(actorID) this.__poolMap ? this._poolMap.get(actorID) : null, 1.748 + 1.749 + // Same as actor, should update debugger connection to use 'actor' 1.750 + // and then remove this. 1.751 + get: function(actorID) this.__poolMap ? this._poolMap.get(actorID) : null, 1.752 + 1.753 + // True if this pool has no children. 1.754 + isEmpty: function() !this.__poolMap || this._poolMap.size == 0, 1.755 + 1.756 + /** 1.757 + * Destroy this item, removing it from a parent if it has one, 1.758 + * and destroying all children if necessary. 1.759 + */ 1.760 + destroy: function() { 1.761 + let parent = this.parent(); 1.762 + if (parent) { 1.763 + parent.unmanage(this); 1.764 + } 1.765 + if (!this.__poolMap) { 1.766 + return; 1.767 + } 1.768 + for (let actor of this.__poolMap.values()) { 1.769 + // Self-owned actors are ok, but don't need destroying twice. 1.770 + if (actor === this) { 1.771 + continue; 1.772 + } 1.773 + let destroy = actor.destroy; 1.774 + if (destroy) { 1.775 + // Disconnect destroy while we're destroying in case of (misbehaving) 1.776 + // circular ownership. 1.777 + actor.destroy = null; 1.778 + destroy.call(actor); 1.779 + actor.destroy = destroy; 1.780 + } 1.781 + }; 1.782 + this.conn.removeActorPool(this, true); 1.783 + this.__poolMap.clear(); 1.784 + this.__poolMap = null; 1.785 + }, 1.786 + 1.787 + /** 1.788 + * For getting along with the debugger server pools, should be removable 1.789 + * eventually. 1.790 + */ 1.791 + cleanup: function() { 1.792 + this.destroy(); 1.793 + } 1.794 +}); 1.795 +exports.Pool = Pool; 1.796 + 1.797 +/** 1.798 + * An actor in the actor tree. 1.799 + */ 1.800 +let Actor = Class({ 1.801 + extends: Pool, 1.802 + 1.803 + // Will contain the actor's ID 1.804 + actorID: null, 1.805 + 1.806 + /** 1.807 + * Initialize an actor. 1.808 + * 1.809 + * @param optional conn 1.810 + * Either a DebuggerServerConnection or a DebuggerClient. Must have 1.811 + * addActorPool, removeActorPool, and poolFor. 1.812 + * conn can be null if the subclass provides a conn property. 1.813 + * @constructor 1.814 + */ 1.815 + initialize: function(conn) { 1.816 + Pool.prototype.initialize.call(this, conn); 1.817 + 1.818 + // Forward events to the connection. 1.819 + if (this._actorSpec && this._actorSpec.events) { 1.820 + for (let key of this._actorSpec.events.keys()) { 1.821 + let name = key; 1.822 + let sendEvent = this._sendEvent.bind(this, name) 1.823 + this.on(name, (...args) => { 1.824 + sendEvent.apply(null, args); 1.825 + }); 1.826 + } 1.827 + } 1.828 + }, 1.829 + 1.830 + _sendEvent: function(name, ...args) { 1.831 + if (!this._actorSpec.events.has(name)) { 1.832 + // It's ok to emit events that don't go over the wire. 1.833 + return; 1.834 + } 1.835 + let request = this._actorSpec.events.get(name); 1.836 + let packet = request.write(args, this); 1.837 + packet.from = packet.from || this.actorID; 1.838 + this.conn.send(packet); 1.839 + }, 1.840 + 1.841 + destroy: function() { 1.842 + Pool.prototype.destroy.call(this); 1.843 + this.actorID = null; 1.844 + }, 1.845 + 1.846 + /** 1.847 + * Override this method in subclasses to serialize the actor. 1.848 + * @param [optional] string hint 1.849 + * Optional string to customize the form. 1.850 + * @returns A jsonable object. 1.851 + */ 1.852 + form: function(hint) { 1.853 + return { actor: this.actorID } 1.854 + }, 1.855 + 1.856 + writeError: function(err) { 1.857 + console.error(err); 1.858 + if (err.stack) { 1.859 + dump(err.stack); 1.860 + } 1.861 + this.conn.send({ 1.862 + from: this.actorID, 1.863 + error: "unknownError", 1.864 + message: err.toString() 1.865 + }); 1.866 + }, 1.867 + 1.868 + _queueResponse: function(create) { 1.869 + let pending = this._pendingResponse || promise.resolve(null); 1.870 + let response = create(pending); 1.871 + this._pendingResponse = response; 1.872 + } 1.873 +}); 1.874 +exports.Actor = Actor; 1.875 + 1.876 +/** 1.877 + * Tags a prtotype method as an actor method implementation. 1.878 + * 1.879 + * @param function fn 1.880 + * The implementation function, will be returned. 1.881 + * @param spec 1.882 + * The method specification, with the following (optional) properties: 1.883 + * request (object): a request template. 1.884 + * response (object): a response template. 1.885 + * oneway (bool): 'true' if no response should be sent. 1.886 + * telemetry (string): Telemetry probe ID for measuring completion time. 1.887 + */ 1.888 +exports.method = function(fn, spec={}) { 1.889 + fn._methodSpec = Object.freeze(spec); 1.890 + if (spec.request) Object.freeze(spec.request); 1.891 + if (spec.response) Object.freeze(spec.response); 1.892 + return fn; 1.893 +} 1.894 + 1.895 +/** 1.896 + * Process an actor definition from its prototype and generate 1.897 + * request handlers. 1.898 + */ 1.899 +let actorProto = function(actorProto) { 1.900 + if (actorProto._actorSpec) { 1.901 + throw new Error("actorProto called twice on the same actor prototype!"); 1.902 + } 1.903 + 1.904 + let protoSpec = { 1.905 + methods: [], 1.906 + }; 1.907 + 1.908 + // Find method specifications attached to prototype properties. 1.909 + for (let name of Object.getOwnPropertyNames(actorProto)) { 1.910 + let desc = Object.getOwnPropertyDescriptor(actorProto, name); 1.911 + if (!desc.value) { 1.912 + continue; 1.913 + } 1.914 + 1.915 + if (desc.value._methodSpec) { 1.916 + let frozenSpec = desc.value._methodSpec; 1.917 + let spec = {}; 1.918 + spec.name = frozenSpec.name || name; 1.919 + spec.request = Request(object.merge({type: spec.name}, frozenSpec.request || undefined)); 1.920 + spec.response = Response(frozenSpec.response || undefined); 1.921 + spec.telemetry = frozenSpec.telemetry; 1.922 + spec.release = frozenSpec.release; 1.923 + spec.oneway = frozenSpec.oneway; 1.924 + 1.925 + protoSpec.methods.push(spec); 1.926 + } 1.927 + } 1.928 + 1.929 + // Find event specifications 1.930 + if (actorProto.events) { 1.931 + protoSpec.events = new Map(); 1.932 + for (let name in actorProto.events) { 1.933 + let eventRequest = actorProto.events[name]; 1.934 + Object.freeze(eventRequest); 1.935 + protoSpec.events.set(name, Request(object.merge({type: name}, eventRequest))); 1.936 + } 1.937 + } 1.938 + 1.939 + // Generate request handlers for each method definition 1.940 + actorProto.requestTypes = Object.create(null); 1.941 + protoSpec.methods.forEach(spec => { 1.942 + let handler = function(packet, conn) { 1.943 + try { 1.944 + let args = spec.request.read(packet, this); 1.945 + 1.946 + let ret = this[spec.name].apply(this, args); 1.947 + 1.948 + if (spec.oneway) { 1.949 + // No need to send a response. 1.950 + return; 1.951 + } 1.952 + 1.953 + let sendReturn = (ret) => { 1.954 + let response = spec.response.write(ret, this); 1.955 + response.from = this.actorID; 1.956 + // If spec.release has been specified, destroy the object. 1.957 + if (spec.release) { 1.958 + try { 1.959 + this.destroy(); 1.960 + } catch(e) { 1.961 + this.writeError(e); 1.962 + return; 1.963 + } 1.964 + } 1.965 + 1.966 + conn.send(response); 1.967 + }; 1.968 + 1.969 + this._queueResponse(p => { 1.970 + return p 1.971 + .then(() => ret) 1.972 + .then(sendReturn) 1.973 + .then(null, this.writeError.bind(this)); 1.974 + }) 1.975 + } catch(e) { 1.976 + this._queueResponse(p => { 1.977 + return p.then(() => this.writeError(e)); 1.978 + }); 1.979 + } 1.980 + }; 1.981 + 1.982 + actorProto.requestTypes[spec.request.type] = handler; 1.983 + }); 1.984 + 1.985 + actorProto._actorSpec = protoSpec; 1.986 + return actorProto; 1.987 +} 1.988 + 1.989 +/** 1.990 + * Create an actor class for the given actor prototype. 1.991 + * 1.992 + * @param object proto 1.993 + * The object prototype. Must have a 'typeName' property, 1.994 + * should have method definitions, can have event definitions. 1.995 + */ 1.996 +exports.ActorClass = function(proto) { 1.997 + if (!proto.typeName) { 1.998 + throw Error("Actor prototype must have a typeName member."); 1.999 + } 1.1000 + proto.extends = Actor; 1.1001 + if (!registeredTypes.has(proto.typeName)) { 1.1002 + types.addActorType(proto.typeName); 1.1003 + } 1.1004 + let cls = Class(actorProto(proto)); 1.1005 + 1.1006 + registeredTypes.get(proto.typeName).actorSpec = proto._actorSpec; 1.1007 + return cls; 1.1008 +}; 1.1009 + 1.1010 +/** 1.1011 + * Base class for client-side actor fronts. 1.1012 + */ 1.1013 +let Front = Class({ 1.1014 + extends: Pool, 1.1015 + 1.1016 + actorID: null, 1.1017 + 1.1018 + /** 1.1019 + * The base class for client-side actor fronts. 1.1020 + * 1.1021 + * @param optional conn 1.1022 + * Either a DebuggerServerConnection or a DebuggerClient. Must have 1.1023 + * addActorPool, removeActorPool, and poolFor. 1.1024 + * conn can be null if the subclass provides a conn property. 1.1025 + * @param optional form 1.1026 + * The json form provided by the server. 1.1027 + * @constructor 1.1028 + */ 1.1029 + initialize: function(conn=null, form=null, detail=null, context=null) { 1.1030 + Pool.prototype.initialize.call(this, conn); 1.1031 + this._requests = []; 1.1032 + if (form) { 1.1033 + this.actorID = form.actor; 1.1034 + this.form(form, detail, context); 1.1035 + } 1.1036 + }, 1.1037 + 1.1038 + destroy: function() { 1.1039 + // Reject all outstanding requests, they won't make sense after 1.1040 + // the front is destroyed. 1.1041 + while (this._requests && this._requests.length > 0) { 1.1042 + let deferred = this._requests.shift(); 1.1043 + deferred.reject(new Error("Connection closed")); 1.1044 + } 1.1045 + Pool.prototype.destroy.call(this); 1.1046 + this.actorID = null; 1.1047 + }, 1.1048 + 1.1049 + /** 1.1050 + * @returns a promise that will resolve to the actorID this front 1.1051 + * represents. 1.1052 + */ 1.1053 + actor: function() { return promise.resolve(this.actorID) }, 1.1054 + 1.1055 + toString: function() { return "[Front for " + this.typeName + "/" + this.actorID + "]" }, 1.1056 + 1.1057 + /** 1.1058 + * Update the actor from its representation. 1.1059 + * Subclasses should override this. 1.1060 + */ 1.1061 + form: function(form) {}, 1.1062 + 1.1063 + /** 1.1064 + * Send a packet on the connection. 1.1065 + */ 1.1066 + send: function(packet) { 1.1067 + if (packet.to) { 1.1068 + this.conn._transport.send(packet); 1.1069 + } else { 1.1070 + this.actor().then(actorID => { 1.1071 + packet.to = actorID; 1.1072 + this.conn._transport.send(packet); 1.1073 + }); 1.1074 + } 1.1075 + }, 1.1076 + 1.1077 + /** 1.1078 + * Send a two-way request on the connection. 1.1079 + */ 1.1080 + request: function(packet) { 1.1081 + let deferred = promise.defer(); 1.1082 + this._requests.push(deferred); 1.1083 + this.send(packet); 1.1084 + return deferred.promise; 1.1085 + }, 1.1086 + 1.1087 + /** 1.1088 + * Handler for incoming packets from the client's actor. 1.1089 + */ 1.1090 + onPacket: function(packet) { 1.1091 + // Pick off event packets 1.1092 + if (this._clientSpec.events && this._clientSpec.events.has(packet.type)) { 1.1093 + let event = this._clientSpec.events.get(packet.type); 1.1094 + let args = event.request.read(packet, this); 1.1095 + if (event.pre) { 1.1096 + event.pre.forEach((pre) => pre.apply(this, args)); 1.1097 + } 1.1098 + events.emit.apply(null, [this, event.name].concat(args)); 1.1099 + return; 1.1100 + } 1.1101 + 1.1102 + // Remaining packets must be responses. 1.1103 + if (this._requests.length === 0) { 1.1104 + let msg = "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet); 1.1105 + let err = Error(msg); 1.1106 + console.error(err); 1.1107 + throw err; 1.1108 + } 1.1109 + 1.1110 + let deferred = this._requests.shift(); 1.1111 + if (packet.error) { 1.1112 + deferred.reject(packet.error); 1.1113 + } else { 1.1114 + deferred.resolve(packet); 1.1115 + } 1.1116 + } 1.1117 +}); 1.1118 +exports.Front = Front; 1.1119 + 1.1120 +/** 1.1121 + * A method tagged with preEvent will be called after recieving a packet 1.1122 + * for that event, and before the front emits the event. 1.1123 + */ 1.1124 +exports.preEvent = function(eventName, fn) { 1.1125 + fn._preEvent = eventName; 1.1126 + return fn; 1.1127 +} 1.1128 + 1.1129 +/** 1.1130 + * Mark a method as a custom front implementation, replacing the generated 1.1131 + * front method. 1.1132 + * 1.1133 + * @param function fn 1.1134 + * The front implementation, will be returned. 1.1135 + * @param object options 1.1136 + * Options object: 1.1137 + * impl (string): If provided, the generated front method will be 1.1138 + * stored as this property on the prototype. 1.1139 + */ 1.1140 +exports.custom = function(fn, options={}) { 1.1141 + fn._customFront = options; 1.1142 + return fn; 1.1143 +} 1.1144 + 1.1145 +function prototypeOf(obj) { 1.1146 + return typeof(obj) === "function" ? obj.prototype : obj; 1.1147 +} 1.1148 + 1.1149 +/** 1.1150 + * Process a front definition from its prototype and generate 1.1151 + * request methods. 1.1152 + */ 1.1153 +let frontProto = function(proto) { 1.1154 + let actorType = prototypeOf(proto.actorType); 1.1155 + if (proto._actorSpec) { 1.1156 + throw new Error("frontProto called twice on the same front prototype!"); 1.1157 + } 1.1158 + proto._actorSpec = actorType._actorSpec; 1.1159 + proto.typeName = actorType.typeName; 1.1160 + 1.1161 + // Generate request methods. 1.1162 + let methods = proto._actorSpec.methods; 1.1163 + methods.forEach(spec => { 1.1164 + let name = spec.name; 1.1165 + 1.1166 + // If there's already a property by this name in the front, it must 1.1167 + // be a custom front method. 1.1168 + if (name in proto) { 1.1169 + let custom = proto[spec.name]._customFront; 1.1170 + if (custom === undefined) { 1.1171 + throw Error("Existing method for " + spec.name + " not marked customFront while processing " + actorType.typeName + "."); 1.1172 + } 1.1173 + // If the user doesn't need the impl don't generate it. 1.1174 + if (!custom.impl) { 1.1175 + return; 1.1176 + } 1.1177 + name = custom.impl; 1.1178 + } 1.1179 + 1.1180 + proto[name] = function(...args) { 1.1181 + let histogram, startTime; 1.1182 + if (spec.telemetry) { 1.1183 + if (spec.oneway) { 1.1184 + // That just doesn't make sense. 1.1185 + throw Error("Telemetry specified for a oneway request"); 1.1186 + } 1.1187 + let transportType = this.conn.localTransport 1.1188 + ? "LOCAL_" 1.1189 + : "REMOTE_"; 1.1190 + let histogramId = "DEVTOOLS_DEBUGGER_RDP_" 1.1191 + + transportType + spec.telemetry + "_MS"; 1.1192 + try { 1.1193 + histogram = Services.telemetry.getHistogramById(histogramId); 1.1194 + startTime = new Date(); 1.1195 + } catch(ex) { 1.1196 + // XXX: Is this expected in xpcshell tests? 1.1197 + console.error(ex); 1.1198 + spec.telemetry = false; 1.1199 + } 1.1200 + } 1.1201 + 1.1202 + let packet = spec.request.write(args, this); 1.1203 + if (spec.oneway) { 1.1204 + // Fire-and-forget oneway packets. 1.1205 + this.send(packet); 1.1206 + return undefined; 1.1207 + } 1.1208 + 1.1209 + return this.request(packet).then(response => { 1.1210 + let ret = spec.response.read(response, this); 1.1211 + 1.1212 + if (histogram) { 1.1213 + histogram.add(+new Date - startTime); 1.1214 + } 1.1215 + 1.1216 + return ret; 1.1217 + }).then(null, promiseDone); 1.1218 + } 1.1219 + 1.1220 + // Release methods should call the destroy function on return. 1.1221 + if (spec.release) { 1.1222 + let fn = proto[name]; 1.1223 + proto[name] = function(...args) { 1.1224 + return fn.apply(this, args).then(result => { 1.1225 + this.destroy(); 1.1226 + return result; 1.1227 + }) 1.1228 + } 1.1229 + } 1.1230 + }); 1.1231 + 1.1232 + 1.1233 + // Process event specifications 1.1234 + proto._clientSpec = {}; 1.1235 + 1.1236 + let events = proto._actorSpec.events; 1.1237 + if (events) { 1.1238 + // This actor has events, scan the prototype for preEvent handlers... 1.1239 + let preHandlers = new Map(); 1.1240 + for (let name of Object.getOwnPropertyNames(proto)) { 1.1241 + let desc = Object.getOwnPropertyDescriptor(proto, name); 1.1242 + if (!desc.value) { 1.1243 + continue; 1.1244 + } 1.1245 + if (desc.value._preEvent) { 1.1246 + let preEvent = desc.value._preEvent; 1.1247 + if (!events.has(preEvent)) { 1.1248 + throw Error("preEvent for event that doesn't exist: " + preEvent); 1.1249 + } 1.1250 + let handlers = preHandlers.get(preEvent); 1.1251 + if (!handlers) { 1.1252 + handlers = []; 1.1253 + preHandlers.set(preEvent, handlers); 1.1254 + } 1.1255 + handlers.push(desc.value); 1.1256 + } 1.1257 + } 1.1258 + 1.1259 + proto._clientSpec.events = new Map(); 1.1260 + 1.1261 + for (let [name, request] of events) { 1.1262 + proto._clientSpec.events.set(request.type, { 1.1263 + name: name, 1.1264 + request: request, 1.1265 + pre: preHandlers.get(name) 1.1266 + }); 1.1267 + } 1.1268 + } 1.1269 + return proto; 1.1270 +} 1.1271 + 1.1272 +/** 1.1273 + * Create a front class for the given actor class, with the given prototype. 1.1274 + * 1.1275 + * @param ActorClass actorType 1.1276 + * The actor class you're creating a front for. 1.1277 + * @param object proto 1.1278 + * The object prototype. Must have a 'typeName' property, 1.1279 + * should have method definitions, can have event definitions. 1.1280 + */ 1.1281 +exports.FrontClass = function(actorType, proto) { 1.1282 + proto.actorType = actorType; 1.1283 + proto.extends = Front; 1.1284 + let cls = Class(frontProto(proto)); 1.1285 + registeredTypes.get(cls.prototype.typeName).frontClass = cls; 1.1286 + return cls; 1.1287 +} 1.1288 + 1.1289 + 1.1290 +exports.dumpActorSpec = function(type) { 1.1291 + let actorSpec = type.actorSpec; 1.1292 + let ret = { 1.1293 + category: "actor", 1.1294 + typeName: type.name, 1.1295 + methods: [], 1.1296 + events: {} 1.1297 + }; 1.1298 + 1.1299 + for (let method of actorSpec.methods) { 1.1300 + ret.methods.push({ 1.1301 + name: method.name, 1.1302 + release: method.release || undefined, 1.1303 + oneway: method.oneway || undefined, 1.1304 + request: method.request.describe(), 1.1305 + response: method.response.describe() 1.1306 + }); 1.1307 + } 1.1308 + 1.1309 + if (actorSpec.events) { 1.1310 + for (let [name, request] of actorSpec.events) { 1.1311 + ret.events[name] = request.describe(); 1.1312 + } 1.1313 + } 1.1314 + 1.1315 + 1.1316 + JSON.stringify(ret); 1.1317 + 1.1318 + return ret; 1.1319 +} 1.1320 + 1.1321 +exports.dumpProtocolSpec = function() { 1.1322 + let ret = { 1.1323 + types: {}, 1.1324 + }; 1.1325 + 1.1326 + for (let [name, type] of registeredTypes) { 1.1327 + // Force lazy instantiation if needed. 1.1328 + type = types.getType(name); 1.1329 + if (type.category === "dict") { 1.1330 + ret.types[name] = { 1.1331 + category: "dict", 1.1332 + typeName: name, 1.1333 + specializations: type.specializations 1.1334 + } 1.1335 + } else if (type.category === "actor") { 1.1336 + ret.types[name] = exports.dumpActorSpec(type); 1.1337 + } 1.1338 + } 1.1339 + 1.1340 + return ret; 1.1341 +}