/* 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 { rootSpec } = require("resource://devtools/shared/specs/root.js"); const { FrontClassWithSpec, registerFront, } = require("resource://devtools/shared/protocol.js"); loader.lazyRequireGetter( this, "getFront", "resource://devtools/shared/protocol.js", true ); class RootFront extends FrontClassWithSpec(rootSpec) { constructor(client, targetFront, parentFront) { super(client, targetFront, parentFront); // Cache root form as this will always be the same value. Object.defineProperty(this, "rootForm", { get() { delete this.rootForm; this.rootForm = this.getRoot(); return this.rootForm; }, configurable: true, }); // Cache of already created global scoped fronts // [typeName:string => Front instance] this.fronts = new Map(); this._client = client; } form(form) { // Root Front is a special Front. It is the only one to set its actor ID manually // out of the form object returned by RootActor.sayHello which is called when calling // DevToolsClient.connect(). this.actorID = form.from; this.applicationType = form.applicationType; this.traits = form.traits; } /** * Retrieve all service worker registrations with their corresponding workers. * @param {Array} [workerTargets] (optional) * Array containing the result of a call to `listAllWorkerTargets`. * (this exists to avoid duplication of calls to that method) * @return {Object[]} result - An Array of Objects with the following format * - {result[].registration} - The registration front * - {result[].workers} Array of form-like objects for service workers */ async listAllServiceWorkers(workerTargets) { const result = []; const { registrations } = await this.listServiceWorkerRegistrations(); const allWorkers = workerTargets ? workerTargets : await this.listAllWorkerTargets(); for (const registrationFront of registrations) { // workers from the registration, ordered from most recent to older const workers = [ registrationFront.activeWorker, registrationFront.waitingWorker, registrationFront.installingWorker, registrationFront.evaluatingWorker, ] // filter out non-existing workers .filter(w => !!w) // build a worker object with its WorkerDescriptorFront .map(workerFront => { const workerDescriptorFront = allWorkers.find( targetFront => targetFront.id === workerFront.id ); return { id: workerFront.id, name: workerFront.url, state: workerFront.state, stateText: workerFront.stateText, url: workerFront.url, workerDescriptorFront, }; }); // TODO: return only the worker targets. See Bug 1620605 result.push({ registration: registrationFront, workers, }); } return result; } /** * Retrieve all service worker registrations as well as workers from the parent and * content processes. Listing service workers involves merging information coming from * registrations and workers, this method will combine this information to present a * unified array of serviceWorkers. If you are only interested in other workers, use * listWorkers. * * @return {Object} * - {Array} service * array of form-like objects for serviceworkers * - {Array} shared * Array of WorkerTargetActor forms, containing shared workers. * - {Array} other * Array of WorkerTargetActor forms, containing other workers. */ async listAllWorkers() { const allWorkers = await this.listAllWorkerTargets(); const serviceWorkers = await this.listAllServiceWorkers(allWorkers); // NOTE: listAllServiceWorkers() now returns all the workers belonging to // a registration. To preserve the usual behavior at about:debugging, // in which we show only the most recent one, we grab the first // worker in the array only. const result = { service: serviceWorkers .map(({ registration, workers }) => { return workers.slice(0, 1).map(worker => { return Object.assign(worker, { registrationFront: registration, fetch: registration.fetch, }); }); }) .flat(), shared: [], other: [], }; allWorkers.forEach(front => { const worker = { id: front.id, url: front.url, name: front.url, workerDescriptorFront: front, }; switch (front.type) { case Ci.nsIWorkerDebugger.TYPE_SERVICE: // do nothing, since we already fetched them in `serviceWorkers` break; case Ci.nsIWorkerDebugger.TYPE_SHARED: result.shared.push(worker); break; default: result.other.push(worker); } }); return result; } /** Get the target fronts for all worker threads running in any process. */ async listAllWorkerTargets() { const listParentWorkers = async () => { const { workers } = await this.listWorkers(); return workers; }; const listChildWorkers = async () => { const processes = await this.listProcesses(); const processWorkers = await Promise.all( processes.map(async processDescriptorFront => { // Ignore parent process if (processDescriptorFront.isParentProcessDescriptor) { return []; } try { const front = await processDescriptorFront.getTarget(); if (!front) { return []; } const response = await front.listWorkers(); return response.workers; } catch (e) { if (e.message.includes("Connection closed")) { return []; } throw e; } }) ); return processWorkers.flat(); }; const [parentWorkers, childWorkers] = await Promise.all([ listParentWorkers(), listChildWorkers(), ]); return parentWorkers.concat(childWorkers); } /** * Fetch the ProcessDescriptorFront for the main process. * * `getProcess` requests allows to fetch the descriptor for any process and * the main process is having the process ID zero. */ getMainProcess() { return this.getProcess(0); } /** * Fetch the tab descriptor for the currently selected tab, or for a specific * tab given as first parameter. * * @param [optional] object filter * A dictionary object with following optional attributes: * - browserId: use to match any tab * - tab: a reference to xul:tab element (used for local tab debugging) * - isWebExtension: an optional boolean to flag TabDescriptors * If nothing is specified, returns the actor for the currently * selected tab. */ async getTab(filter) { const packet = {}; if (filter) { if (typeof filter.browserId == "number") { packet.browserId = filter.browserId; } else if ("tab" in filter) { const browser = filter.tab.linkedBrowser; packet.browserId = browser.browserId; } else { // Throw if a filter object have been passed but without // any clearly idenfified filter. throw new Error("Unsupported argument given to getTab request"); } } const descriptorFront = await super.getTab(packet); // Will flag TabDescriptor used by WebExtension codebase. if (filter?.isWebExtension) { descriptorFront.setIsForWebExtension(true); } // If the tab is a local tab, forward it to the descriptor. if (filter?.tab?.tagName == "tab") { // Ignore the fake `tab` object we receive, where there is only a // `linkedBrowser` attribute, but this isn't a real element. // devtools/client/framework/test/browser_toolbox_target.js is passing such // a fake tab. descriptorFront.setLocalTab(filter.tab); } return descriptorFront; } /** * Fetch the target front for a given add-on. * This is just an helper on top of `listAddons` request. * * @param object filter * A dictionary object with following attribute: * - id: used to match the add-on to connect to. */ async getAddon({ id }) { const addons = await this.listAddons(); const webextensionDescriptorFront = addons.find(addon => addon.id === id); return webextensionDescriptorFront; } /** * Fetch the target front for a given worker. * This is just an helper on top of `listAllWorkers` request. * * @param id */ async getWorker(id) { const { service, shared, other } = await this.listAllWorkers(); const worker = [...service, ...shared, ...other].find(w => w.id === id); if (!worker) { return null; } return worker.workerDescriptorFront || worker.registrationFront; } /** * Test request that returns the object passed as first argument. * * `echo` is special as all the property of the given object have to be passed * on the packet object. That's not something that can be achieve by requester helper. */ echo(packet) { packet.type = "echo"; return this.request(packet); } /* * This function returns a protocol.js Front for any root actor. * i.e. the one directly served from RootActor.listTabs or getRoot. * * @param String typeName * The type name used in protocol.js's spec for this actor. */ async getFront(typeName) { let front = this.fronts.get(typeName); if (front) { return front; } const rootForm = await this.rootForm; front = getFront(this._client, typeName, rootForm); this.fronts.set(typeName, front); return front; } /* * This function returns true if the root actor has a registered global actor * with a given name. * @param {String} actorName * The name of a global actor. * * @return {Boolean} */ async hasActor(actorName) { const rootForm = await this.rootForm; return !!rootForm[actorName + "Actor"]; } } exports.RootFront = RootFront; registerFront(RootFront);