/* 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"; #ifndef MERGED_COMPARTMENT this.EXPORTED_SYMBOLS = ["ProviderManager"]; const {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); #endif Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://services-common/log4moz.js"); Cu.import("resource://services-common/utils.js"); /** * Handles and coordinates the collection of metrics data from providers. * * This provides an interface for managing `Metrics.Provider` instances. It * provides APIs for bulk collection of data. */ this.ProviderManager = function (storage) { this._log = Log4Moz.repository.getLogger("Services.Metrics.ProviderManager"); this._providers = new Map(); this._storage = storage; this._providerInitQueue = []; this._providerInitializing = false; this._pullOnlyProviders = {}; this._pullOnlyProvidersRegisterCount = 0; this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED; this._pullOnlyProvidersCurrentPromise = null; // Callback to allow customization of providers after they are constructed // but before they call out into their initialization code. this.onProviderInit = null; } this.ProviderManager.prototype = Object.freeze({ PULL_ONLY_NOT_REGISTERED: "none", PULL_ONLY_REGISTERING: "registering", PULL_ONLY_UNREGISTERING: "unregistering", PULL_ONLY_REGISTERED: "registered", get providers() { let providers = []; for (let [name, entry] of this._providers) { providers.push(entry.provider); } return providers; }, /** * Obtain a provider from its name. */ getProvider: function (name) { let provider = this._providers.get(name); if (!provider) { return null; } return provider.provider; }, /** * Registers providers from a category manager category. * * This examines the specified category entries and registers found * providers. * * Category entries are essentially JS modules and the name of the symbol * within that module that is a `Metrics.Provider` instance. * * The category entry name is the name of the JS type for the provider. The * value is the resource:// URI to import which makes this type available. * * Example entry: * * FooProvider resource://gre/modules/foo.jsm * * One can register entries in the application's .manifest file. e.g. * * category healthreport-js-provider-default FooProvider resource://gre/modules/foo.jsm * category healthreport-js-provider-nightly EyeballProvider resource://gre/modules/eyeball.jsm * * Then to load them: * * let reporter = getHealthReporter("healthreport."); * reporter.registerProvidersFromCategoryManager("healthreport-js-provider-default"); * * If the category has no defined members, this call has no effect, and no error is raised. * * @param category * (string) Name of category from which to query and load. * @return a newly spawned Task. */ registerProvidersFromCategoryManager: function (category) { this._log.info("Registering providers from category: " + category); let cm = Cc["@mozilla.org/categorymanager;1"] .getService(Ci.nsICategoryManager); let promises = []; let enumerator = cm.enumerateCategory(category); while (enumerator.hasMoreElements()) { let entry = enumerator.getNext() .QueryInterface(Ci.nsISupportsCString) .toString(); let uri = cm.getCategoryEntry(category, entry); this._log.info("Attempting to load provider from category manager: " + entry + " from " + uri); try { let ns = {}; Cu.import(uri, ns); let promise = this.registerProviderFromType(ns[entry]); if (promise) { promises.push(promise); } } catch (ex) { this._recordProviderError(entry, "Error registering provider from category manager", ex); continue; } } return Task.spawn(function wait() { for (let promise of promises) { yield promise; } }); }, /** * Registers a `MetricsProvider` with this manager. * * Once a `MetricsProvider` is registered, data will be collected from it * whenever we collect data. * * The returned value is a promise that will be resolved once registration * is complete. * * Providers are initialized as part of registration by calling * provider.init(). * * @param provider * (Metrics.Provider) The provider instance to register. * * @return Promise */ registerProvider: function (provider) { if (!(provider instanceof Provider)) { throw new Error("Argument must be a Provider instance."); } if (this._providers.has(provider.name)) { return CommonUtils.laterTickResolvingPromise(); } let deferred = Promise.defer(); this._providerInitQueue.push([provider, deferred]); if (this._providerInitQueue.length == 1) { this._popAndInitProvider(); } return deferred.promise; }, /** * Registers a provider from its constructor function. * * If the provider is pull-only, it will be stashed away and * initialized later. Null will be returned. * * If it is not pull-only, it will be initialized immediately and a * promise will be returned. The promise will be resolved when the * provider has finished initializing. */ registerProviderFromType: function (type) { let proto = type.prototype; if (proto.pullOnly) { this._log.info("Provider is pull-only. Deferring initialization: " + proto.name); this._pullOnlyProviders[proto.name] = type; return null; } let provider = this._initProviderFromType(type); return this.registerProvider(provider); }, /** * Initializes a provider from its type. * * This is how a constructor function should be turned into a provider * instance. * * A side-effect is the provider is registered with the manager. */ _initProviderFromType: function (type) { let provider = new type(); if (this.onProviderInit) { this.onProviderInit(provider); } return provider; }, /** * Remove a named provider from the manager. * * It is the caller's responsibility to shut down the provider * instance. */ unregisterProvider: function (name) { this._providers.delete(name); }, /** * Ensure that pull-only providers are registered. */ ensurePullOnlyProvidersRegistered: function () { let state = this._pullOnlyProvidersState; this._pullOnlyProvidersRegisterCount++; if (state == this.PULL_ONLY_REGISTERED) { this._log.debug("Requested pull-only provider registration and " + "providers are already registered."); return CommonUtils.laterTickResolvingPromise(); } // If we're in the process of registering, chain off that request. if (state == this.PULL_ONLY_REGISTERING) { this._log.debug("Requested pull-only provider registration and " + "registration is already in progress."); return this._pullOnlyProvidersCurrentPromise; } this._log.debug("Pull-only provider registration requested."); // A side-effect of setting this is that an active unregistration will // effectively short circuit and finish as soon as the in-flight // unregistration (if any) finishes. this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERING; let inFlightPromise = this._pullOnlyProvidersCurrentPromise; this._pullOnlyProvidersCurrentPromise = Task.spawn(function registerPullProviders() { if (inFlightPromise) { this._log.debug("Waiting for in-flight pull-only provider activity " + "to finish before registering."); try { yield inFlightPromise; } catch (ex) { this._log.warn("Error when waiting for existing pull-only promise: " + CommonUtils.exceptionStr(ex)); } } for each (let providerType in this._pullOnlyProviders) { // Short-circuit if we're no longer registering. if (this._pullOnlyProvidersState != this.PULL_ONLY_REGISTERING) { this._log.debug("Aborting pull-only provider registration."); break; } try { let provider = this._initProviderFromType(providerType); // This is a no-op if the provider is already registered. So, the // only overhead is constructing an instance. This should be cheap // and isn't worth optimizing. yield this.registerProvider(provider); } catch (ex) { this._recordProviderError(providerType.prototype.name, "Error registering pull-only provider", ex); } } // It's possible we changed state while registering. Only mark as // registered if we didn't change state. if (this._pullOnlyProvidersState == this.PULL_ONLY_REGISTERING) { this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERED; this._pullOnlyProvidersCurrentPromise = null; } }.bind(this)); return this._pullOnlyProvidersCurrentPromise; }, ensurePullOnlyProvidersUnregistered: function () { let state = this._pullOnlyProvidersState; // If we're not registered, this is a no-op. if (state == this.PULL_ONLY_NOT_REGISTERED) { this._log.debug("Requested pull-only provider unregistration but none " + "are registered."); return CommonUtils.laterTickResolvingPromise(); } // If we're currently unregistering, recycle the promise from last time. if (state == this.PULL_ONLY_UNREGISTERING) { this._log.debug("Requested pull-only provider unregistration and " + "unregistration is in progress."); this._pullOnlyProvidersRegisterCount = Math.max(0, this._pullOnlyProvidersRegisterCount - 1); return this._pullOnlyProvidersCurrentPromise; } // We ignore this request while multiple entities have requested // registration because we don't want a request from an "inner," // short-lived request to overwrite the desire of the "parent," // longer-lived request. if (this._pullOnlyProvidersRegisterCount > 1) { this._log.debug("Requested pull-only provider unregistration while " + "other callers still want them registered. Ignoring."); this._pullOnlyProvidersRegisterCount--; return CommonUtils.laterTickResolvingPromise(); } // We are either fully registered or registering with a single consumer. // In both cases we are authoritative and can commence unregistration. this._log.debug("Pull-only providers being unregistered."); this._pullOnlyProvidersRegisterCount = Math.max(0, this._pullOnlyProvidersRegisterCount - 1); this._pullOnlyProvidersState = this.PULL_ONLY_UNREGISTERING; let inFlightPromise = this._pullOnlyProvidersCurrentPromise; this._pullOnlyProvidersCurrentPromise = Task.spawn(function unregisterPullProviders() { if (inFlightPromise) { this._log.debug("Waiting for in-flight pull-only provider activity " + "to complete before unregistering."); try { yield inFlightPromise; } catch (ex) { this._log.warn("Error when waiting for existing pull-only promise: " + CommonUtils.exceptionStr(ex)); } } for (let provider of this.providers) { if (this._pullOnlyProvidersState != this.PULL_ONLY_UNREGISTERING) { return; } if (!provider.pullOnly) { continue; } this._log.info("Shutting down pull-only provider: " + provider.name); try { yield provider.shutdown(); } catch (ex) { this._recordProviderError(provider.name, "Error when shutting down provider", ex); } finally { this.unregisterProvider(provider.name); } } if (this._pullOnlyProvidersState == this.PULL_ONLY_UNREGISTERING) { this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED; this._pullOnlyProvidersCurrentPromise = null; } }.bind(this)); return this._pullOnlyProvidersCurrentPromise; }, _popAndInitProvider: function () { if (!this._providerInitQueue.length || this._providerInitializing) { return; } let [provider, deferred] = this._providerInitQueue.shift(); this._providerInitializing = true; this._log.info("Initializing provider with storage: " + provider.name); Task.spawn(function initProvider() { try { let result = yield provider.init(this._storage); this._log.info("Provider successfully initialized: " + provider.name); this._providers.set(provider.name, { provider: provider, constantsCollected: false, }); deferred.resolve(result); } catch (ex) { this._recordProviderError(provider.name, "Failed to initialize", ex); deferred.reject(ex); } finally { this._providerInitializing = false; this._popAndInitProvider(); } }.bind(this)); }, /** * Collects all constant measurements from all providers. * * Returns a Promise that will be fulfilled once all data providers have * provided their constant data. A side-effect of this promise fulfillment * is that the manager is populated with the obtained collection results. * The resolved value to the promise is this `ProviderManager` instance. */ collectConstantData: function () { let entries = []; for (let [name, entry] of this._providers) { if (entry.constantsCollected) { this._log.trace("Provider has already provided constant data: " + name); continue; } entries.push(entry); } let onCollect = function (entry, result) { entry.constantsCollected = true; }; return this._callCollectOnProviders(entries, "collectConstantData", onCollect); }, /** * Calls collectDailyData on all providers. */ collectDailyData: function () { return this._callCollectOnProviders(this._providers.values(), "collectDailyData"); }, _callCollectOnProviders: function (entries, fnProperty, onCollect=null) { let promises = []; for (let entry of entries) { let provider = entry.provider; let collectPromise; try { collectPromise = provider[fnProperty].call(provider); } catch (ex) { this._recordProviderError(provider.name, "Exception when calling " + "collect function: " + fnProperty, ex); continue; } if (!collectPromise) { this._recordProviderError(provider.name, "Does not return a promise " + "from " + fnProperty + "()"); continue; } let promise = collectPromise.then(function onCollected(result) { if (onCollect) { try { onCollect(entry, result); } catch (ex) { this._log.warn("onCollect callback threw: " + CommonUtils.exceptionStr(ex)); } } return CommonUtils.laterTickResolvingPromise(result); }); promises.push([provider.name, promise]); } return this._handleCollectionPromises(promises); }, /** * Handles promises returned by the collect* functions. * * This consumes the data resolved by the promises and returns a new promise * that will be resolved once all promises have been resolved. * * The promise is resolved even if one of the underlying collection * promises is rejected. */ _handleCollectionPromises: function (promises) { return Task.spawn(function waitForPromises() { for (let [name, promise] of promises) { try { yield promise; this._log.debug("Provider collected successfully: " + name); } catch (ex) { this._recordProviderError(name, "Failed to collect", ex); } } throw new Task.Result(this); }.bind(this)); }, /** * Record an error that occurred operating on a provider. */ _recordProviderError: function (name, msg, ex) { let msg = "Provider error: " + name + ": " + msg; if (ex) { msg += ": " + CommonUtils.exceptionStr(ex); } this._log.warn(msg); if (this.onProviderError) { try { this.onProviderError(msg); } catch (callError) { this._log.warn("Exception when calling onProviderError callback: " + CommonUtils.exceptionStr(callError)); } } }, });