summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/utils/actor-registry.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/utils/actor-registry.js')
-rw-r--r--devtools/server/actors/utils/actor-registry.js418
1 files changed, 418 insertions, 0 deletions
diff --git a/devtools/server/actors/utils/actor-registry.js b/devtools/server/actors/utils/actor-registry.js
new file mode 100644
index 0000000000..ae88c38c6b
--- /dev/null
+++ b/devtools/server/actors/utils/actor-registry.js
@@ -0,0 +1,418 @@
+/* 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 gRegisteredModules = Object.create(null);
+
+const ActorRegistry = {
+ // Map of global actor names to actor constructors.
+ globalActorFactories: {},
+ // Map of target-scoped actor names to actor constructors.
+ targetScopedActorFactories: {},
+ init(connections) {
+ this._connections = connections;
+ },
+
+ /**
+ * Register a CommonJS module with the devtools server.
+ * @param id string
+ * The ID of a CommonJS module.
+ * The actor is going to be registered immediately, but loaded only
+ * when a client starts sending packets to an actor with the same id.
+ *
+ * @param options object
+ * An object with 3 mandatory attributes:
+ * - prefix (string):
+ * The prefix of an actor is used to compute:
+ * - the `actorID` of each new actor instance (ex: prefix1). (See Pool.manage)
+ * - the actor name in the listTabs request. Sending a listTabs
+ * request to the root actor returns actor IDs. IDs are in
+ * dictionaries, with actor names as keys and actor IDs as values.
+ * The actor name is the prefix to which the "Actor" string is
+ * appended. So for an actor with the `console` prefix, the actor
+ * name will be `consoleActor`.
+ * - constructor (string):
+ * the name of the exported symbol to be used as the actor
+ * constructor.
+ * - type (a dictionary of booleans with following attribute names):
+ * - "global"
+ * registers a global actor instance, if true.
+ * A global actor has the root actor as its parent.
+ * - "target"
+ * registers a target-scoped actor instance, if true.
+ * A new actor will be created for each target, such as a tab.
+ */
+ registerModule(id, options) {
+ if (id in gRegisteredModules) {
+ return;
+ }
+
+ if (!options) {
+ throw new Error(
+ "ActorRegistry.registerModule requires an options argument"
+ );
+ }
+ const { prefix, constructor, type } = options;
+ if (typeof prefix !== "string") {
+ throw new Error(
+ `Lazy actor definition for '${id}' requires a string ` +
+ `'prefix' option.`
+ );
+ }
+ if (typeof constructor !== "string") {
+ throw new Error(
+ `Lazy actor definition for '${id}' requires a string ` +
+ `'constructor' option.`
+ );
+ }
+ if (!("global" in type) && !("target" in type)) {
+ throw new Error(
+ `Lazy actor definition for '${id}' requires a dictionary ` +
+ `'type' option whose attributes can be 'global' or 'target'.`
+ );
+ }
+ const name = prefix + "Actor";
+ const mod = {
+ id,
+ prefix,
+ constructorName: constructor,
+ type,
+ globalActor: type.global,
+ targetScopedActor: type.target,
+ };
+ gRegisteredModules[id] = mod;
+ if (mod.targetScopedActor) {
+ this.addTargetScopedActor(mod, name);
+ }
+ if (mod.globalActor) {
+ this.addGlobalActor(mod, name);
+ }
+ },
+
+ /**
+ * Unregister a previously-loaded CommonJS module from the devtools server.
+ */
+ unregisterModule(id) {
+ const mod = gRegisteredModules[id];
+ if (!mod) {
+ throw new Error(
+ "Tried to unregister a module that was not previously registered."
+ );
+ }
+
+ // Lazy actors
+ if (mod.targetScopedActor) {
+ this.removeTargetScopedActor(mod);
+ }
+ if (mod.globalActor) {
+ this.removeGlobalActor(mod);
+ }
+
+ delete gRegisteredModules[id];
+ },
+
+ /**
+ * Install Firefox-specific actors.
+ *
+ * /!\ Be careful when adding a new actor, especially global actors.
+ * Any new global actor will be exposed and returned by the root actor.
+ */
+ addBrowserActors() {
+ this.registerModule("devtools/server/actors/preference", {
+ prefix: "preference",
+ constructor: "PreferenceActor",
+ type: { global: true },
+ });
+ this.registerModule("devtools/server/actors/addon/addons", {
+ prefix: "addons",
+ constructor: "AddonsActor",
+ type: { global: true },
+ });
+ this.registerModule("devtools/server/actors/device", {
+ prefix: "device",
+ constructor: "DeviceActor",
+ type: { global: true },
+ });
+ this.registerModule("devtools/server/actors/heap-snapshot-file", {
+ prefix: "heapSnapshotFile",
+ constructor: "HeapSnapshotFileActor",
+ type: { global: true },
+ });
+ // Always register this as a global module, even while there is a pref turning
+ // on and off the other performance actor. This actor shouldn't conflict with
+ // the other one. These are also lazily loaded so there shouldn't be a performance
+ // impact.
+ this.registerModule("devtools/server/actors/perf", {
+ prefix: "perf",
+ constructor: "PerfActor",
+ type: { global: true },
+ });
+ /**
+ * Always register parent accessibility actor as a global module. This
+ * actor is responsible for all top level accessibility actor functionality
+ * that relies on the parent process.
+ */
+ this.registerModule(
+ "devtools/server/actors/accessibility/parent-accessibility",
+ {
+ prefix: "parentAccessibility",
+ constructor: "ParentAccessibilityActor",
+ type: { global: true },
+ }
+ );
+
+ this.registerModule("devtools/server/actors/screenshot", {
+ prefix: "screenshot",
+ constructor: "ScreenshotActor",
+ type: { global: true },
+ });
+ },
+
+ /**
+ * Install target-scoped actors.
+ */
+ addTargetScopedActors() {
+ this.registerModule("devtools/server/actors/webconsole", {
+ prefix: "console",
+ constructor: "WebConsoleActor",
+ type: { target: true },
+ });
+ this.registerModule("devtools/server/actors/inspector/inspector", {
+ prefix: "inspector",
+ constructor: "InspectorActor",
+ type: { target: true },
+ });
+ this.registerModule("devtools/server/actors/style-sheets", {
+ prefix: "styleSheets",
+ constructor: "StyleSheetsActor",
+ type: { target: true },
+ });
+ this.registerModule("devtools/server/actors/memory", {
+ prefix: "memory",
+ constructor: "MemoryActor",
+ type: { target: true },
+ });
+ this.registerModule("devtools/server/actors/reflow", {
+ prefix: "reflow",
+ constructor: "ReflowActor",
+ type: { target: true },
+ });
+ this.registerModule("devtools/server/actors/css-properties", {
+ prefix: "cssProperties",
+ constructor: "CssPropertiesActor",
+ type: { target: true },
+ });
+ this.registerModule("devtools/server/actors/animation", {
+ prefix: "animations",
+ constructor: "AnimationsActor",
+ type: { target: true },
+ });
+ this.registerModule("devtools/server/actors/emulation/responsive", {
+ prefix: "responsive",
+ constructor: "ResponsiveActor",
+ type: { target: true },
+ });
+ this.registerModule(
+ "devtools/server/actors/addon/webextension-inspected-window",
+ {
+ prefix: "webExtensionInspectedWindow",
+ constructor: "WebExtensionInspectedWindowActor",
+ type: { target: true },
+ }
+ );
+ this.registerModule("devtools/server/actors/accessibility/accessibility", {
+ prefix: "accessibility",
+ constructor: "AccessibilityActor",
+ type: { target: true },
+ });
+ this.registerModule("devtools/server/actors/changes", {
+ prefix: "changes",
+ constructor: "ChangesActor",
+ type: { target: true },
+ });
+ this.registerModule("devtools/server/actors/manifest", {
+ prefix: "manifest",
+ constructor: "ManifestActor",
+ type: { target: true },
+ });
+ this.registerModule(
+ "devtools/server/actors/network-monitor/network-content",
+ {
+ prefix: "networkContent",
+ constructor: "NetworkContentActor",
+ type: { target: true },
+ }
+ );
+ this.registerModule("devtools/server/actors/screenshot-content", {
+ prefix: "screenshotContent",
+ constructor: "ScreenshotContentActor",
+ type: { target: true },
+ });
+ this.registerModule("devtools/server/actors/tracer", {
+ prefix: "tracer",
+ constructor: "TracerActor",
+ type: { target: true },
+ });
+ this.registerModule("devtools/server/actors/objects-manager", {
+ prefix: "objectsManager",
+ constructor: "ObjectsManagerActor",
+ type: { target: true },
+ });
+ },
+
+ /**
+ * Registers handlers for new target-scoped request types defined dynamically.
+ *
+ * Note that the name of the request type is not allowed to clash with existing protocol
+ * packet properties, like 'title', 'url' or 'actor', since that would break the protocol.
+ *
+ * @param options object
+ * - constructorName: (required)
+ * name of actor constructor, which is also used when removing the actor.
+ * One of the following:
+ * - id:
+ * module ID that contains the actor
+ * - constructorFun:
+ * a function to construct the actor
+ * @param name string
+ * The name of the new request type.
+ */
+ addTargetScopedActor(options, name) {
+ if (!name) {
+ throw Error("addTargetScopedActor requires the `name` argument");
+ }
+ if (["title", "url", "actor"].includes(name)) {
+ throw Error(name + " is not allowed");
+ }
+ if (this.targetScopedActorFactories.hasOwnProperty(name)) {
+ throw Error(name + " already exists");
+ }
+ this.targetScopedActorFactories[name] = { options, name };
+ },
+
+ /**
+ * Unregisters the handler for the specified target-scoped request type.
+ *
+ * When unregistering an existing target-scoped actor, we remove the actor factory as
+ * well as all existing instances of the actor.
+ *
+ * @param actor object, string
+ * In case of object:
+ * The `actor` object being given to related addTargetScopedActor call.
+ * In case of string:
+ * The `name` string being given to related addTargetScopedActor call.
+ */
+ removeTargetScopedActor(actorOrName) {
+ let name;
+ if (typeof actorOrName == "string") {
+ name = actorOrName;
+ } else {
+ const actor = actorOrName;
+ for (const factoryName in this.targetScopedActorFactories) {
+ const handler = this.targetScopedActorFactories[factoryName];
+ if (
+ handler.options.constructorName == actor.name ||
+ handler.options.id == actor.id
+ ) {
+ name = factoryName;
+ break;
+ }
+ }
+ }
+ if (!name) {
+ return;
+ }
+ delete this.targetScopedActorFactories[name];
+ for (const connID of Object.getOwnPropertyNames(this._connections)) {
+ // DevToolsServerConnection in child process don't have rootActor
+ if (this._connections[connID].rootActor) {
+ this._connections[connID].rootActor.removeActorByName(name);
+ }
+ }
+ },
+
+ /**
+ * Registers handlers for new browser-scoped request types defined dynamically.
+ *
+ * Note that the name of the request type is not allowed to clash with existing protocol
+ * packet properties, like 'from', 'tabs' or 'selected', since that would break the protocol.
+ *
+ * @param options object
+ * - constructorName: (required)
+ * name of actor constructor, which is also used when removing the actor.
+ * One of the following:
+ * - id:
+ * module ID that contains the actor
+ * - constructorFun:
+ * a function to construct the actor
+ * @param name string
+ * The name of the new request type.
+ */
+ addGlobalActor(options, name) {
+ if (!name) {
+ throw Error("addGlobalActor requires the `name` argument");
+ }
+ if (["from", "tabs", "selected"].includes(name)) {
+ throw Error(name + " is not allowed");
+ }
+ if (this.globalActorFactories.hasOwnProperty(name)) {
+ throw Error(name + " already exists");
+ }
+ this.globalActorFactories[name] = { options, name };
+ },
+
+ /**
+ * Unregisters the handler for the specified browser-scoped request type.
+ *
+ * When unregistering an existing global actor, we remove the actor factory as well as
+ * all existing instances of the actor.
+ *
+ * @param actor object, string
+ * In case of object:
+ * The `actor` object being given to related addGlobalActor call.
+ * In case of string:
+ * The `name` string being given to related addGlobalActor call.
+ */
+ removeGlobalActor(actorOrName) {
+ let name;
+ if (typeof actorOrName == "string") {
+ name = actorOrName;
+ } else {
+ const actor = actorOrName;
+ for (const factoryName in this.globalActorFactories) {
+ const handler = this.globalActorFactories[factoryName];
+ if (
+ handler.options.constructorName == actor.name ||
+ handler.options.id == actor.id
+ ) {
+ name = factoryName;
+ break;
+ }
+ }
+ }
+ if (!name) {
+ return;
+ }
+ delete this.globalActorFactories[name];
+ for (const connID of Object.getOwnPropertyNames(this._connections)) {
+ // DevToolsServerConnection in child process don't have rootActor
+ if (this._connections[connID].rootActor) {
+ this._connections[connID].rootActor.removeActorByName(name);
+ }
+ }
+ },
+
+ destroy() {
+ for (const id of Object.getOwnPropertyNames(gRegisteredModules)) {
+ this.unregisterModule(id);
+ }
+ gRegisteredModules = Object.create(null);
+
+ this.globalActorFactories = {};
+ this.targetScopedActorFactories = {};
+ },
+};
+
+exports.ActorRegistry = ActorRegistry;