/* 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"; const { extend } = require("devtools/shared/extend"); var EventEmitter = require("devtools/shared/event-emitter"); var {getStack, callFunctionWithAsyncStack} = require("devtools/shared/platform/stack"); var {settleAll} = require("devtools/shared/DevToolsUtils"); var {lazyLoadSpec, lazyLoadFront} = require("devtools/shared/specs/index"); // Bug 1454373: devtools/shared/defer still uses Promise.jsm which is slower // than DOM Promises. So implement our own copy of `defer` based on DOM Promises. function defer() { let resolve, reject; const promise = new Promise(function() { resolve = arguments[0]; reject = arguments[1]; }); return { resolve: resolve, reject: reject, promise: promise, }; } /** * 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; } // Try to lazy load the spec, if not already loaded. if (lazyLoadSpec(type)) { // If a spec module was lazy loaded, it will synchronously call // generateActorSpec, and set the type in `registeredTypes`. reg = registeredTypes.get(type); if (reg) { return reg; } } // New type, see if it's a collection/lifetime type: const sep = type.indexOf(":"); if (sep >= 0) { const collection = type.substring(0, sep); const 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 const pieces = type.split("#", 2); if (pieces.length > 1) { if (pieces[1] != "actorid") { throw new Error("Unsupported detail, only support 'actorid', got: " + pieces[1]); } return types.addActorDetail(type, pieces[0], pieces[1]); } 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.next === "function") { 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."); } const type = Object.assign({ 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. const 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:" * typestring. * * @param type subtype * The subtype to be held by the array. */ types.addArrayType = function(subtype) { subtype = types.getType(subtype); const 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) => { if (v && typeof v.next === "function") { v = [...v]; } return v.map(i => subtype.read(i, ctx)); }, write: (v, ctx) => { if (v && typeof v.next === "function") { v = [...v]; } return 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) { const specTypes = {}; for (const prop in specializations) { try { specTypes[prop] = types.getType(specializations[prop]); } catch (e) { // Types may not be defined yet. Sometimes, we define the type *after* using it, but // also, we have cyclic definitions on types. So lazily load them when they are not // immediately available. loader.lazyGetter(specTypes, prop, () => { return types.getType(specializations[prop]); }); } } return types.addType(name, { category: "dict", specializations, read: (v, ctx) => { const ret = {}; for (const prop in v) { if (prop in specTypes) { ret[prop] = specTypes[prop].read(v[prop], ctx); } else { ret[prop] = v[prop]; } } return ret; }, write: (v, ctx) => { const ret = {}; for (const prop in v) { if (prop in specTypes) { ret[prop] = specTypes[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) { // We call addActorType from: // FrontClassWithSpec when registering front synchronously, // generateActorSpec when defining specs, // specs modules to register actor type early to use them in other types if (registeredTypes.has(name)) { return registeredTypes.get(name); } const 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. const actorID = typeof (v) === "string" ? v : v.actor; let front = ctx.conn.getActor(actorID); if (!front) { // If front isn't instantiated yet, create one. // Try lazy loading front if not already loaded. // The front module will synchronously call `FrontClassWithSpec` and // augment `type` with the `frontClass` attribute. if (!type.frontClass) { lazyLoadFront(name); } // Use intermediate Class variable to please eslint requiring // a capital letter for all constructors. const Class = type.frontClass; front = new Class(ctx.conn); front.actorID = actorID; ctx.marshallPool().manage(front); } // When the type `${name}#actorid` is used, `v` is a string refering to the // actor ID. We only set the actorID just before and so do not need anything else. if (detail != "actorid") { v = identityWrite(v); front.form(v, 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); } if (detail == "actorid") { return v.actorID; } return identityWrite(v.form(detail)); } // Writing a request from the client side, just send the actor id. return v.actorID; }, }); 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}`); } 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 ':' * 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}`); } const 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 = function(index, type) { this.index = index; // Prevent force loading all Arg types by accessing it only when needed loader.lazyGetter(this, "type", function() { return types.getType(type); }); }; Arg.prototype = { 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, }; }, }; // Outside of protocol.js, Arg is called as factory method, without the new keyword. exports.Arg = function(index, type) { return new Arg(index, type); }; /** * 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 = function(index, type) { Arg.call(this, index, type); }; Option.prototype = extend(Arg.prototype, { write: function(arg, ctx, name) { // Ignore if arg is undefined or null; allow other falsy values if (arg == undefined || arg[name] == undefined) { return undefined; } const 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, }; }, }); // Outside of protocol.js, Option is called as factory method, without the new keyword. exports.Option = function(index, type) { return new Option(index, type); }; /** * Placeholder for return values in a response template. * * @param type type * The return value should be marshalled as this type. */ var RetVal = function(type) { // Prevent force loading all RetVal types by accessing it only when needed loader.lazyGetter(this, "type", function() { return types.getType(type); }); }; RetVal.prototype = { 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, }; }, }; // Outside of protocol.js, RetVal is called as factory method, without the new keyword. exports.RetVal = function(type) { return new RetVal(type); }; /* Template handling functions */ /** * Get the value at a given path, or undefined if not found. */ function getPath(obj, path) { for (const 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 (const 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 = function(template = {}) { this.type = template.type; this.template = template; this.args = findPlaceholders(template, Arg); }; Request.prototype = { /** * 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) { const ret = {}; for (const key in this.template) { const value = this.template[key]; if (value instanceof Arg) { ret[key] = value.write(value.index in fnArgs ? fnArgs[value.index] : undefined, ctx, key); } else if (key == "type") { ret[key] = value; } else { throw new Error("Request can only an object with `Arg` or `Option` properties"); } } return ret; }, /** * 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) { const fnArgs = []; for (const templateArg of this.args) { const arg = templateArg.placeholder; const path = templateArg.path; const 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 = function(template = {}) { this.template = template; const placeholders = findPlaceholders(template, RetVal); if (placeholders.length > 1) { throw Error("More than one RetVal specified in response"); } const placeholder = placeholders.shift(); if (placeholder) { this.retVal = placeholder.placeholder; this.path = placeholder.path; } }; Response.prototype = { /** * 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) { // Consider that `template` is either directly a `RetVal`, // or a dictionary with may be one `RetVal`. if (this.template instanceof RetVal) { return this.template.write(ret, ctx); } const result = {}; for (const key in this.template) { const value = this.template[key]; if (value instanceof RetVal) { result[key] = value.write(ret, ctx); } else { throw new Error("Response can only be a `RetVal` instance or an object " + "with one property being a `RetVal` instance."); } } return result; }, /** * 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; } const 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. 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 */ class Pool extends EventEmitter { constructor(conn) { super(); if (conn) { this.conn = conn; } this.__poolMap = null; } /** * Return the parent pool for this client. */ parent() { return this.conn.poolFor(this.actorID); } poolFor(actorID) { return this.conn.poolFor(actorID); } /** * Override this if you want actors returned by this actor * to belong to a different actor by default. */ marshallPool() { 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. */ 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(actor) { if (!actor.actorID) { actor.actorID = this.conn.allocID(actor.actorPrefix || actor.typeName); } else { // If the actor is already registerd in a pool, remove it without destroying it. // This happens for example when an addon is reloaded. To see this behavior, take a // look at devtools/server/tests/unit/test_addon_reload.js // TODO: not all actors have been moved to protocol.js, so they do not all have // a parent field. Remove the check for the parent once the conversion is finished const parent = this.poolFor(actor.actorID); if (parent) { parent.unmanage(actor); } } this._poolMap.set(actor.actorID, actor); } /** * Remove an actor as a child of this pool. */ unmanage(actor) { this.__poolMap && this.__poolMap.delete(actor.actorID); } // true if the given actor ID exists in the pool. has(actorID) { return this.__poolMap && this._poolMap.has(actorID); } // The actor for a given actor id stored in this pool actor(actorID) { if (this.__poolMap) { return this._poolMap.get(actorID); } return null; } // Same as actor, should update debugger connection to use 'actor' // and then remove this. get(actorID) { if (this.__poolMap) { return this._poolMap.get(actorID); } return null; } // True if this pool has no children. isEmpty() { return !this.__poolMap || this._poolMap.size == 0; } // Generator that yields each non-self child of the pool. * poolChildren() { if (!this.__poolMap) { return; } for (const actor of this.__poolMap.values()) { // Self-owned actors are ok, but don't need visiting twice. if (actor === this) { continue; } yield actor; } } /** * Destroy this item, removing it from a parent if it has one, * and destroying all children if necessary. */ destroy() { const parent = this.parent(); if (parent) { parent.unmanage(this); } if (!this.__poolMap) { return; } for (const actor of this.__poolMap.values()) { // Self-owned actors are ok, but don't need destroying twice. if (actor === this) { continue; } const 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() { this.destroy(); } } exports.Pool = Pool; /** * Keep track of which actorSpecs have been created. If a replica of a spec * is created, it can be caught, and specs which inherit from other specs will * not overwrite eachother. */ var actorSpecs = new WeakMap(); /** * An actor in the actor tree. * * @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 */ class Actor extends Pool { // Existing Actors extending this class expect initialize to contain constructor logic. initialize(conn) { // Repeat Pool.constructor here as we can't call it from initialize // This is to be removed once actors switch to es classes and are able to call // Actor's contructor. if (conn) { this.conn = conn; } // Will contain the actor's ID this.actorID = null; this._actorSpec = actorSpecs.get(Object.getPrototypeOf(this)); // Forward events to the connection. if (this._actorSpec && this._actorSpec.events) { for (const [name, request] of this._actorSpec.events.entries()) { this.on(name, (...args) => { this._sendEvent(name, request, ...args); }); } } } toString() { return "[Actor " + this.typeName + "/" + this.actorID + "]"; } _sendEvent(name, request, ...args) { if (!this.actorID) { console.error(`Tried to send a '${name}' event on an already destroyed actor` + ` '${this.typeName}'`); return; } 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() { super.destroy(); 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(hint) { return { actor: this.actorID }; } writeError(error, typeName, method) { console.error(`Error while calling actor '${typeName}'s method '${method}'`, error.message); if (error.stack) { console.error(error.stack); } this.conn.send({ from: this.actorID, error: error.error || "unknownError", message: error.message, }); } _queueResponse(create) { const pending = this._pendingResponse || Promise.resolve(null); const response = create(pending); this._pendingResponse = response; } /** * Throw an error with the passed message and attach an `error` property to the Error * object so it can be consumed by the writeError function. * @param {String} error: A string (usually a single word serving as an id) that will * be assign to error.error. * @param {String} message: The string that will be passed to the Error constructor. * @throws This always throw. */ throwError(error, message) { const err = new Error(message); err.error = error; throw err; } } 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. */ 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; }; /** * Generates an actor specification from an actor description. */ var generateActorSpec = function(actorDesc) { const actorSpec = { typeName: actorDesc.typeName, methods: [], }; // Find method and form specifications attached to properties. for (const name of Object.getOwnPropertyNames(actorDesc)) { const desc = Object.getOwnPropertyDescriptor(actorDesc, name); if (!desc.value) { continue; } if (desc.value._methodSpec) { const methodSpec = desc.value._methodSpec; const spec = {}; spec.name = methodSpec.name || name; spec.request = new Request(Object.assign({type: spec.name}, methodSpec.request || undefined)); spec.response = new Response(methodSpec.response || undefined); spec.release = methodSpec.release; spec.oneway = methodSpec.oneway; actorSpec.methods.push(spec); } } // Find additional method specifications if (actorDesc.methods) { for (const name in actorDesc.methods) { const methodSpec = actorDesc.methods[name]; const spec = {}; spec.name = methodSpec.name || name; spec.request = new Request(Object.assign({type: spec.name}, methodSpec.request || undefined)); spec.response = new Response(methodSpec.response || undefined); spec.release = methodSpec.release; spec.oneway = methodSpec.oneway; actorSpec.methods.push(spec); } } // Find event specifications if (actorDesc.events) { actorSpec.events = new Map(); for (const name in actorDesc.events) { const eventRequest = actorDesc.events[name]; Object.freeze(eventRequest); actorSpec.events.set(name, new Request(Object.assign({type: name}, eventRequest))); } } if (!registeredTypes.has(actorSpec.typeName)) { types.addActorType(actorSpec.typeName); } registeredTypes.get(actorSpec.typeName).actorSpec = actorSpec; return actorSpec; }; exports.generateActorSpec = generateActorSpec; /** * Generates request handlers as described by the given actor specification on * the given actor prototype. Returns the actor prototype. */ var generateRequestHandlers = function(actorSpec, actorProto) { actorProto.typeName = actorSpec.typeName; // Generate request handlers for each method definition actorProto.requestTypes = Object.create(null); actorSpec.methods.forEach(spec => { const 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; } if (!this[spec.name]) { throw new Error(`Spec for '${actorProto.typeName}' specifies a '${spec.name}'` + ` method that isn't implemented by the actor`); } const ret = this[spec.name].apply(this, args); const sendReturn = (retToSend) => { if (spec.oneway) { // No need to send a response. return; } let response; try { response = spec.response.write(retToSend, 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, actorProto.typeName, spec.name); return; } } conn.send(response); }; this._queueResponse(p => { return p .then(() => ret) .then(sendReturn) .catch(e => this.writeError(e, actorProto.typeName, spec.name)); }); } catch (e) { this._queueResponse(p => { return p.then(() => this.writeError(e, actorProto.typeName, spec.name)); }); } }; actorProto.requestTypes[spec.request.type] = handler; }); return actorProto; }; /** * Create an actor class for the given actor specification and prototype. * * @param object actorSpec * The actor specification. Must have a 'typeName' property. * @param object actorProto * The actor prototype. Should have method definitions, can have event * definitions. */ var ActorClassWithSpec = function(actorSpec, actorProto) { if (!actorSpec.typeName) { throw Error("Actor specification must have a typeName member."); } // Existing Actors are relying on the initialize instead of constructor methods. const cls = function() { const instance = Object.create(cls.prototype); instance.initialize.apply(instance, arguments); return instance; }; cls.prototype = extend(Actor.prototype, generateRequestHandlers(actorSpec, actorProto)); actorSpecs.set(cls.prototype, actorSpec); return cls; }; exports.ActorClassWithSpec = ActorClassWithSpec; /** * 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. * @constructor */ class Front extends Pool { constructor(conn = null) { super(conn); this.actorID = null; this._requests = []; // Front listener functions registered via `onFront` get notified // of new fronts via this dedicated EventEmitter object. this._frontListeners = new EventEmitter(); // List of optional listener for each event, that is processed immediatly on packet // receival, before emitting event via EventEmitter on the Front. // These listeners are register via Front.before function. // Map(Event Name[string] => Event Listener[function]) this._beforeListeners = new Map(); } destroy() { // Reject all outstanding requests, they won't make sense after // the front is destroyed. while (this._requests && this._requests.length > 0) { const { deferred, to, type, stack } = this._requests.shift(); const msg = "Connection closed, pending request to " + to + ", type " + type + " failed" + "\n\nRequest stack:\n" + stack.formattedStack; deferred.reject(new Error(msg)); } super.destroy(); this.clearEvents(); this.actorID = null; this._frontListeners = null; this._beforeListeners = null; } manage(front) { if (!front.actorID) { throw new Error("Can't manage front without an actor ID.\n" + "Ensure server supports " + front.typeName + "."); } super.manage(front); // Call listeners registered via `onFront` method this._frontListeners.emit(front.typeName, front); } // Run callback on every front of this type that currently exists, and on every // instantiation of front type in the future. onFront(typeName, callback) { // First fire the callback on already instantiated fronts for (const front of this.poolChildren()) { if (front.typeName == typeName) { callback(front); } } // Then register the callback for fronts instantiated in the future this._frontListeners.on(typeName, callback); } /** * Register an event listener that will be called immediately on packer receival. * The given callback is going to be called before emitting the event via EventEmitter * API on the Front. Event emitting will be delayed if the callback is async. * Only one such listener can be registered per type of event. * * @param String type * Event emitted by the actor to intercept. * @param Function callback * Function that will process the event. */ before(type, callback) { if (this._beforeListeners.has(type)) { throw new Error(`Can't register multiple before listeners for "${type}".`); } this._beforeListeners.set(type, callback); } toString() { return "[Front for " + this.typeName + "/" + this.actorID + "]"; } /** * Update the actor from its representation. * Subclasses should override this. */ form(form) {} /** * Send a packet on the connection. */ send(packet) { if (packet.to) { this.conn._transport.send(packet); } else { packet.to = this.actorID; // The connection might be closed during the promise resolution if (this.conn._transport) { this.conn._transport.send(packet); } } } /** * Send a two-way request on the connection. */ request(packet) { const deferred = defer(); // Save packet basics for debugging const { to, type } = packet; this._requests.push({ deferred, to: to || this.actorID, type, stack: getStack(), }); this.send(packet); return deferred.promise; } /** * Handler for incoming packets from the client's actor. */ onPacket(packet) { // Pick off event packets const type = packet.type || undefined; if (this._clientSpec.events && this._clientSpec.events.has(type)) { const 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; } // Check for "pre event" callback to be processed before emitting events on fronts // Use event.name instead of packet.type to use specific event name instead of RDP // packet's type. const beforeEvent = this._beforeListeners.get(event.name); if (beforeEvent) { const result = beforeEvent.apply(this, args); // Check to see if the beforeEvent returned a promise -- if so, // wait for their resolution before emitting. Otherwise, emit synchronously. if (result && typeof result.then == "function") { result.then(() => { super.emit(event.name, ...args); }); return; } } super.emit(event.name, ...args); return; } // Remaining packets must be responses. if (this._requests.length === 0) { const msg = "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet); const err = Error(msg); console.error(err); throw err; } const { deferred, stack } = this._requests.shift(); callFunctionWithAsyncStack(() => { if (packet.error) { // "Protocol error" is here to avoid TBPL heuristics. See also // https://dxr.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"); } hasRequests() { return !!this._requests.length; } /** * Wait for all current requests from this front to settle. This is especially useful * for tests and other utility environments that may not have events or mechanisms to * await the completion of requests without this utility. * * @return Promise * Resolved when all requests have settled. */ waitForRequestsToSettle() { return settleAll(this._requests.map(({ deferred }) => deferred.promise)); } } exports.Front = Front; /** * Generates request methods as described by the given actor specification on * the given front prototype. Returns the front prototype. */ var generateRequestMethods = function(actorSpec, frontProto) { if (frontProto._actorSpec) { throw new Error("frontProto called twice on the same front prototype!"); } frontProto.typeName = actorSpec.typeName; // Generate request methods. const methods = actorSpec.methods; methods.forEach(spec => { const name = spec.name; frontProto[name] = function(...args) { // If this.actorID are not available, the request will not be able to complete. // The front was probably destroyed earlier. if (!this.actorID) { throw new Error( `Can not send request because front '${this.typeName}' is already destroyed.`); } 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; } return ret; }); }; // Release methods should call the destroy function on return. if (spec.release) { const fn = frontProto[name]; frontProto[name] = function(...args) { return fn.apply(this, args).then(result => { this.destroy(); return result; }); }; } }); // Process event specifications frontProto._clientSpec = {}; const actorEvents = actorSpec.events; if (actorEvents) { frontProto._clientSpec.events = new Map(); for (const [name, request] of actorEvents) { frontProto._clientSpec.events.set(request.type, { name, request, }); } } frontProto._actorSpec = actorSpec; return frontProto; }; /** * Create a front class for the given actor specification and front prototype. * * @param object actorSpec * The actor specification 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. */ var FrontClassWithSpec = function(actorSpec) { class OneFront extends Front { } generateRequestMethods(actorSpec, OneFront.prototype); return OneFront; }; exports.FrontClassWithSpec = FrontClassWithSpec; exports.registerFront = function(cls) { const { typeName } = cls.prototype; if (!registeredTypes.has(typeName)) { types.addActorType(typeName); } registeredTypes.get(typeName).frontClass = cls; }; exports.dumpActorSpec = function(type) { const actorSpec = type.actorSpec; const ret = { category: "actor", typeName: type.name, methods: [], events: {}, }; for (const 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 (const [name, request] of actorSpec.events) { ret.events[name] = request.describe(); } } JSON.stringify(ret); return ret; }; exports.dumpProtocolSpec = function() { const ret = { types: {}, }; for (let [name, type] of registeredTypes) { // Force lazy instantiation if needed. type = types.getType(name); const 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; }; /** * Instantiate a global (preference, device) or target-scoped (webconsole, inspector) * front of the given type by picking its actor ID out of either the target or root * front's form. * * @param DebuggerClient client * The DebuggerClient instance to use. * @param string typeName * The type name of the front to instantiate. This is defined in its specifiation. * @param json form * If we want to instantiate a global actor's front, this is the root front's form, * otherwise we are instantiating a target-scoped front from the target front's form. */ function getFront(client, typeName, form) { const type = types.getType(typeName); if (!type) { throw new Error(`No spec for front type '${typeName}'.`); } if (!type.frontClass) { lazyLoadFront(typeName); } // Use intermediate Class variable to please eslint requiring // a capital letter for all constructors. const Class = type.frontClass; const instance = new Class(client); const { formAttributeName } = instance; if (!formAttributeName) { throw new Error(`Can't find the form attribute name for ${typeName}`); } // Retrive the actor ID from root or target actor's form instance.actorID = form[formAttributeName]; if (!instance.actorID) { throw new Error(`Can't find the actor ID for ${typeName} from root or target` + ` actor's form.`); } // Historically, all global and target scoped front were the first protocol.js in the // hierarchy of fronts. So that they have to self-own themself. But now, Root and Target // are fronts and should own them. The only issue here is that we should manage the // front *before* calling initialize which is going to try managing child fronts. instance.manage(instance); if (typeof (instance.initialize) == "function") { return instance.initialize().then(() => instance); } return instance; } exports.getFront = getFront;