diff options
Diffstat (limited to 'devtools/server/actors/common.js')
-rw-r--r-- | devtools/server/actors/common.js | 521 |
1 files changed, 521 insertions, 0 deletions
diff --git a/devtools/server/actors/common.js b/devtools/server/actors/common.js new file mode 100644 index 000000000..0177c6749 --- /dev/null +++ b/devtools/server/actors/common.js @@ -0,0 +1,521 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 promise = require("promise"); +const { method } = require("devtools/shared/protocol"); + +/** + * Creates "registered" actors factory meant for creating another kind of + * factories, ObservedActorFactory, during the call to listTabs. + * These factories live in DebuggerServer.{tab|global}ActorFactories. + * + * These actors only exposes: + * - `name` string attribute used to match actors by constructor name + * in DebuggerServer.remove{Global,Tab}Actor. + * - `createObservedActorFactory` function to create "observed" actors factory + * + * @param options object, function + * Either an object or a function. + * If given an object: + * + * If given a function (deprecated): + * Constructor function of an actor. + * The constructor function for this actor type. + * This expects to be called as a constructor (i.e. with 'new'), + * and passed two arguments: the DebuggerServerConnection, and + * the BrowserTabActor with which it will be associated. + * Only used for deprecated eagerly loaded actors. + * + */ +function RegisteredActorFactory(options, prefix) { + // By default the actor name will also be used for the actorID prefix. + this._prefix = prefix; + if (typeof (options) != "function") { + // actors definition registered by actorRegistryActor + if (options.constructorFun) { + this._getConstructor = () => options.constructorFun; + } else { + // Lazy actor definition, where options contains all the information + // required to load the actor lazily. + this._getConstructor = function () { + // Load the module + let mod; + try { + mod = require(options.id); + } catch (e) { + throw new Error("Unable to load actor module '" + options.id + "'.\n" + + e.message + "\n" + e.stack + "\n"); + } + // Fetch the actor constructor + let c = mod[options.constructorName]; + if (!c) { + throw new Error("Unable to find actor constructor named '" + + options.constructorName + "'. (Is it exported?)"); + } + return c; + }; + } + // Exposes `name` attribute in order to allow removeXXXActor to match + // the actor by its actor constructor name. + this.name = options.constructorName; + } else { + // Old actor case, where options is a function that is the actor constructor. + this._getConstructor = () => options; + // Exposes `name` attribute in order to allow removeXXXActor to match + // the actor by its actor constructor name. + this.name = options.name; + + // For old actors, we allow the use of a different prefix for actorID + // than for listTabs actor names, by fetching a prefix on the actor prototype. + // (Used by ChromeDebuggerActor) + if (options.prototype && options.prototype.actorPrefix) { + this._prefix = options.prototype.actorPrefix; + } + } +} +RegisteredActorFactory.prototype.createObservedActorFactory = function (conn, parentActor) { + return new ObservedActorFactory(this._getConstructor, this._prefix, conn, parentActor); +}; +exports.RegisteredActorFactory = RegisteredActorFactory; + +/** + * Creates "observed" actors factory meant for creating real actor instances. + * These factories lives in actor pools and fake various actor attributes. + * They will be replaced in actor pools by final actor instances during + * the first request for the same actorID from DebuggerServer._getOrCreateActor. + * + * ObservedActorFactory fakes the following actors attributes: + * actorPrefix (string) Used by ActorPool.addActor to compute the actor id + * actorID (string) Set by ActorPool.addActor just after being instantiated + * registeredPool (object) Set by ActorPool.addActor just after being + * instantiated + * And exposes the following method: + * createActor (function) Instantiate an actor that is going to replace + * this factory in the actor pool. + */ +function ObservedActorFactory(getConstructor, prefix, conn, parentActor) { + this._getConstructor = getConstructor; + this._conn = conn; + this._parentActor = parentActor; + + this.actorPrefix = prefix; + + this.actorID = null; + this.registeredPool = null; +} +ObservedActorFactory.prototype.createActor = function () { + // Fetch the actor constructor + let c = this._getConstructor(); + // Instantiate a new actor instance + let instance = new c(this._conn, this._parentActor); + instance.conn = this._conn; + instance.parentID = this._parentActor.actorID; + // We want the newly-constructed actor to completely replace the factory + // actor. Reusing the existing actor ID will make sure ActorPool.addActor + // does the right thing. + instance.actorID = this.actorID; + this.registeredPool.addActor(instance); + return instance; +}; +exports.ObservedActorFactory = ObservedActorFactory; + + +/** + * Methods shared between RootActor and BrowserTabActor. + */ + +/** + * Populate |this._extraActors| as specified by |aFactories|, reusing whatever + * actors are already there. Add all actors in the final extra actors table to + * |aPool|. + * + * The root actor and the tab actor use this to instantiate actors that other + * parts of the browser have specified with DebuggerServer.addTabActor and + * DebuggerServer.addGlobalActor. + * + * @param aFactories + * An object whose own property names are the names of properties to add to + * some reply packet (say, a tab actor grip or the "listTabs" response + * form), and whose own property values are actor constructor functions, as + * documented for addTabActor and addGlobalActor. + * + * @param this + * The BrowserRootActor or BrowserTabActor with which the new actors will + * be associated. It should support whatever API the |aFactories| + * constructor functions might be interested in, as it is passed to them. + * For the sake of CommonCreateExtraActors itself, it should have at least + * the following properties: + * + * - _extraActors + * An object whose own property names are factory table (and packet) + * property names, and whose values are no-argument actor constructors, + * of the sort that one can add to an ActorPool. + * + * - conn + * The DebuggerServerConnection in which the new actors will participate. + * + * - actorID + * The actor's name, for use as the new actors' parentID. + */ +exports.createExtraActors = function createExtraActors(aFactories, aPool) { + // Walk over global actors added by extensions. + for (let name in aFactories) { + let actor = this._extraActors[name]; + if (!actor) { + // Register another factory, but this time specific to this connection. + // It creates a fake actor that looks like an regular actor in the pool, + // but without actually instantiating the actor. + // It will only be instantiated on the first request made to the actor. + actor = aFactories[name].createObservedActorFactory(this.conn, this); + this._extraActors[name] = actor; + } + + // If the actor already exists in the pool, it may have been instantiated, + // so make sure not to overwrite it by a non-instantiated version. + if (!aPool.has(actor.actorID)) { + aPool.addActor(actor); + } + } +}; + +/** + * Append the extra actors in |this._extraActors|, constructed by a prior call + * to CommonCreateExtraActors, to |aObject|. + * + * @param aObject + * The object to which the extra actors should be added, under the + * property names given in the |aFactories| table passed to + * CommonCreateExtraActors. + * + * @param this + * The BrowserRootActor or BrowserTabActor whose |_extraActors| table we + * should use; see above. + */ +exports.appendExtraActors = function appendExtraActors(aObject) { + for (let name in this._extraActors) { + let actor = this._extraActors[name]; + aObject[name] = actor.actorID; + } +}; + +/** + * Construct an ActorPool. + * + * ActorPools are actorID -> actor mapping and storage. These are + * used to accumulate and quickly dispose of groups of actors that + * share a lifetime. + */ +function ActorPool(aConnection) +{ + this.conn = aConnection; + this._actors = {}; +} + +ActorPool.prototype = { + /** + * Destroy the pool. This will remove all actors from the pool. + */ + destroy: function AP_destroy() { + for (let id in this._actors) { + this.removeActor(this._actors[id]); + } + }, + + /** + * Add an actor to the pool. If the actor doesn't have an ID, allocate one + * from the connection. + * + * @param Object aActor + * The actor to be added to the pool. + */ + addActor: function AP_addActor(aActor) { + aActor.conn = this.conn; + if (!aActor.actorID) { + let prefix = aActor.actorPrefix; + if (!prefix && typeof aActor == "function") { + // typeName is a convention used with protocol.js-based actors + prefix = aActor.prototype.actorPrefix || aActor.prototype.typeName; + } + aActor.actorID = this.conn.allocID(prefix || undefined); + } + + // If the actor is already in a pool, remove it without destroying it. + if (aActor.registeredPool) { + delete aActor.registeredPool._actors[aActor.actorID]; + } + aActor.registeredPool = this; + + this._actors[aActor.actorID] = aActor; + }, + + /** + * Remove an actor from the pool. If the actor has a disconnect method, call + * it. + */ + removeActor: function AP_remove(aActor) { + delete this._actors[aActor.actorID]; + if (aActor.disconnect) { + aActor.disconnect(); + } + }, + + get: function AP_get(aActorID) { + return this._actors[aActorID] || undefined; + }, + + has: function AP_has(aActorID) { + return aActorID in this._actors; + }, + + /** + * Returns true if the pool is empty. + */ + isEmpty: function AP_isEmpty() { + return Object.keys(this._actors).length == 0; + }, + + /** + * Match the api expected by the protocol library. + */ + unmanage: function (aActor) { + return this.removeActor(aActor); + }, + + forEach: function (callback) { + for (let name in this._actors) { + callback(this._actors[name]); + } + }, +}; + +exports.ActorPool = ActorPool; + +/** + * An OriginalLocation represents a location in an original source. + * + * @param SourceActor actor + * A SourceActor representing an original source. + * @param Number line + * A line within the given source. + * @param Number column + * A column within the given line. + * @param String name + * The name of the symbol corresponding to this OriginalLocation. + */ +function OriginalLocation(actor, line, column, name) { + this._connection = actor ? actor.conn : null; + this._actorID = actor ? actor.actorID : undefined; + this._line = line; + this._column = column; + this._name = name; +} + +OriginalLocation.fromGeneratedLocation = function (generatedLocation) { + return new OriginalLocation( + generatedLocation.generatedSourceActor, + generatedLocation.generatedLine, + generatedLocation.generatedColumn + ); +}; + +OriginalLocation.prototype = { + get originalSourceActor() { + return this._connection ? this._connection.getActor(this._actorID) : null; + }, + + get originalUrl() { + let actor = this.originalSourceActor; + let source = actor.source; + return source ? source.url : actor._originalUrl; + }, + + get originalLine() { + return this._line; + }, + + get originalColumn() { + return this._column; + }, + + get originalName() { + return this._name; + }, + + get generatedSourceActor() { + throw new Error("Shouldn't access generatedSourceActor from an OriginalLocation"); + }, + + get generatedLine() { + throw new Error("Shouldn't access generatedLine from an OriginalLocation"); + }, + + get generatedColumn() { + throw new Error("Shouldn't access generatedColumn from an Originallocation"); + }, + + equals: function (other) { + return this.originalSourceActor.url == other.originalSourceActor.url && + this.originalLine === other.originalLine && + (this.originalColumn === undefined || + other.originalColumn === undefined || + this.originalColumn === other.originalColumn); + }, + + toJSON: function () { + return { + source: this.originalSourceActor.form(), + line: this.originalLine, + column: this.originalColumn + }; + } +}; + +exports.OriginalLocation = OriginalLocation; + +/** + * A GeneratedLocation represents a location in a generated source. + * + * @param SourceActor actor + * A SourceActor representing a generated source. + * @param Number line + * A line within the given source. + * @param Number column + * A column within the given line. + */ +function GeneratedLocation(actor, line, column, lastColumn) { + this._connection = actor ? actor.conn : null; + this._actorID = actor ? actor.actorID : undefined; + this._line = line; + this._column = column; + this._lastColumn = (lastColumn !== undefined) ? lastColumn : column + 1; +} + +GeneratedLocation.fromOriginalLocation = function (originalLocation) { + return new GeneratedLocation( + originalLocation.originalSourceActor, + originalLocation.originalLine, + originalLocation.originalColumn + ); +}; + +GeneratedLocation.prototype = { + get originalSourceActor() { + throw new Error(); + }, + + get originalUrl() { + throw new Error("Shouldn't access originalUrl from a GeneratedLocation"); + }, + + get originalLine() { + throw new Error("Shouldn't access originalLine from a GeneratedLocation"); + }, + + get originalColumn() { + throw new Error("Shouldn't access originalColumn from a GeneratedLocation"); + }, + + get originalName() { + throw new Error("Shouldn't access originalName from a GeneratedLocation"); + }, + + get generatedSourceActor() { + return this._connection ? this._connection.getActor(this._actorID) : null; + }, + + get generatedLine() { + return this._line; + }, + + get generatedColumn() { + return this._column; + }, + + get generatedLastColumn() { + return this._lastColumn; + }, + + equals: function (other) { + return this.generatedSourceActor.url == other.generatedSourceActor.url && + this.generatedLine === other.generatedLine && + (this.generatedColumn === undefined || + other.generatedColumn === undefined || + this.generatedColumn === other.generatedColumn); + }, + + toJSON: function () { + return { + source: this.generatedSourceActor.form(), + line: this.generatedLine, + column: this.generatedColumn, + lastColumn: this.generatedLastColumn + }; + } +}; + +exports.GeneratedLocation = GeneratedLocation; + +/** + * A method decorator that ensures the actor is in the expected state before + * proceeding. If the actor is not in the expected state, the decorated method + * returns a rejected promise. + * + * The actor's state must be at this.state property. + * + * @param String expectedState + * The expected state. + * @param String activity + * Additional info about what's going on. + * @param Function method + * The actor method to proceed with when the actor is in the expected + * state. + * + * @returns Function + * The decorated method. + */ +function expectState(expectedState, method, activity) { + return function (...args) { + if (this.state !== expectedState) { + const msg = `Wrong state while ${activity}:` + + `Expected '${expectedState}', ` + + `but current state is '${this.state}'.`; + return promise.reject(new Error(msg)); + } + + return method.apply(this, args); + }; +} + +exports.expectState = expectState; + +/** + * Proxies a call from an actor to an underlying module, stored + * as `bridge` on the actor. This allows a module to be defined in one + * place, usable by other modules/actors on the server, but a separate + * module defining the actor/RDP definition. + * + * @see Framerate implementation: devtools/server/performance/framerate.js + * @see Framerate actor definition: devtools/server/actors/framerate.js + */ +function actorBridge(methodName, definition = {}) { + return method(function () { + return this.bridge[methodName].apply(this.bridge, arguments); + }, definition); +} +exports.actorBridge = actorBridge; + +/** + * Like `actorBridge`, but without a spec definition, for when the actor is + * created with `ActorClassWithSpec` rather than vanilla `ActorClass`. + */ +function actorBridgeWithSpec (methodName) { + return method(function () { + return this.bridge[methodName].apply(this.bridge, arguments); + }); +} +exports.actorBridgeWithSpec = actorBridgeWithSpec; |