michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Preferences.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://services-common/utils.js"); michael@0: michael@0: michael@0: const ROOT_BRANCH = "datareporting."; michael@0: const POLICY_BRANCH = ROOT_BRANCH + "policy."; michael@0: const SESSIONS_BRANCH = ROOT_BRANCH + "sessions."; michael@0: const HEALTHREPORT_BRANCH = ROOT_BRANCH + "healthreport."; michael@0: const HEALTHREPORT_LOGGING_BRANCH = HEALTHREPORT_BRANCH + "logging."; michael@0: const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000; michael@0: const DEFAULT_LOAD_DELAY_FIRST_RUN_MSEC = 60 * 1000; michael@0: michael@0: /** michael@0: * The Firefox Health Report XPCOM service. michael@0: * michael@0: * External consumers will be interested in the "reporter" property of this michael@0: * service. This property is a `HealthReporter` instance that powers the michael@0: * service. The property may be null if the Health Report service is not michael@0: * enabled. michael@0: * michael@0: * EXAMPLE USAGE michael@0: * ============= michael@0: * michael@0: * let reporter = Cc["@mozilla.org/datareporting/service;1"] michael@0: * .getService(Ci.nsISupports) michael@0: * .wrappedJSObject michael@0: * .healthReporter; michael@0: * michael@0: * if (reporter.haveRemoteData) { michael@0: * // ... michael@0: * } michael@0: * michael@0: * IMPLEMENTATION NOTES michael@0: * ==================== michael@0: * michael@0: * In order to not adversely impact application start time, the `HealthReporter` michael@0: * instance is not initialized until a few seconds after "final-ui-startup." michael@0: * The exact delay is configurable via preferences so it can be adjusted with michael@0: * a hotfix extension if the default value is ever problematic. Because of the michael@0: * overhead with the initial creation of the database, the first run is delayed michael@0: * even more than subsequent runs. This does mean that the first moments of michael@0: * browser activity may be lost by FHR. michael@0: * michael@0: * Shutdown of the `HealthReporter` instance is handled completely within the michael@0: * instance (it registers observers on initialization). See the notes on that michael@0: * type for more. michael@0: */ michael@0: this.DataReportingService = function () { michael@0: this.wrappedJSObject = this; michael@0: michael@0: this._quitting = false; michael@0: michael@0: this._os = Cc["@mozilla.org/observer-service;1"] michael@0: .getService(Ci.nsIObserverService); michael@0: } michael@0: michael@0: DataReportingService.prototype = Object.freeze({ michael@0: classID: Components.ID("{41f6ae36-a79f-4613-9ac3-915e70f83789}"), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference]), michael@0: michael@0: //--------------------------------------------- michael@0: // Start of policy listeners. michael@0: //--------------------------------------------- michael@0: michael@0: /** michael@0: * Called when policy requests data upload. michael@0: */ michael@0: onRequestDataUpload: function (request) { michael@0: if (!this.healthReporter) { michael@0: return; michael@0: } michael@0: michael@0: this.healthReporter.requestDataUpload(request); michael@0: }, michael@0: michael@0: onNotifyDataPolicy: function (request) { michael@0: Observers.notify("datareporting:notify-data-policy:request", request); michael@0: }, michael@0: michael@0: onRequestRemoteDelete: function (request) { michael@0: if (!this.healthReporter) { michael@0: return; michael@0: } michael@0: michael@0: this.healthReporter.deleteRemoteData(request); michael@0: }, michael@0: michael@0: //--------------------------------------------- michael@0: // End of policy listeners. michael@0: //--------------------------------------------- michael@0: michael@0: observe: function observe(subject, topic, data) { michael@0: switch (topic) { michael@0: case "app-startup": michael@0: this._os.addObserver(this, "profile-after-change", true); michael@0: break; michael@0: michael@0: case "profile-after-change": michael@0: this._os.removeObserver(this, "profile-after-change"); michael@0: michael@0: try { michael@0: this._prefs = new Preferences(HEALTHREPORT_BRANCH); michael@0: michael@0: // We don't initialize the sessions recorder unless Health Report is michael@0: // around to provide pruning of data. michael@0: // michael@0: // FUTURE consider having the SessionsRecorder always enabled and/or michael@0: // living in its own XPCOM service. michael@0: if (this._prefs.get("service.enabled", true)) { michael@0: this.sessionRecorder = new SessionRecorder(SESSIONS_BRANCH); michael@0: this.sessionRecorder.onStartup(); michael@0: } michael@0: michael@0: // We can't interact with prefs until after the profile is present. michael@0: let policyPrefs = new Preferences(POLICY_BRANCH); michael@0: this.policy = new DataReportingPolicy(policyPrefs, this._prefs, this); michael@0: michael@0: this._os.addObserver(this, "sessionstore-windows-restored", true); michael@0: } catch (ex) { michael@0: Cu.reportError("Exception when initializing data reporting service: " + michael@0: CommonUtils.exceptionStr(ex)); michael@0: } michael@0: break; michael@0: michael@0: case "sessionstore-windows-restored": michael@0: this._os.removeObserver(this, "sessionstore-windows-restored"); michael@0: this._os.addObserver(this, "quit-application", false); michael@0: michael@0: this.policy.startPolling(); michael@0: michael@0: // Don't initialize Firefox Health Reporter collection and submission michael@0: // service unless it is enabled. michael@0: if (!this._prefs.get("service.enabled", true)) { michael@0: return; michael@0: } michael@0: michael@0: let haveFirstRun = this._prefs.get("service.firstRun", false); michael@0: let delayInterval; michael@0: michael@0: if (haveFirstRun) { michael@0: delayInterval = this._prefs.get("service.loadDelayMsec") || michael@0: DEFAULT_LOAD_DELAY_MSEC; michael@0: } else { michael@0: delayInterval = this._prefs.get("service.loadDelayFirstRunMsec") || michael@0: DEFAULT_LOAD_DELAY_FIRST_RUN_MSEC; michael@0: } michael@0: michael@0: // Delay service loading a little more so things have an opportunity michael@0: // to cool down first. michael@0: this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: this.timer.initWithCallback({ michael@0: notify: function notify() { michael@0: delete this.timer; michael@0: michael@0: // There could be a race between "quit-application" firing and michael@0: // this callback being invoked. We close that door. michael@0: if (this._quitting) { michael@0: return; michael@0: } michael@0: michael@0: // Side effect: instantiates the reporter instance if not already michael@0: // accessed. michael@0: // michael@0: // The instance installs its own shutdown observers. So, we just michael@0: // fire and forget: it will clean itself up. michael@0: let reporter = this.healthReporter; michael@0: }.bind(this), michael@0: }, delayInterval, this.timer.TYPE_ONE_SHOT); michael@0: michael@0: break; michael@0: michael@0: case "quit-application": michael@0: this._os.removeObserver(this, "quit-application"); michael@0: this._quitting = true; michael@0: michael@0: // Shutdown doesn't clear pending timers. So, we need to explicitly michael@0: // cancel our health reporter initialization timer or else it will michael@0: // attempt initialization after shutdown has commenced. This would michael@0: // likely lead to stalls or crashes. michael@0: if (this.timer) { michael@0: this.timer.cancel(); michael@0: } michael@0: michael@0: if (this.policy) { michael@0: this.policy.stopPolling(); michael@0: } michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The HealthReporter instance associated with this service. michael@0: * michael@0: * If the service is disabled, this will return null. michael@0: * michael@0: * The obtained instance may not be fully initialized. michael@0: */ michael@0: get healthReporter() { michael@0: if (!this._prefs.get("service.enabled", true)) { michael@0: return null; michael@0: } michael@0: michael@0: if ("_healthReporter" in this) { michael@0: return this._healthReporter; michael@0: } michael@0: michael@0: try { michael@0: this._loadHealthReporter(); michael@0: } catch (ex) { michael@0: this._healthReporter = null; michael@0: Cu.reportError("Exception when obtaining health reporter: " + michael@0: CommonUtils.exceptionStr(ex)); michael@0: } michael@0: michael@0: return this._healthReporter; michael@0: }, michael@0: michael@0: _loadHealthReporter: function () { michael@0: // This should never happen. It was added to help trace down bug 924307. michael@0: if (!this.policy) { michael@0: throw new Error("this.policy not set."); michael@0: } michael@0: michael@0: let ns = {}; michael@0: // Lazy import so application startup isn't adversely affected. michael@0: michael@0: Cu.import("resource://gre/modules/Task.jsm", ns); michael@0: Cu.import("resource://gre/modules/HealthReport.jsm", ns); michael@0: Cu.import("resource://gre/modules/Log.jsm", ns); michael@0: michael@0: // How many times will we rewrite this code before rolling it up into a michael@0: // generic module? See also bug 451283. michael@0: const LOGGERS = [ michael@0: "Services.DataReporting", michael@0: "Services.HealthReport", michael@0: "Services.Metrics", michael@0: "Services.BagheeraClient", michael@0: "Sqlite.Connection.healthreport", michael@0: ]; michael@0: michael@0: let loggingPrefs = new Preferences(HEALTHREPORT_LOGGING_BRANCH); michael@0: if (loggingPrefs.get("consoleEnabled", true)) { michael@0: let level = loggingPrefs.get("consoleLevel", "Warn"); michael@0: let appender = new ns.Log.ConsoleAppender(); michael@0: appender.level = ns.Log.Level[level] || ns.Log.Level.Warn; michael@0: michael@0: for (let name of LOGGERS) { michael@0: let logger = ns.Log.repository.getLogger(name); michael@0: logger.addAppender(appender); michael@0: } michael@0: } michael@0: michael@0: if (loggingPrefs.get("dumpEnabled", false)) { michael@0: let level = loggingPrefs.get("dumpLevel", "Debug"); michael@0: let appender = new ns.Log.DumpAppender(); michael@0: appender.level = ns.Log.Level[level] || ns.Log.Level.Debug; michael@0: michael@0: for (let name of LOGGERS) { michael@0: let logger = ns.Log.repository.getLogger(name); michael@0: logger.addAppender(appender); michael@0: } michael@0: } michael@0: michael@0: this._healthReporter = new ns.HealthReporter(HEALTHREPORT_BRANCH, michael@0: this.policy, michael@0: this.sessionRecorder); michael@0: michael@0: // Wait for initialization to finish so if a shutdown occurs before init michael@0: // has finished we don't adversely affect app startup on next run. michael@0: this._healthReporter.init().then(function onInit() { michael@0: this._prefs.set("service.firstRun", true); michael@0: }.bind(this)); michael@0: }, michael@0: }); michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataReportingService]); michael@0: michael@0: #define MERGED_COMPARTMENT michael@0: michael@0: #include ../common/observers.js michael@0: ; michael@0: #include policy.jsm michael@0: ; michael@0: #include sessions.jsm michael@0: ; michael@0: