/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* 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;