diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/server/actors/root.js | 604 |
1 files changed, 604 insertions, 0 deletions
diff --git a/devtools/server/actors/root.js b/devtools/server/actors/root.js new file mode 100644 index 0000000000..90836d524e --- /dev/null +++ b/devtools/server/actors/root.js @@ -0,0 +1,604 @@ +/* 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"; + +// protocol.js uses objects as exceptions in order to define +// error packets. +/* eslint-disable no-throw-literal */ + +const { Actor, Pool } = require("resource://devtools/shared/protocol.js"); +const { rootSpec } = require("resource://devtools/shared/specs/root.js"); + +const { + LazyPool, + createExtraActors, +} = require("resource://devtools/shared/protocol/lazy-pool.js"); +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); +const Resources = require("resource://devtools/server/actors/resources/index.js"); + +loader.lazyRequireGetter( + this, + "ProcessDescriptorActor", + "resource://devtools/server/actors/descriptors/process.js", + true +); + +/* Root actor for the remote debugging protocol. */ + +/** + * Create a remote debugging protocol root actor. + * + * @param conn + * The DevToolsServerConnection whose root actor we are constructing. + * + * @param parameters + * The properties of |parameters| provide backing objects for the root + * actor's requests; if a given property is omitted from |parameters|, the + * root actor won't implement the corresponding requests or notifications. + * Supported properties: + * + * - tabList: a live list (see below) of target actors for tabs. If present, + * the new root actor supports the 'listTabs' request, providing the live + * list's elements as its target actors, and sending 'tabListChanged' + * notifications when the live list's contents change. One actor in + * this list must have a true '.selected' property. + * + * - addonList: a live list (see below) of addon actors. If present, the + * new root actor supports the 'listAddons' request, providing the live + * list's elements as its addon actors, and sending 'addonListchanged' + * notifications when the live list's contents change. + * + * - globalActorFactories: an object |A| describing further actors to + * attach to the 'listTabs' reply. This is the type accumulated by + * ActorRegistry.addGlobalActor. For each own property |P| of |A|, + * the root actor adds a property named |P| to the 'listTabs' + * reply whose value is the name of an actor constructed by + * |A[P]|. + * + * - onShutdown: a function to call when the root actor is destroyed. + * + * Instance properties: + * + * - applicationType: the string the root actor will include as the + * "applicationType" property in the greeting packet. By default, this + * is "browser". + * + * Live lists: + * + * A "live list", as used for the |tabList|, is an object that presents a + * list of actors, and also notifies its clients of changes to the list. A + * live list's interface is two properties: + * + * - getList: a method that returns a promise to the contents of the list. + * + * - onListChanged: a handler called, with no arguments, when the set of + * values the iterator would produce has changed since the last + * time 'iterator' was called. This may only be set to null or a + * callable value (one for which the typeof operator returns + * 'function'). (Note that the live list will not call the + * onListChanged handler until the list has been iterated over + * once; if nobody's seen the list in the first place, nobody + * should care if its contents have changed!) + * + * When the list changes, the list implementation should ensure that any + * actors yielded in previous iterations whose referents (tabs) still exist + * get yielded again in subsequent iterations. If the underlying referent + * is the same, the same actor should be presented for it. + * + * The root actor registers an 'onListChanged' handler on the appropriate + * list when it may need to send the client 'tabListChanged' notifications, + * and is careful to remove the handler whenever it does not need to send + * such notifications (including when it is destroyed). This means that + * live list implementations can use the state of the handler property (set + * or null) to install and remove observers and event listeners. + * + * Note that, as the only way for the root actor to see the members of the + * live list is to begin an iteration over the list, the live list need not + * actually produce any actors until they are reached in the course of + * iteration: alliterative lazy live lists. + */ +class RootActor extends Actor { + constructor(conn, parameters) { + super(conn, rootSpec); + + this._parameters = parameters; + this._onTabListChanged = this.onTabListChanged.bind(this); + this._onAddonListChanged = this.onAddonListChanged.bind(this); + this._onWorkerListChanged = this.onWorkerListChanged.bind(this); + this._onServiceWorkerRegistrationListChanged = + this.onServiceWorkerRegistrationListChanged.bind(this); + this._onProcessListChanged = this.onProcessListChanged.bind(this); + + this._extraActors = {}; + + this._globalActorPool = new LazyPool(this.conn); + + this.applicationType = "browser"; + + // Compute the list of all supported Root Resources + const supportedResources = {}; + for (const resourceType in Resources.RootResources) { + supportedResources[resourceType] = true; + } + + this.traits = { + networkMonitor: true, + resources: supportedResources, + // @backward-compat { version 84 } Expose the pref value to the client. + // Services.prefs is undefined in xpcshell tests. + workerConsoleApiMessagesDispatchedToMainThread: Services.prefs + ? Services.prefs.getBoolPref( + "dom.worker.console.dispatch_events_to_main_thread" + ) + : true, + }; + } + + /** + * Return a 'hello' packet as specified by the Remote Debugging Protocol. + */ + sayHello() { + return { + from: this.actorID, + applicationType: this.applicationType, + /* This is not in the spec, but it's used by tests. */ + testConnectionPrefix: this.conn.prefix, + traits: this.traits, + }; + } + + forwardingCancelled(prefix) { + return { + from: this.actorID, + type: "forwardingCancelled", + prefix, + }; + } + + /** + * Destroys the actor from the browser window. + */ + destroy() { + Resources.unwatchAllResources(this); + + super.destroy(); + + /* Tell the live lists we aren't watching any more. */ + if (this._parameters.tabList) { + this._parameters.tabList.destroy(); + } + if (this._parameters.addonList) { + this._parameters.addonList.onListChanged = null; + } + if (this._parameters.workerList) { + this._parameters.workerList.destroy(); + } + if (this._parameters.serviceWorkerRegistrationList) { + this._parameters.serviceWorkerRegistrationList.onListChanged = null; + } + if (this._parameters.processList) { + this._parameters.processList.onListChanged = null; + } + if (typeof this._parameters.onShutdown === "function") { + this._parameters.onShutdown(); + } + // Cleanup Actors on destroy + if (this._tabDescriptorActorPool) { + this._tabDescriptorActorPool.destroy(); + } + if (this._processDescriptorActorPool) { + this._processDescriptorActorPool.destroy(); + } + if (this._globalActorPool) { + this._globalActorPool.destroy(); + } + if (this._addonTargetActorPool) { + this._addonTargetActorPool.destroy(); + } + if (this._workerDescriptorActorPool) { + this._workerDescriptorActorPool.destroy(); + } + if (this._frameDescriptorActorPool) { + this._frameDescriptorActorPool.destroy(); + } + + if (this._serviceWorkerRegistrationActorPool) { + this._serviceWorkerRegistrationActorPool.destroy(); + } + this._extraActors = null; + this._tabDescriptorActorPool = null; + this._globalActorPool = null; + this._parameters = null; + } + + /** + * Gets the "root" form, which lists all the global actors that affect the entire + * browser. + */ + getRoot() { + // Create global actors + if (!this._globalActorPool) { + this._globalActorPool = new LazyPool(this.conn); + } + const actors = createExtraActors( + this._parameters.globalActorFactories, + this._globalActorPool, + this + ); + + return actors; + } + + /* The 'listTabs' request and the 'tabListChanged' notification. */ + + /** + * Handles the listTabs request. The actors will survive until at least + * the next listTabs request. + */ + async listTabs() { + const tabList = this._parameters.tabList; + if (!tabList) { + throw { + error: "noTabs", + message: "This root actor has no browser tabs.", + }; + } + + // Now that a client has requested the list of tabs, we reattach the onListChanged + // listener in order to be notified if the list of tabs changes again in the future. + tabList.onListChanged = this._onTabListChanged; + + // Walk the tab list, accumulating the array of target actors for the reply, and + // moving all the actors to a new Pool. We'll replace the old tab target actor + // pool with the one we build here, thus retiring any actors that didn't get listed + // again, and preparing any new actors to receive packets. + const newActorPool = new Pool(this.conn, "listTabs-tab-descriptors"); + + const tabDescriptorActors = await tabList.getList(); + for (const tabDescriptorActor of tabDescriptorActors) { + newActorPool.manage(tabDescriptorActor); + } + + // Drop the old actorID -> actor map. Actors that still mattered were added to the + // new map; others will go away. + if (this._tabDescriptorActorPool) { + this._tabDescriptorActorPool.destroy(); + } + this._tabDescriptorActorPool = newActorPool; + + return tabDescriptorActors; + } + + /** + * Return the tab descriptor actor for the tab identified by one of the IDs + * passed as argument. + * + * See BrowserTabList.prototype.getTab for the definition of these IDs. + */ + async getTab({ browserId }) { + const tabList = this._parameters.tabList; + if (!tabList) { + throw { + error: "noTabs", + message: "This root actor has no browser tabs.", + }; + } + if (!this._tabDescriptorActorPool) { + this._tabDescriptorActorPool = new Pool( + this.conn, + "getTab-tab-descriptors" + ); + } + + let descriptorActor; + try { + descriptorActor = await tabList.getTab({ + browserId, + }); + } catch (error) { + if (error.error) { + // Pipe expected errors as-is to the client + throw error; + } + throw { + error: "noTab", + message: "Unexpected error while calling getTab(): " + error, + }; + } + + descriptorActor.parentID = this.actorID; + this._tabDescriptorActorPool.manage(descriptorActor); + + return descriptorActor; + } + + onTabListChanged() { + this.conn.send({ from: this.actorID, type: "tabListChanged" }); + /* It's a one-shot notification; no need to watch any more. */ + this._parameters.tabList.onListChanged = null; + } + + /** + * This function can receive the following option from devtools client. + * + * @param {Object} option + * - iconDataURL: {boolean} + * When true, make data url from the icon of addon, then make possible to + * access by iconDataURL in the actor. The iconDataURL is useful when + * retrieving addons from a remote device, because the raw iconURL might not + * be accessible on the client. + */ + async listAddons(option) { + const addonList = this._parameters.addonList; + if (!addonList) { + throw { + error: "noAddons", + message: "This root actor has no browser addons.", + }; + } + + // Reattach the onListChanged listener now that a client requested the list. + addonList.onListChanged = this._onAddonListChanged; + + const addonTargetActors = await addonList.getList(); + const addonTargetActorPool = new Pool(this.conn, "addon-descriptors"); + for (const addonTargetActor of addonTargetActors) { + if (option.iconDataURL) { + await addonTargetActor.loadIconDataURL(); + } + + addonTargetActorPool.manage(addonTargetActor); + } + + if (this._addonTargetActorPool) { + this._addonTargetActorPool.destroy(); + } + this._addonTargetActorPool = addonTargetActorPool; + + return addonTargetActors; + } + + onAddonListChanged() { + this.conn.send({ from: this.actorID, type: "addonListChanged" }); + this._parameters.addonList.onListChanged = null; + } + + listWorkers() { + const workerList = this._parameters.workerList; + if (!workerList) { + throw { + error: "noWorkers", + message: "This root actor has no workers.", + }; + } + + // Reattach the onListChanged listener now that a client requested the list. + workerList.onListChanged = this._onWorkerListChanged; + + return workerList.getList().then(actors => { + const pool = new Pool(this.conn, "worker-targets"); + for (const actor of actors) { + pool.manage(actor); + } + + // Do not destroy the pool before transfering ownership to the newly created + // pool, so that we do not accidently destroy actors that are still in use. + if (this._workerDescriptorActorPool) { + this._workerDescriptorActorPool.destroy(); + } + + this._workerDescriptorActorPool = pool; + + return { + workers: actors, + }; + }); + } + + onWorkerListChanged() { + this.conn.send({ from: this.actorID, type: "workerListChanged" }); + this._parameters.workerList.onListChanged = null; + } + + listServiceWorkerRegistrations() { + const registrationList = this._parameters.serviceWorkerRegistrationList; + if (!registrationList) { + throw { + error: "noServiceWorkerRegistrations", + message: "This root actor has no service worker registrations.", + }; + } + + // Reattach the onListChanged listener now that a client requested the list. + registrationList.onListChanged = + this._onServiceWorkerRegistrationListChanged; + + return registrationList.getList().then(actors => { + const pool = new Pool(this.conn, "service-workers-registrations"); + for (const actor of actors) { + pool.manage(actor); + } + + if (this._serviceWorkerRegistrationActorPool) { + this._serviceWorkerRegistrationActorPool.destroy(); + } + this._serviceWorkerRegistrationActorPool = pool; + + return { + registrations: actors, + }; + }); + } + + onServiceWorkerRegistrationListChanged() { + this.conn.send({ + from: this.actorID, + type: "serviceWorkerRegistrationListChanged", + }); + this._parameters.serviceWorkerRegistrationList.onListChanged = null; + } + + listProcesses() { + const { processList } = this._parameters; + if (!processList) { + throw { + error: "noProcesses", + message: "This root actor has no processes.", + }; + } + processList.onListChanged = this._onProcessListChanged; + const processes = processList.getList(); + const pool = new Pool(this.conn, "process-descriptors"); + for (const metadata of processes) { + let processDescriptor = this._getKnownDescriptor( + metadata.id, + this._processDescriptorActorPool + ); + if (!processDescriptor) { + processDescriptor = new ProcessDescriptorActor(this.conn, metadata); + } + pool.manage(processDescriptor); + } + // Do not destroy the pool before transfering ownership to the newly created + // pool, so that we do not accidently destroy actors that are still in use. + if (this._processDescriptorActorPool) { + this._processDescriptorActorPool.destroy(); + } + this._processDescriptorActorPool = pool; + return [...this._processDescriptorActorPool.poolChildren()]; + } + + onProcessListChanged() { + this.conn.send({ from: this.actorID, type: "processListChanged" }); + this._parameters.processList.onListChanged = null; + } + + async getProcess(id) { + if (!DevToolsServer.allowChromeProcess) { + throw { + error: "forbidden", + message: "You are not allowed to debug chrome.", + }; + } + if (typeof id != "number") { + throw { + error: "wrongParameter", + message: "getProcess requires a valid `id` attribute.", + }; + } + this._processDescriptorActorPool = + this._processDescriptorActorPool || + new Pool(this.conn, "process-descriptors"); + + let processDescriptor = this._getKnownDescriptor( + id, + this._processDescriptorActorPool + ); + if (!processDescriptor) { + // The parent process has id == 0, based on ProcessActorList::getList implementation + const options = { id, parent: id === 0 }; + processDescriptor = new ProcessDescriptorActor(this.conn, options); + this._processDescriptorActorPool.manage(processDescriptor); + } + return processDescriptor; + } + + _getKnownDescriptor(id, pool) { + // if there is no pool, then we do not have any descriptors + if (!pool) { + return null; + } + for (const descriptor of pool.poolChildren()) { + if (descriptor.id === id) { + return descriptor; + } + } + return null; + } + + /** + * Remove the extra actor (added by ActorRegistry.addGlobalActor or + * ActorRegistry.addTargetScopedActor) name |name|. + */ + removeActorByName(name) { + if (name in this._extraActors) { + const actor = this._extraActors[name]; + if (this._globalActorPool.has(actor.actorID)) { + actor.destroy(); + } + if (this._tabDescriptorActorPool) { + // Iterate over WindowGlobalTargetActor instances to also remove target-scoped + // actors created during listTabs for each document. + for (const tab in this._tabDescriptorActorPool.poolChildren()) { + tab.removeActorByName(name); + } + } + delete this._extraActors[name]; + } + } + + /** + * Start watching for a list of resource types. + * + * See WatcherActor.watchResources. + */ + async watchResources(resourceTypes) { + await Resources.watchResources(this, resourceTypes); + } + + /** + * Stop watching for a list of resource types. + * + * See WatcherActor.unwatchResources. + */ + unwatchResources(resourceTypes) { + Resources.unwatchResources(this, resourceTypes); + } + + /** + * Clear resources of a list of resource types. + * + * See WatcherActor.clearResources. + */ + clearResources(resourceTypes) { + Resources.clearResources(this, resourceTypes); + } + + /** + * Called by Resource Watchers, when new resources are available, updated or destroyed. + * + * @param String updateType + * Can be "available", "updated" or "destroyed" + * @param Array<json> resources + * List of all resources. A resource is a JSON object piped over to the client. + * It can contain actor IDs. + * It can also be or contain an actor form, to be manually marshalled by the client. + * (i.e. the frontend would have to manually instantiate a Front for the given actor form) + */ + notifyResources(updateType, resources) { + if (resources.length === 0) { + // Don't try to emit if the resources array is empty. + return; + } + + switch (updateType) { + case "available": + this.emit(`resource-available-form`, resources); + break; + case "updated": + this.emit(`resource-updated-form`, resources); + break; + case "destroyed": + this.emit(`resource-destroyed-form`, resources); + break; + default: + throw new Error("Unsupported update type: " + updateType); + } + } +} + +exports.RootActor = RootActor; |