|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
|
8 |
|
9 Cu.import("resource://gre/modules/Preferences.jsm"); |
|
10 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
11 Cu.import("resource://services-common/utils.js"); |
|
12 |
|
13 |
|
14 const ROOT_BRANCH = "datareporting."; |
|
15 const POLICY_BRANCH = ROOT_BRANCH + "policy."; |
|
16 const SESSIONS_BRANCH = ROOT_BRANCH + "sessions."; |
|
17 const HEALTHREPORT_BRANCH = ROOT_BRANCH + "healthreport."; |
|
18 const HEALTHREPORT_LOGGING_BRANCH = HEALTHREPORT_BRANCH + "logging."; |
|
19 const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000; |
|
20 const DEFAULT_LOAD_DELAY_FIRST_RUN_MSEC = 60 * 1000; |
|
21 |
|
22 /** |
|
23 * The Firefox Health Report XPCOM service. |
|
24 * |
|
25 * External consumers will be interested in the "reporter" property of this |
|
26 * service. This property is a `HealthReporter` instance that powers the |
|
27 * service. The property may be null if the Health Report service is not |
|
28 * enabled. |
|
29 * |
|
30 * EXAMPLE USAGE |
|
31 * ============= |
|
32 * |
|
33 * let reporter = Cc["@mozilla.org/datareporting/service;1"] |
|
34 * .getService(Ci.nsISupports) |
|
35 * .wrappedJSObject |
|
36 * .healthReporter; |
|
37 * |
|
38 * if (reporter.haveRemoteData) { |
|
39 * // ... |
|
40 * } |
|
41 * |
|
42 * IMPLEMENTATION NOTES |
|
43 * ==================== |
|
44 * |
|
45 * In order to not adversely impact application start time, the `HealthReporter` |
|
46 * instance is not initialized until a few seconds after "final-ui-startup." |
|
47 * The exact delay is configurable via preferences so it can be adjusted with |
|
48 * a hotfix extension if the default value is ever problematic. Because of the |
|
49 * overhead with the initial creation of the database, the first run is delayed |
|
50 * even more than subsequent runs. This does mean that the first moments of |
|
51 * browser activity may be lost by FHR. |
|
52 * |
|
53 * Shutdown of the `HealthReporter` instance is handled completely within the |
|
54 * instance (it registers observers on initialization). See the notes on that |
|
55 * type for more. |
|
56 */ |
|
57 this.DataReportingService = function () { |
|
58 this.wrappedJSObject = this; |
|
59 |
|
60 this._quitting = false; |
|
61 |
|
62 this._os = Cc["@mozilla.org/observer-service;1"] |
|
63 .getService(Ci.nsIObserverService); |
|
64 } |
|
65 |
|
66 DataReportingService.prototype = Object.freeze({ |
|
67 classID: Components.ID("{41f6ae36-a79f-4613-9ac3-915e70f83789}"), |
|
68 |
|
69 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, |
|
70 Ci.nsISupportsWeakReference]), |
|
71 |
|
72 //--------------------------------------------- |
|
73 // Start of policy listeners. |
|
74 //--------------------------------------------- |
|
75 |
|
76 /** |
|
77 * Called when policy requests data upload. |
|
78 */ |
|
79 onRequestDataUpload: function (request) { |
|
80 if (!this.healthReporter) { |
|
81 return; |
|
82 } |
|
83 |
|
84 this.healthReporter.requestDataUpload(request); |
|
85 }, |
|
86 |
|
87 onNotifyDataPolicy: function (request) { |
|
88 Observers.notify("datareporting:notify-data-policy:request", request); |
|
89 }, |
|
90 |
|
91 onRequestRemoteDelete: function (request) { |
|
92 if (!this.healthReporter) { |
|
93 return; |
|
94 } |
|
95 |
|
96 this.healthReporter.deleteRemoteData(request); |
|
97 }, |
|
98 |
|
99 //--------------------------------------------- |
|
100 // End of policy listeners. |
|
101 //--------------------------------------------- |
|
102 |
|
103 observe: function observe(subject, topic, data) { |
|
104 switch (topic) { |
|
105 case "app-startup": |
|
106 this._os.addObserver(this, "profile-after-change", true); |
|
107 break; |
|
108 |
|
109 case "profile-after-change": |
|
110 this._os.removeObserver(this, "profile-after-change"); |
|
111 |
|
112 try { |
|
113 this._prefs = new Preferences(HEALTHREPORT_BRANCH); |
|
114 |
|
115 // We don't initialize the sessions recorder unless Health Report is |
|
116 // around to provide pruning of data. |
|
117 // |
|
118 // FUTURE consider having the SessionsRecorder always enabled and/or |
|
119 // living in its own XPCOM service. |
|
120 if (this._prefs.get("service.enabled", true)) { |
|
121 this.sessionRecorder = new SessionRecorder(SESSIONS_BRANCH); |
|
122 this.sessionRecorder.onStartup(); |
|
123 } |
|
124 |
|
125 // We can't interact with prefs until after the profile is present. |
|
126 let policyPrefs = new Preferences(POLICY_BRANCH); |
|
127 this.policy = new DataReportingPolicy(policyPrefs, this._prefs, this); |
|
128 |
|
129 this._os.addObserver(this, "sessionstore-windows-restored", true); |
|
130 } catch (ex) { |
|
131 Cu.reportError("Exception when initializing data reporting service: " + |
|
132 CommonUtils.exceptionStr(ex)); |
|
133 } |
|
134 break; |
|
135 |
|
136 case "sessionstore-windows-restored": |
|
137 this._os.removeObserver(this, "sessionstore-windows-restored"); |
|
138 this._os.addObserver(this, "quit-application", false); |
|
139 |
|
140 this.policy.startPolling(); |
|
141 |
|
142 // Don't initialize Firefox Health Reporter collection and submission |
|
143 // service unless it is enabled. |
|
144 if (!this._prefs.get("service.enabled", true)) { |
|
145 return; |
|
146 } |
|
147 |
|
148 let haveFirstRun = this._prefs.get("service.firstRun", false); |
|
149 let delayInterval; |
|
150 |
|
151 if (haveFirstRun) { |
|
152 delayInterval = this._prefs.get("service.loadDelayMsec") || |
|
153 DEFAULT_LOAD_DELAY_MSEC; |
|
154 } else { |
|
155 delayInterval = this._prefs.get("service.loadDelayFirstRunMsec") || |
|
156 DEFAULT_LOAD_DELAY_FIRST_RUN_MSEC; |
|
157 } |
|
158 |
|
159 // Delay service loading a little more so things have an opportunity |
|
160 // to cool down first. |
|
161 this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
|
162 this.timer.initWithCallback({ |
|
163 notify: function notify() { |
|
164 delete this.timer; |
|
165 |
|
166 // There could be a race between "quit-application" firing and |
|
167 // this callback being invoked. We close that door. |
|
168 if (this._quitting) { |
|
169 return; |
|
170 } |
|
171 |
|
172 // Side effect: instantiates the reporter instance if not already |
|
173 // accessed. |
|
174 // |
|
175 // The instance installs its own shutdown observers. So, we just |
|
176 // fire and forget: it will clean itself up. |
|
177 let reporter = this.healthReporter; |
|
178 }.bind(this), |
|
179 }, delayInterval, this.timer.TYPE_ONE_SHOT); |
|
180 |
|
181 break; |
|
182 |
|
183 case "quit-application": |
|
184 this._os.removeObserver(this, "quit-application"); |
|
185 this._quitting = true; |
|
186 |
|
187 // Shutdown doesn't clear pending timers. So, we need to explicitly |
|
188 // cancel our health reporter initialization timer or else it will |
|
189 // attempt initialization after shutdown has commenced. This would |
|
190 // likely lead to stalls or crashes. |
|
191 if (this.timer) { |
|
192 this.timer.cancel(); |
|
193 } |
|
194 |
|
195 if (this.policy) { |
|
196 this.policy.stopPolling(); |
|
197 } |
|
198 break; |
|
199 } |
|
200 }, |
|
201 |
|
202 /** |
|
203 * The HealthReporter instance associated with this service. |
|
204 * |
|
205 * If the service is disabled, this will return null. |
|
206 * |
|
207 * The obtained instance may not be fully initialized. |
|
208 */ |
|
209 get healthReporter() { |
|
210 if (!this._prefs.get("service.enabled", true)) { |
|
211 return null; |
|
212 } |
|
213 |
|
214 if ("_healthReporter" in this) { |
|
215 return this._healthReporter; |
|
216 } |
|
217 |
|
218 try { |
|
219 this._loadHealthReporter(); |
|
220 } catch (ex) { |
|
221 this._healthReporter = null; |
|
222 Cu.reportError("Exception when obtaining health reporter: " + |
|
223 CommonUtils.exceptionStr(ex)); |
|
224 } |
|
225 |
|
226 return this._healthReporter; |
|
227 }, |
|
228 |
|
229 _loadHealthReporter: function () { |
|
230 // This should never happen. It was added to help trace down bug 924307. |
|
231 if (!this.policy) { |
|
232 throw new Error("this.policy not set."); |
|
233 } |
|
234 |
|
235 let ns = {}; |
|
236 // Lazy import so application startup isn't adversely affected. |
|
237 |
|
238 Cu.import("resource://gre/modules/Task.jsm", ns); |
|
239 Cu.import("resource://gre/modules/HealthReport.jsm", ns); |
|
240 Cu.import("resource://gre/modules/Log.jsm", ns); |
|
241 |
|
242 // How many times will we rewrite this code before rolling it up into a |
|
243 // generic module? See also bug 451283. |
|
244 const LOGGERS = [ |
|
245 "Services.DataReporting", |
|
246 "Services.HealthReport", |
|
247 "Services.Metrics", |
|
248 "Services.BagheeraClient", |
|
249 "Sqlite.Connection.healthreport", |
|
250 ]; |
|
251 |
|
252 let loggingPrefs = new Preferences(HEALTHREPORT_LOGGING_BRANCH); |
|
253 if (loggingPrefs.get("consoleEnabled", true)) { |
|
254 let level = loggingPrefs.get("consoleLevel", "Warn"); |
|
255 let appender = new ns.Log.ConsoleAppender(); |
|
256 appender.level = ns.Log.Level[level] || ns.Log.Level.Warn; |
|
257 |
|
258 for (let name of LOGGERS) { |
|
259 let logger = ns.Log.repository.getLogger(name); |
|
260 logger.addAppender(appender); |
|
261 } |
|
262 } |
|
263 |
|
264 if (loggingPrefs.get("dumpEnabled", false)) { |
|
265 let level = loggingPrefs.get("dumpLevel", "Debug"); |
|
266 let appender = new ns.Log.DumpAppender(); |
|
267 appender.level = ns.Log.Level[level] || ns.Log.Level.Debug; |
|
268 |
|
269 for (let name of LOGGERS) { |
|
270 let logger = ns.Log.repository.getLogger(name); |
|
271 logger.addAppender(appender); |
|
272 } |
|
273 } |
|
274 |
|
275 this._healthReporter = new ns.HealthReporter(HEALTHREPORT_BRANCH, |
|
276 this.policy, |
|
277 this.sessionRecorder); |
|
278 |
|
279 // Wait for initialization to finish so if a shutdown occurs before init |
|
280 // has finished we don't adversely affect app startup on next run. |
|
281 this._healthReporter.init().then(function onInit() { |
|
282 this._prefs.set("service.firstRun", true); |
|
283 }.bind(this)); |
|
284 }, |
|
285 }); |
|
286 |
|
287 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataReportingService]); |
|
288 |
|
289 #define MERGED_COMPARTMENT |
|
290 |
|
291 #include ../common/observers.js |
|
292 ; |
|
293 #include policy.jsm |
|
294 ; |
|
295 #include sessions.jsm |
|
296 ; |
|
297 |