mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1fcbd86174
In a following patch, all DevTools moz.build files will use DevToolsModules to install JS modules at a path that corresponds directly to their source tree location. Here we rewrite all require and import calls to match the new location that these files are installed to.
1478 lines
39 KiB
JavaScript
1478 lines
39 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
var { Cu, components } = require("chrome");
|
|
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
|
var Services = require("Services");
|
|
var promise = require("promise");
|
|
var {Class} = require("sdk/core/heritage");
|
|
var {EventTarget} = require("sdk/event/target");
|
|
var events = require("sdk/event/core");
|
|
var object = require("sdk/util/object");
|
|
|
|
exports.emit = events.emit;
|
|
|
|
/**
|
|
* Types: named marshallers/demarshallers.
|
|
*
|
|
* Types provide a 'write' function that takes a js representation and
|
|
* returns a protocol representation, and a "read" function that
|
|
* takes a protocol representation and returns a js representation.
|
|
*
|
|
* The read and write methods are also passed a context object that
|
|
* represent the actor or front requesting the translation.
|
|
*
|
|
* Types are referred to with a typestring. Basic types are
|
|
* registered by name using addType, and more complex types can
|
|
* be generated by adding detail to the type name.
|
|
*/
|
|
|
|
var types = Object.create(null);
|
|
exports.types = types;
|
|
|
|
var registeredTypes = types.registeredTypes = new Map();
|
|
var registeredLifetimes = types.registeredLifetimes = new Map();
|
|
|
|
/**
|
|
* Return the type object associated with a given typestring.
|
|
* If passed a type object, it will be returned unchanged.
|
|
*
|
|
* Types can be registered with addType, or can be created on
|
|
* the fly with typestrings. Examples:
|
|
*
|
|
* boolean
|
|
* threadActor
|
|
* threadActor#detail
|
|
* array:threadActor
|
|
* array:array:threadActor#detail
|
|
*
|
|
* @param [typestring|type] type
|
|
* Either a typestring naming a type or a type object.
|
|
*
|
|
* @returns a type object.
|
|
*/
|
|
types.getType = function(type) {
|
|
if (!type) {
|
|
return types.Primitive;
|
|
}
|
|
|
|
if (typeof(type) !== "string") {
|
|
return type;
|
|
}
|
|
|
|
// If already registered, we're done here.
|
|
let reg = registeredTypes.get(type);
|
|
if (reg) return reg;
|
|
|
|
// New type, see if it's a collection/lifetime type:
|
|
let sep = type.indexOf(":");
|
|
if (sep >= 0) {
|
|
let collection = type.substring(0, sep);
|
|
let subtype = types.getType(type.substring(sep + 1));
|
|
|
|
if (collection === "array") {
|
|
return types.addArrayType(subtype);
|
|
} else if (collection === "nullable") {
|
|
return types.addNullableType(subtype);
|
|
}
|
|
|
|
if (registeredLifetimes.has(collection)) {
|
|
return types.addLifetimeType(collection, subtype);
|
|
}
|
|
|
|
throw Error("Unknown collection type: " + collection);
|
|
}
|
|
|
|
// Not a collection, might be actor detail
|
|
let pieces = type.split("#", 2);
|
|
if (pieces.length > 1) {
|
|
return types.addActorDetail(type, pieces[0], pieces[1]);
|
|
}
|
|
|
|
// Might be a lazily-loaded type
|
|
if (type === "longstring") {
|
|
require("devtools/server/actors/string");
|
|
return registeredTypes.get("longstring");
|
|
}
|
|
|
|
throw Error("Unknown type: " + type);
|
|
}
|
|
|
|
/**
|
|
* Don't allow undefined when writing primitive types to packets. If
|
|
* you want to allow undefined, use a nullable type.
|
|
*/
|
|
function identityWrite(v) {
|
|
if (v === undefined) {
|
|
throw Error("undefined passed where a value is required");
|
|
}
|
|
// This has to handle iterator->array conversion because arrays of
|
|
// primitive types pass through here.
|
|
if (v && typeof (v) === "object" && Symbol.iterator in v) {
|
|
return [...v];
|
|
}
|
|
return v;
|
|
}
|
|
|
|
/**
|
|
* Add a type to the type system.
|
|
*
|
|
* When registering a type, you can provide `read` and `write` methods.
|
|
*
|
|
* The `read` method will be passed a JS object value from the JSON
|
|
* packet and must return a native representation. The `write` method will
|
|
* be passed a native representation and should provide a JSONable value.
|
|
*
|
|
* These methods will both be passed a context. The context is the object
|
|
* performing or servicing the request - on the server side it will be
|
|
* an Actor, on the client side it will be a Front.
|
|
*
|
|
* @param typestring name
|
|
* Name to register
|
|
* @param object typeObject
|
|
* An object whose properties will be stored in the type, including
|
|
* the `read` and `write` methods.
|
|
* @param object options
|
|
* Can specify `thawed` to prevent the type from being frozen.
|
|
*
|
|
* @returns a type object that can be used in protocol definitions.
|
|
*/
|
|
types.addType = function(name, typeObject={}, options={}) {
|
|
if (registeredTypes.has(name)) {
|
|
throw Error("Type '" + name + "' already exists.");
|
|
}
|
|
|
|
let type = object.merge({
|
|
toString() { return "[protocol type:" + name + "]"},
|
|
name: name,
|
|
primitive: !(typeObject.read || typeObject.write),
|
|
read: identityWrite,
|
|
write: identityWrite
|
|
}, typeObject);
|
|
|
|
registeredTypes.set(name, type);
|
|
|
|
return type;
|
|
};
|
|
|
|
/**
|
|
* Remove a type previously registered with the system.
|
|
* Primarily useful for types registered by addons.
|
|
*/
|
|
types.removeType = function(name) {
|
|
// This type may still be referenced by other types, make sure
|
|
// those references don't work.
|
|
let type = registeredTypes.get(name);
|
|
|
|
type.name = "DEFUNCT:" + name;
|
|
type.category = "defunct";
|
|
type.primitive = false;
|
|
type.read = type.write = function() { throw new Error("Using defunct type: " + name); };
|
|
|
|
registeredTypes.delete(name);
|
|
}
|
|
|
|
/**
|
|
* Add an array type to the type system.
|
|
*
|
|
* getType() will call this function if provided an "array:<type>"
|
|
* typestring.
|
|
*
|
|
* @param type subtype
|
|
* The subtype to be held by the array.
|
|
*/
|
|
types.addArrayType = function(subtype) {
|
|
subtype = types.getType(subtype);
|
|
|
|
let name = "array:" + subtype.name;
|
|
|
|
// Arrays of primitive types are primitive types themselves.
|
|
if (subtype.primitive) {
|
|
return types.addType(name);
|
|
}
|
|
return types.addType(name, {
|
|
category: "array",
|
|
read: (v, ctx) => [...v].map(i => subtype.read(i, ctx)),
|
|
write: (v, ctx) => [...v].map(i => subtype.write(i, ctx))
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Add a dict type to the type system. This allows you to serialize
|
|
* a JS object that contains non-primitive subtypes.
|
|
*
|
|
* Properties of the value that aren't included in the specializations
|
|
* will be serialized as primitive values.
|
|
*
|
|
* @param object specializations
|
|
* A dict of property names => type
|
|
*/
|
|
types.addDictType = function(name, specializations) {
|
|
return types.addType(name, {
|
|
category: "dict",
|
|
specializations: specializations,
|
|
read: (v, ctx) => {
|
|
let ret = {};
|
|
for (let prop in v) {
|
|
if (prop in specializations) {
|
|
ret[prop] = types.getType(specializations[prop]).read(v[prop], ctx);
|
|
} else {
|
|
ret[prop] = v[prop];
|
|
}
|
|
}
|
|
return ret;
|
|
},
|
|
|
|
write: (v, ctx) => {
|
|
let ret = {};
|
|
for (let prop in v) {
|
|
if (prop in specializations) {
|
|
ret[prop] = types.getType(specializations[prop]).write(v[prop], ctx);
|
|
} else {
|
|
ret[prop] = v[prop];
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Register an actor type with the type system.
|
|
*
|
|
* Types are marshalled differently when communicating server->client
|
|
* than they are when communicating client->server. The server needs
|
|
* to provide useful information to the client, so uses the actor's
|
|
* `form` method to get a json representation of the actor. When
|
|
* making a request from the client we only need the actor ID string.
|
|
*
|
|
* This function can be called before the associated actor has been
|
|
* constructed, but the read and write methods won't work until
|
|
* the associated addActorImpl or addActorFront methods have been
|
|
* called during actor/front construction.
|
|
*
|
|
* @param string name
|
|
* The typestring to register.
|
|
*/
|
|
types.addActorType = function(name) {
|
|
let type = types.addType(name, {
|
|
_actor: true,
|
|
category: "actor",
|
|
read: (v, ctx, detail) => {
|
|
// If we're reading a request on the server side, just
|
|
// find the actor registered with this actorID.
|
|
if (ctx instanceof Actor) {
|
|
return ctx.conn.getActor(v);
|
|
}
|
|
|
|
// Reading a response on the client side, check for an
|
|
// existing front on the connection, and create the front
|
|
// if it isn't found.
|
|
let actorID = typeof(v) === "string" ? v : v.actor;
|
|
let front = ctx.conn.getActor(actorID);
|
|
if (!front) {
|
|
front = new type.frontClass(ctx.conn);
|
|
front.actorID = actorID;
|
|
ctx.marshallPool().manage(front);
|
|
}
|
|
|
|
v = type.formType(detail).read(v, front, detail);
|
|
front.form(v, detail, ctx);
|
|
|
|
return front;
|
|
},
|
|
write: (v, ctx, detail) => {
|
|
// If returning a response from the server side, make sure
|
|
// the actor is added to a parent object and return its form.
|
|
if (v instanceof Actor) {
|
|
if (!v.actorID) {
|
|
ctx.marshallPool().manage(v);
|
|
}
|
|
return type.formType(detail).write(v.form(detail), ctx, detail);
|
|
}
|
|
|
|
// Writing a request from the client side, just send the actor id.
|
|
return v.actorID;
|
|
},
|
|
formType: (detail) => {
|
|
if (!("formType" in type.actorSpec)) {
|
|
return types.Primitive;
|
|
}
|
|
|
|
let formAttr = "formType";
|
|
if (detail) {
|
|
formAttr += "#" + detail;
|
|
}
|
|
|
|
if (!(formAttr in type.actorSpec)) {
|
|
throw new Error("No type defined for " + formAttr);
|
|
}
|
|
|
|
return type.actorSpec[formAttr];
|
|
}
|
|
});
|
|
return type;
|
|
}
|
|
|
|
types.addNullableType = function(subtype) {
|
|
subtype = types.getType(subtype);
|
|
return types.addType("nullable:" + subtype.name, {
|
|
category: "nullable",
|
|
read: (value, ctx) => {
|
|
if (value == null) {
|
|
return value;
|
|
}
|
|
return subtype.read(value, ctx);
|
|
},
|
|
write: (value, ctx) => {
|
|
if (value == null) {
|
|
return value;
|
|
}
|
|
return subtype.write(value, ctx);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Register an actor detail type. This is just like an actor type, but
|
|
* will pass a detail hint to the actor's form method during serialization/
|
|
* deserialization.
|
|
*
|
|
* This is called by getType() when passed an 'actorType#detail' string.
|
|
*
|
|
* @param string name
|
|
* The typestring to register this type as.
|
|
* @param type actorType
|
|
* The actor type you'll be detailing.
|
|
* @param string detail
|
|
* The detail to pass.
|
|
*/
|
|
types.addActorDetail = function(name, actorType, detail) {
|
|
actorType = types.getType(actorType);
|
|
if (!actorType._actor) {
|
|
throw Error("Details only apply to actor types, tried to add detail '" + detail + "'' to " + actorType.name + "\n");
|
|
}
|
|
return types.addType(name, {
|
|
_actor: true,
|
|
category: "detail",
|
|
read: (v, ctx) => actorType.read(v, ctx, detail),
|
|
write: (v, ctx) => actorType.write(v, ctx, detail)
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Register an actor lifetime. This lets the type system find a parent
|
|
* actor that differs from the actor fulfilling the request.
|
|
*
|
|
* @param string name
|
|
* The lifetime name to use in typestrings.
|
|
* @param string prop
|
|
* The property of the actor that holds the parent that should be used.
|
|
*/
|
|
types.addLifetime = function(name, prop) {
|
|
if (registeredLifetimes.has(name)) {
|
|
throw Error("Lifetime '" + name + "' already registered.");
|
|
}
|
|
registeredLifetimes.set(name, prop);
|
|
}
|
|
|
|
/**
|
|
* Remove a previously-registered lifetime. Useful for lifetimes registered
|
|
* in addons.
|
|
*/
|
|
types.removeLifetime = function(name) {
|
|
registeredLifetimes.delete(name);
|
|
}
|
|
|
|
/**
|
|
* Register a lifetime type. This creates an actor type tied to the given
|
|
* lifetime.
|
|
*
|
|
* This is called by getType() when passed a '<lifetimeType>:<actorType>'
|
|
* typestring.
|
|
*
|
|
* @param string lifetime
|
|
* A lifetime string previously regisered with addLifetime()
|
|
* @param type subtype
|
|
* An actor type
|
|
*/
|
|
types.addLifetimeType = function(lifetime, subtype) {
|
|
subtype = types.getType(subtype);
|
|
if (!subtype._actor) {
|
|
throw Error("Lifetimes only apply to actor types, tried to apply lifetime '" + lifetime + "'' to " + subtype.name);
|
|
}
|
|
let prop = registeredLifetimes.get(lifetime);
|
|
return types.addType(lifetime + ":" + subtype.name, {
|
|
category: "lifetime",
|
|
read: (value, ctx) => subtype.read(value, ctx[prop]),
|
|
write: (value, ctx) => subtype.write(value, ctx[prop])
|
|
})
|
|
}
|
|
|
|
// Add a few named primitive types.
|
|
types.Primitive = types.addType("primitive");
|
|
types.String = types.addType("string");
|
|
types.Number = types.addType("number");
|
|
types.Boolean = types.addType("boolean");
|
|
types.JSON = types.addType("json");
|
|
|
|
/**
|
|
* Request/Response templates and generation
|
|
*
|
|
* Request packets are specified as json templates with
|
|
* Arg and Option placeholders where arguments should be
|
|
* placed.
|
|
*
|
|
* Reponse packets are also specified as json templates,
|
|
* with a RetVal placeholder where the return value should be
|
|
* placed.
|
|
*/
|
|
|
|
/**
|
|
* Placeholder for simple arguments.
|
|
*
|
|
* @param number index
|
|
* The argument index to place at this position.
|
|
* @param type type
|
|
* The argument should be marshalled as this type.
|
|
* @constructor
|
|
*/
|
|
var Arg = Class({
|
|
initialize: function(index, type) {
|
|
this.index = index;
|
|
this.type = types.getType(type);
|
|
},
|
|
|
|
write: function(arg, ctx) {
|
|
return this.type.write(arg, ctx);
|
|
},
|
|
|
|
read: function(v, ctx, outArgs) {
|
|
outArgs[this.index] = this.type.read(v, ctx);
|
|
},
|
|
|
|
describe: function() {
|
|
return {
|
|
_arg: this.index,
|
|
type: this.type.name,
|
|
}
|
|
}
|
|
});
|
|
exports.Arg = Arg;
|
|
|
|
/**
|
|
* Placeholder for an options argument value that should be hoisted
|
|
* into the packet.
|
|
*
|
|
* If provided in a method specification:
|
|
*
|
|
* { optionArg: Option(1)}
|
|
*
|
|
* Then arguments[1].optionArg will be placed in the packet in this
|
|
* value's place.
|
|
*
|
|
* @param number index
|
|
* The argument index of the options value.
|
|
* @param type type
|
|
* The argument should be marshalled as this type.
|
|
* @constructor
|
|
*/
|
|
var Option = Class({
|
|
extends: Arg,
|
|
initialize: function(index, type) {
|
|
Arg.prototype.initialize.call(this, index, type)
|
|
},
|
|
|
|
write: function(arg, ctx, name) {
|
|
// Ignore if arg is undefined or null; allow other falsy values
|
|
if (arg == undefined || arg[name] == undefined) {
|
|
return undefined;
|
|
}
|
|
let v = arg[name];
|
|
return this.type.write(v, ctx);
|
|
},
|
|
read: function(v, ctx, outArgs, name) {
|
|
if (outArgs[this.index] === undefined) {
|
|
outArgs[this.index] = {};
|
|
}
|
|
if (v === undefined) {
|
|
return;
|
|
}
|
|
outArgs[this.index][name] = this.type.read(v, ctx);
|
|
},
|
|
|
|
describe: function() {
|
|
return {
|
|
_option: this.index,
|
|
type: this.type.name,
|
|
}
|
|
}
|
|
});
|
|
|
|
exports.Option = Option;
|
|
|
|
/**
|
|
* Placeholder for return values in a response template.
|
|
*
|
|
* @param type type
|
|
* The return value should be marshalled as this type.
|
|
*/
|
|
var RetVal = Class({
|
|
initialize: function(type) {
|
|
this.type = types.getType(type);
|
|
},
|
|
|
|
write: function(v, ctx) {
|
|
return this.type.write(v, ctx);
|
|
},
|
|
|
|
read: function(v, ctx) {
|
|
return this.type.read(v, ctx);
|
|
},
|
|
|
|
describe: function() {
|
|
return {
|
|
_retval: this.type.name
|
|
}
|
|
}
|
|
});
|
|
|
|
exports.RetVal = RetVal;
|
|
|
|
/* Template handling functions */
|
|
|
|
/**
|
|
* Get the value at a given path, or undefined if not found.
|
|
*/
|
|
function getPath(obj, path) {
|
|
for (let name of path) {
|
|
if (!(name in obj)) {
|
|
return undefined;
|
|
}
|
|
obj = obj[name];
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
* Find Placeholders in the template and save them along with their
|
|
* paths.
|
|
*/
|
|
function findPlaceholders(template, constructor, path=[], placeholders=[]) {
|
|
if (!template || typeof(template) != "object") {
|
|
return placeholders;
|
|
}
|
|
|
|
if (template instanceof constructor) {
|
|
placeholders.push({ placeholder: template, path: [...path] });
|
|
return placeholders;
|
|
}
|
|
|
|
for (let name in template) {
|
|
path.push(name);
|
|
findPlaceholders(template[name], constructor, path, placeholders);
|
|
path.pop();
|
|
}
|
|
|
|
return placeholders;
|
|
}
|
|
|
|
|
|
function describeTemplate(template) {
|
|
return JSON.parse(JSON.stringify(template, (key, value) => {
|
|
if (value.describe) {
|
|
return value.describe();
|
|
}
|
|
return value;
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Manages a request template.
|
|
*
|
|
* @param object template
|
|
* The request template.
|
|
* @construcor
|
|
*/
|
|
var Request = Class({
|
|
initialize: function(template={}) {
|
|
this.type = template.type;
|
|
this.template = template;
|
|
this.args = findPlaceholders(template, Arg);
|
|
},
|
|
|
|
/**
|
|
* Write a request.
|
|
*
|
|
* @param array fnArgs
|
|
* The function arguments to place in the request.
|
|
* @param object ctx
|
|
* The object making the request.
|
|
* @returns a request packet.
|
|
*/
|
|
write: function(fnArgs, ctx) {
|
|
let str = JSON.stringify(this.template, (key, value) => {
|
|
if (value instanceof Arg) {
|
|
return value.write(value.index in fnArgs ? fnArgs[value.index] : undefined,
|
|
ctx, key);
|
|
}
|
|
return value;
|
|
});
|
|
return JSON.parse(str);
|
|
},
|
|
|
|
/**
|
|
* Read a request.
|
|
*
|
|
* @param object packet
|
|
* The request packet.
|
|
* @param object ctx
|
|
* The object making the request.
|
|
* @returns an arguments array
|
|
*/
|
|
read: function(packet, ctx) {
|
|
let fnArgs = [];
|
|
for (let templateArg of this.args) {
|
|
let arg = templateArg.placeholder;
|
|
let path = templateArg.path;
|
|
let name = path[path.length - 1];
|
|
arg.read(getPath(packet, path), ctx, fnArgs, name);
|
|
}
|
|
return fnArgs;
|
|
},
|
|
|
|
describe: function() { return describeTemplate(this.template); }
|
|
});
|
|
|
|
/**
|
|
* Manages a response template.
|
|
*
|
|
* @param object template
|
|
* The response template.
|
|
* @construcor
|
|
*/
|
|
var Response = Class({
|
|
initialize: function(template={}) {
|
|
this.template = template;
|
|
let placeholders = findPlaceholders(template, RetVal);
|
|
if (placeholders.length > 1) {
|
|
throw Error("More than one RetVal specified in response");
|
|
}
|
|
let placeholder = placeholders.shift();
|
|
if (placeholder) {
|
|
this.retVal = placeholder.placeholder;
|
|
this.path = placeholder.path;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Write a response for the given return value.
|
|
*
|
|
* @param val ret
|
|
* The return value.
|
|
* @param object ctx
|
|
* The object writing the response.
|
|
*/
|
|
write: function(ret, ctx) {
|
|
return JSON.parse(JSON.stringify(this.template, function(key, value) {
|
|
if (value instanceof RetVal) {
|
|
return value.write(ret, ctx);
|
|
}
|
|
return value;
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Read a return value from the given response.
|
|
*
|
|
* @param object packet
|
|
* The response packet.
|
|
* @param object ctx
|
|
* The object reading the response.
|
|
*/
|
|
read: function(packet, ctx) {
|
|
if (!this.retVal) {
|
|
return undefined;
|
|
}
|
|
let v = getPath(packet, this.path);
|
|
return this.retVal.read(v, ctx);
|
|
},
|
|
|
|
describe: function() { return describeTemplate(this.template); }
|
|
});
|
|
|
|
/**
|
|
* Actor and Front implementations
|
|
*/
|
|
|
|
/**
|
|
* A protocol object that can manage the lifetime of other protocol
|
|
* objects.
|
|
*/
|
|
var Pool = Class({
|
|
extends: EventTarget,
|
|
|
|
/**
|
|
* Pools are used on both sides of the connection to help coordinate
|
|
* lifetimes.
|
|
*
|
|
* @param optional conn
|
|
* Either a DebuggerServerConnection or a DebuggerClient. Must have
|
|
* addActorPool, removeActorPool, and poolFor.
|
|
* conn can be null if the subclass provides a conn property.
|
|
* @constructor
|
|
*/
|
|
initialize: function(conn) {
|
|
if (conn) {
|
|
this.conn = conn;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Return the parent pool for this client.
|
|
*/
|
|
parent: function() { return this.conn.poolFor(this.actorID) },
|
|
|
|
/**
|
|
* Override this if you want actors returned by this actor
|
|
* to belong to a different actor by default.
|
|
*/
|
|
marshallPool: function() { return this; },
|
|
|
|
/**
|
|
* Pool is the base class for all actors, even leaf nodes.
|
|
* If the child map is actually referenced, go ahead and create
|
|
* the stuff needed by the pool.
|
|
*/
|
|
__poolMap: null,
|
|
get _poolMap() {
|
|
if (this.__poolMap) return this.__poolMap;
|
|
this.__poolMap = new Map();
|
|
this.conn.addActorPool(this);
|
|
return this.__poolMap;
|
|
},
|
|
|
|
/**
|
|
* Add an actor as a child of this pool.
|
|
*/
|
|
manage: function(actor) {
|
|
if (!actor.actorID) {
|
|
actor.actorID = this.conn.allocID(actor.actorPrefix || actor.typeName);
|
|
}
|
|
|
|
this._poolMap.set(actor.actorID, actor);
|
|
return actor;
|
|
},
|
|
|
|
/**
|
|
* Remove an actor as a child of this pool.
|
|
*/
|
|
unmanage: function(actor) {
|
|
this.__poolMap && this.__poolMap.delete(actor.actorID);
|
|
},
|
|
|
|
// true if the given actor ID exists in the pool.
|
|
has: function(actorID) {
|
|
return this.__poolMap && this._poolMap.has(actorID);
|
|
},
|
|
|
|
// The actor for a given actor id stored in this pool
|
|
actor: function(actorID) {
|
|
return this.__poolMap ? this._poolMap.get(actorID) : null;
|
|
},
|
|
|
|
// Same as actor, should update debugger connection to use 'actor'
|
|
// and then remove this.
|
|
get: function(actorID) {
|
|
return this.__poolMap ? this._poolMap.get(actorID) : null;
|
|
},
|
|
|
|
// True if this pool has no children.
|
|
isEmpty: function() {
|
|
return !this.__poolMap || this._poolMap.size == 0;
|
|
},
|
|
|
|
/**
|
|
* Destroy this item, removing it from a parent if it has one,
|
|
* and destroying all children if necessary.
|
|
*/
|
|
destroy: function() {
|
|
let parent = this.parent();
|
|
if (parent) {
|
|
parent.unmanage(this);
|
|
}
|
|
if (!this.__poolMap) {
|
|
return;
|
|
}
|
|
for (let actor of this.__poolMap.values()) {
|
|
// Self-owned actors are ok, but don't need destroying twice.
|
|
if (actor === this) {
|
|
continue;
|
|
}
|
|
let destroy = actor.destroy;
|
|
if (destroy) {
|
|
// Disconnect destroy while we're destroying in case of (misbehaving)
|
|
// circular ownership.
|
|
actor.destroy = null;
|
|
destroy.call(actor);
|
|
actor.destroy = destroy;
|
|
}
|
|
};
|
|
this.conn.removeActorPool(this, true);
|
|
this.__poolMap.clear();
|
|
this.__poolMap = null;
|
|
},
|
|
|
|
/**
|
|
* For getting along with the debugger server pools, should be removable
|
|
* eventually.
|
|
*/
|
|
cleanup: function() {
|
|
this.destroy();
|
|
}
|
|
});
|
|
exports.Pool = Pool;
|
|
|
|
/**
|
|
* An actor in the actor tree.
|
|
*/
|
|
var Actor = Class({
|
|
extends: Pool,
|
|
|
|
// Will contain the actor's ID
|
|
actorID: null,
|
|
|
|
/**
|
|
* Initialize an actor.
|
|
*
|
|
* @param optional conn
|
|
* Either a DebuggerServerConnection or a DebuggerClient. Must have
|
|
* addActorPool, removeActorPool, and poolFor.
|
|
* conn can be null if the subclass provides a conn property.
|
|
* @constructor
|
|
*/
|
|
initialize: function(conn) {
|
|
Pool.prototype.initialize.call(this, conn);
|
|
|
|
// Forward events to the connection.
|
|
if (this._actorSpec && this._actorSpec.events) {
|
|
for (let key of this._actorSpec.events.keys()) {
|
|
let name = key;
|
|
let sendEvent = this._sendEvent.bind(this, name)
|
|
this.on(name, (...args) => {
|
|
sendEvent.apply(null, args);
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
toString: function() { return "[Actor " + this.typeName + "/" + this.actorID + "]" },
|
|
|
|
_sendEvent: function(name, ...args) {
|
|
if (!this._actorSpec.events.has(name)) {
|
|
// It's ok to emit events that don't go over the wire.
|
|
return;
|
|
}
|
|
let request = this._actorSpec.events.get(name);
|
|
let packet;
|
|
try {
|
|
packet = request.write(args, this);
|
|
} catch(ex) {
|
|
console.error("Error sending event: " + name);
|
|
throw ex;
|
|
}
|
|
packet.from = packet.from || this.actorID;
|
|
this.conn.send(packet);
|
|
},
|
|
|
|
destroy: function() {
|
|
Pool.prototype.destroy.call(this);
|
|
this.actorID = null;
|
|
},
|
|
|
|
/**
|
|
* Override this method in subclasses to serialize the actor.
|
|
* @param [optional] string hint
|
|
* Optional string to customize the form.
|
|
* @returns A jsonable object.
|
|
*/
|
|
form: function(hint) {
|
|
return { actor: this.actorID }
|
|
},
|
|
|
|
writeError: function(err) {
|
|
console.error(err);
|
|
if (err.stack) {
|
|
dump(err.stack);
|
|
}
|
|
this.conn.send({
|
|
from: this.actorID,
|
|
error: "unknownError",
|
|
message: err.toString()
|
|
});
|
|
},
|
|
|
|
_queueResponse: function(create) {
|
|
let pending = this._pendingResponse || promise.resolve(null);
|
|
let response = create(pending);
|
|
this._pendingResponse = response;
|
|
}
|
|
});
|
|
exports.Actor = Actor;
|
|
|
|
/**
|
|
* Tags a prtotype method as an actor method implementation.
|
|
*
|
|
* @param function fn
|
|
* The implementation function, will be returned.
|
|
* @param spec
|
|
* The method specification, with the following (optional) properties:
|
|
* request (object): a request template.
|
|
* response (object): a response template.
|
|
* oneway (bool): 'true' if no response should be sent.
|
|
* telemetry (string): Telemetry probe ID for measuring completion time.
|
|
*/
|
|
exports.method = function(fn, spec={}) {
|
|
fn._methodSpec = Object.freeze(spec);
|
|
if (spec.request) Object.freeze(spec.request);
|
|
if (spec.response) Object.freeze(spec.response);
|
|
return fn;
|
|
}
|
|
|
|
/**
|
|
* Process an actor definition from its prototype and generate
|
|
* request handlers.
|
|
*/
|
|
var actorProto = function(actorProto) {
|
|
if (actorProto._actorSpec) {
|
|
throw new Error("actorProto called twice on the same actor prototype!");
|
|
}
|
|
|
|
let protoSpec = {
|
|
methods: [],
|
|
};
|
|
|
|
// Find method and form specifications attached to prototype properties.
|
|
for (let name of Object.getOwnPropertyNames(actorProto)) {
|
|
let desc = Object.getOwnPropertyDescriptor(actorProto, name);
|
|
if (!desc.value) {
|
|
continue;
|
|
}
|
|
|
|
if (name.startsWith("formType")) {
|
|
if (typeof(desc.value) === "string") {
|
|
protoSpec[name] = types.getType(desc.value);
|
|
} else if (desc.value.name && registeredTypes.has(desc.value.name)) {
|
|
protoSpec[name] = desc.value;
|
|
} else {
|
|
// Shorthand for a newly-registered DictType.
|
|
protoSpec[name] = types.addDictType(actorProto.typeName + "__" + name, desc.value);
|
|
}
|
|
}
|
|
|
|
if (desc.value._methodSpec) {
|
|
let frozenSpec = desc.value._methodSpec;
|
|
let spec = {};
|
|
spec.name = frozenSpec.name || name;
|
|
spec.request = Request(object.merge({type: spec.name}, frozenSpec.request || undefined));
|
|
spec.response = Response(frozenSpec.response || undefined);
|
|
spec.telemetry = frozenSpec.telemetry;
|
|
spec.release = frozenSpec.release;
|
|
spec.oneway = frozenSpec.oneway;
|
|
|
|
protoSpec.methods.push(spec);
|
|
}
|
|
}
|
|
|
|
// Find event specifications
|
|
if (actorProto.events) {
|
|
protoSpec.events = new Map();
|
|
for (let name in actorProto.events) {
|
|
let eventRequest = actorProto.events[name];
|
|
Object.freeze(eventRequest);
|
|
protoSpec.events.set(name, Request(object.merge({type: name}, eventRequest)));
|
|
}
|
|
}
|
|
|
|
// Generate request handlers for each method definition
|
|
actorProto.requestTypes = Object.create(null);
|
|
protoSpec.methods.forEach(spec => {
|
|
let handler = function(packet, conn) {
|
|
try {
|
|
let args;
|
|
try {
|
|
args = spec.request.read(packet, this);
|
|
} catch(ex) {
|
|
console.error("Error reading request: " + packet.type);
|
|
throw ex;
|
|
}
|
|
|
|
let ret = this[spec.name].apply(this, args);
|
|
|
|
let sendReturn = (ret) => {
|
|
if (spec.oneway) {
|
|
// No need to send a response.
|
|
return;
|
|
}
|
|
|
|
let response;
|
|
try {
|
|
response = spec.response.write(ret, this);
|
|
} catch(ex) {
|
|
console.error("Error writing response to: " + spec.name);
|
|
throw ex;
|
|
}
|
|
response.from = this.actorID;
|
|
// If spec.release has been specified, destroy the object.
|
|
if (spec.release) {
|
|
try {
|
|
this.destroy();
|
|
} catch(e) {
|
|
this.writeError(e);
|
|
return;
|
|
}
|
|
}
|
|
|
|
conn.send(response);
|
|
};
|
|
|
|
this._queueResponse(p => {
|
|
return p
|
|
.then(() => ret)
|
|
.then(sendReturn)
|
|
.then(null, this.writeError.bind(this));
|
|
})
|
|
} catch(e) {
|
|
this._queueResponse(p => {
|
|
return p.then(() => this.writeError(e));
|
|
});
|
|
}
|
|
};
|
|
|
|
actorProto.requestTypes[spec.request.type] = handler;
|
|
});
|
|
|
|
actorProto._actorSpec = protoSpec;
|
|
return actorProto;
|
|
}
|
|
|
|
/**
|
|
* Create an actor class for the given actor prototype.
|
|
*
|
|
* @param object proto
|
|
* The object prototype. Must have a 'typeName' property,
|
|
* should have method definitions, can have event definitions.
|
|
*/
|
|
exports.ActorClass = function(proto) {
|
|
if (!proto.typeName) {
|
|
throw Error("Actor prototype must have a typeName member.");
|
|
}
|
|
proto.extends = Actor;
|
|
if (!registeredTypes.has(proto.typeName)) {
|
|
types.addActorType(proto.typeName);
|
|
}
|
|
let cls = Class(actorProto(proto));
|
|
|
|
registeredTypes.get(proto.typeName).actorSpec = proto._actorSpec;
|
|
return cls;
|
|
};
|
|
|
|
/**
|
|
* Base class for client-side actor fronts.
|
|
*/
|
|
var Front = Class({
|
|
extends: Pool,
|
|
|
|
actorID: null,
|
|
|
|
/**
|
|
* The base class for client-side actor fronts.
|
|
*
|
|
* @param optional conn
|
|
* Either a DebuggerServerConnection or a DebuggerClient. Must have
|
|
* addActorPool, removeActorPool, and poolFor.
|
|
* conn can be null if the subclass provides a conn property.
|
|
* @param optional form
|
|
* The json form provided by the server.
|
|
* @constructor
|
|
*/
|
|
initialize: function(conn=null, form=null, detail=null, context=null) {
|
|
Pool.prototype.initialize.call(this, conn);
|
|
this._requests = [];
|
|
|
|
// protocol.js no longer uses this data in the constructor, only external
|
|
// uses do. External usage of manually-constructed fronts will be
|
|
// drastically reduced if we convert the root and tab actors to
|
|
// protocol.js, in which case this can probably go away.
|
|
if (form) {
|
|
this.actorID = form.actor;
|
|
form = types.getType(this.typeName).formType(detail).read(form, this, detail);
|
|
this.form(form, detail, context);
|
|
}
|
|
},
|
|
|
|
destroy: function() {
|
|
// Reject all outstanding requests, they won't make sense after
|
|
// the front is destroyed.
|
|
while (this._requests && this._requests.length > 0) {
|
|
let { deferred, to, type, stack } = this._requests.shift();
|
|
let msg = "Connection closed, pending request to " + to +
|
|
", type " + type + " failed" +
|
|
"\n\nRequest stack:\n" + stack.formattedStack;
|
|
deferred.reject(new Error(msg));
|
|
}
|
|
Pool.prototype.destroy.call(this);
|
|
this.actorID = null;
|
|
},
|
|
|
|
manage: function(front) {
|
|
if (!front.actorID) {
|
|
throw new Error("Can't manage front without an actor ID.\n" +
|
|
"Ensure server supports " + front.typeName + ".");
|
|
}
|
|
return Pool.prototype.manage.call(this, front);
|
|
},
|
|
|
|
/**
|
|
* @returns a promise that will resolve to the actorID this front
|
|
* represents.
|
|
*/
|
|
actor: function() { return promise.resolve(this.actorID) },
|
|
|
|
toString: function() { return "[Front for " + this.typeName + "/" + this.actorID + "]" },
|
|
|
|
/**
|
|
* Update the actor from its representation.
|
|
* Subclasses should override this.
|
|
*/
|
|
form: function(form) {},
|
|
|
|
/**
|
|
* Send a packet on the connection.
|
|
*/
|
|
send: function(packet) {
|
|
if (packet.to) {
|
|
this.conn._transport.send(packet);
|
|
} else {
|
|
this.actor().then(actorID => {
|
|
packet.to = actorID;
|
|
this.conn._transport.send(packet);
|
|
}).then(null, e => DevToolsUtils.reportException("Front.prototype.send", e));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Send a two-way request on the connection.
|
|
*/
|
|
request: function(packet) {
|
|
let deferred = promise.defer();
|
|
// Save packet basics for debugging
|
|
let { to, type } = packet;
|
|
this._requests.push({
|
|
deferred,
|
|
to: to || this.actorID,
|
|
type,
|
|
stack: components.stack,
|
|
});
|
|
this.send(packet);
|
|
return deferred.promise;
|
|
},
|
|
|
|
/**
|
|
* Handler for incoming packets from the client's actor.
|
|
*/
|
|
onPacket: function(packet) {
|
|
// Pick off event packets
|
|
let type = packet.type || undefined;
|
|
if (this._clientSpec.events && this._clientSpec.events.has(type)) {
|
|
let event = this._clientSpec.events.get(packet.type);
|
|
let args;
|
|
try {
|
|
args = event.request.read(packet, this);
|
|
} catch(ex) {
|
|
console.error("Error reading event: " + packet.type);
|
|
console.exception(ex);
|
|
throw ex;
|
|
}
|
|
if (event.pre) {
|
|
let results = event.pre.map(pre => pre.apply(this, args));
|
|
|
|
// Check to see if any of the preEvents returned a promise -- if so,
|
|
// wait for their resolution before emitting. Otherwise, emit synchronously.
|
|
if (results.some(result => result && typeof result.then === "function")) {
|
|
promise.all(results).then(() => events.emit.apply(null, [this, event.name].concat(args)));
|
|
return;
|
|
}
|
|
}
|
|
|
|
events.emit.apply(null, [this, event.name].concat(args));
|
|
return;
|
|
}
|
|
|
|
// Remaining packets must be responses.
|
|
if (this._requests.length === 0) {
|
|
let msg = "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
|
|
let err = Error(msg);
|
|
console.error(err);
|
|
throw err;
|
|
}
|
|
|
|
let { deferred, stack } = this._requests.shift();
|
|
Cu.callFunctionWithAsyncStack(() => {
|
|
if (packet.error) {
|
|
// "Protocol error" is here to avoid TBPL heuristics. See also
|
|
// https://mxr.mozilla.org/webtools-central/source/tbpl/php/inc/GeneralErrorFilter.php
|
|
let message;
|
|
if (packet.error && packet.message) {
|
|
message = "Protocol error (" + packet.error + "): " + packet.message;
|
|
} else {
|
|
message = packet.error;
|
|
}
|
|
deferred.reject(message);
|
|
} else {
|
|
deferred.resolve(packet);
|
|
}
|
|
}, stack, "DevTools RDP");
|
|
}
|
|
});
|
|
exports.Front = Front;
|
|
|
|
/**
|
|
* A method tagged with preEvent will be called after recieving a packet
|
|
* for that event, and before the front emits the event.
|
|
*/
|
|
exports.preEvent = function(eventName, fn) {
|
|
fn._preEvent = eventName;
|
|
return fn;
|
|
}
|
|
|
|
/**
|
|
* Mark a method as a custom front implementation, replacing the generated
|
|
* front method.
|
|
*
|
|
* @param function fn
|
|
* The front implementation, will be returned.
|
|
* @param object options
|
|
* Options object:
|
|
* impl (string): If provided, the generated front method will be
|
|
* stored as this property on the prototype.
|
|
*/
|
|
exports.custom = function(fn, options={}) {
|
|
fn._customFront = options;
|
|
return fn;
|
|
}
|
|
|
|
function prototypeOf(obj) {
|
|
return typeof(obj) === "function" ? obj.prototype : obj;
|
|
}
|
|
|
|
/**
|
|
* Process a front definition from its prototype and generate
|
|
* request methods.
|
|
*/
|
|
var frontProto = function(proto) {
|
|
let actorType = prototypeOf(proto.actorType);
|
|
if (proto._actorSpec) {
|
|
throw new Error("frontProto called twice on the same front prototype!");
|
|
}
|
|
proto._actorSpec = actorType._actorSpec;
|
|
proto.typeName = actorType.typeName;
|
|
|
|
// Generate request methods.
|
|
let methods = proto._actorSpec.methods;
|
|
methods.forEach(spec => {
|
|
let name = spec.name;
|
|
|
|
// If there's already a property by this name in the front, it must
|
|
// be a custom front method.
|
|
if (name in proto) {
|
|
let custom = proto[spec.name]._customFront;
|
|
if (custom === undefined) {
|
|
throw Error("Existing method for " + spec.name + " not marked customFront while processing " + actorType.typeName + ".");
|
|
}
|
|
// If the user doesn't need the impl don't generate it.
|
|
if (!custom.impl) {
|
|
return;
|
|
}
|
|
name = custom.impl;
|
|
}
|
|
|
|
proto[name] = function(...args) {
|
|
let histogram, startTime;
|
|
if (spec.telemetry) {
|
|
if (spec.oneway) {
|
|
// That just doesn't make sense.
|
|
throw Error("Telemetry specified for a oneway request");
|
|
}
|
|
let transportType = this.conn.localTransport
|
|
? "LOCAL_"
|
|
: "REMOTE_";
|
|
let histogramId = "DEVTOOLS_DEBUGGER_RDP_"
|
|
+ transportType + spec.telemetry + "_MS";
|
|
try {
|
|
histogram = Services.telemetry.getHistogramById(histogramId);
|
|
startTime = new Date();
|
|
} catch(ex) {
|
|
// XXX: Is this expected in xpcshell tests?
|
|
console.error(ex);
|
|
spec.telemetry = false;
|
|
}
|
|
}
|
|
|
|
let packet;
|
|
try {
|
|
packet = spec.request.write(args, this);
|
|
} catch(ex) {
|
|
console.error("Error writing request: " + name);
|
|
throw ex;
|
|
}
|
|
if (spec.oneway) {
|
|
// Fire-and-forget oneway packets.
|
|
this.send(packet);
|
|
return undefined;
|
|
}
|
|
|
|
return this.request(packet).then(response => {
|
|
let ret;
|
|
try {
|
|
ret = spec.response.read(response, this);
|
|
} catch(ex) {
|
|
console.error("Error reading response to: " + name);
|
|
throw ex;
|
|
}
|
|
|
|
if (histogram) {
|
|
histogram.add(+new Date - startTime);
|
|
}
|
|
|
|
return ret;
|
|
});
|
|
}
|
|
|
|
// Release methods should call the destroy function on return.
|
|
if (spec.release) {
|
|
let fn = proto[name];
|
|
proto[name] = function(...args) {
|
|
return fn.apply(this, args).then(result => {
|
|
this.destroy();
|
|
return result;
|
|
})
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
// Process event specifications
|
|
proto._clientSpec = {};
|
|
|
|
let events = proto._actorSpec.events;
|
|
if (events) {
|
|
// This actor has events, scan the prototype for preEvent handlers...
|
|
let preHandlers = new Map();
|
|
for (let name of Object.getOwnPropertyNames(proto)) {
|
|
let desc = Object.getOwnPropertyDescriptor(proto, name);
|
|
if (!desc.value) {
|
|
continue;
|
|
}
|
|
if (desc.value._preEvent) {
|
|
let preEvent = desc.value._preEvent;
|
|
if (!events.has(preEvent)) {
|
|
throw Error("preEvent for event that doesn't exist: " + preEvent);
|
|
}
|
|
let handlers = preHandlers.get(preEvent);
|
|
if (!handlers) {
|
|
handlers = [];
|
|
preHandlers.set(preEvent, handlers);
|
|
}
|
|
handlers.push(desc.value);
|
|
}
|
|
}
|
|
|
|
proto._clientSpec.events = new Map();
|
|
|
|
for (let [name, request] of events) {
|
|
proto._clientSpec.events.set(request.type, {
|
|
name: name,
|
|
request: request,
|
|
pre: preHandlers.get(name)
|
|
});
|
|
}
|
|
}
|
|
return proto;
|
|
}
|
|
|
|
/**
|
|
* Create a front class for the given actor class, with the given prototype.
|
|
*
|
|
* @param ActorClass actorType
|
|
* The actor class you're creating a front for.
|
|
* @param object proto
|
|
* The object prototype. Must have a 'typeName' property,
|
|
* should have method definitions, can have event definitions.
|
|
*/
|
|
exports.FrontClass = function(actorType, proto) {
|
|
proto.actorType = actorType;
|
|
proto.extends = Front;
|
|
let cls = Class(frontProto(proto));
|
|
registeredTypes.get(cls.prototype.typeName).frontClass = cls;
|
|
return cls;
|
|
}
|
|
|
|
|
|
exports.dumpActorSpec = function(type) {
|
|
let actorSpec = type.actorSpec;
|
|
let ret = {
|
|
category: "actor",
|
|
typeName: type.name,
|
|
methods: [],
|
|
events: {}
|
|
};
|
|
|
|
for (let method of actorSpec.methods) {
|
|
ret.methods.push({
|
|
name: method.name,
|
|
release: method.release || undefined,
|
|
oneway: method.oneway || undefined,
|
|
request: method.request.describe(),
|
|
response: method.response.describe()
|
|
});
|
|
}
|
|
|
|
if (actorSpec.events) {
|
|
for (let [name, request] of actorSpec.events) {
|
|
ret.events[name] = request.describe();
|
|
}
|
|
}
|
|
|
|
|
|
JSON.stringify(ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
exports.dumpProtocolSpec = function() {
|
|
let ret = {
|
|
types: {},
|
|
};
|
|
|
|
for (let [name, type] of registeredTypes) {
|
|
// Force lazy instantiation if needed.
|
|
type = types.getType(name);
|
|
let category = type.category || undefined;
|
|
if (category === "dict") {
|
|
ret.types[name] = {
|
|
category: "dict",
|
|
typeName: name,
|
|
specializations: type.specializations
|
|
}
|
|
} else if (category === "actor") {
|
|
ret.types[name] = exports.dumpActorSpec(type);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|