|
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 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
6 Components.utils.import("resource://gre/modules/Services.jsm"); |
|
7 |
|
8 const Cc = Components.classes; |
|
9 const Ci = Components.interfaces; |
|
10 |
|
11 const PREF_APP_UPDATE_LASTUPDATETIME_FMT = "app.update.lastUpdateTime.%ID%"; |
|
12 const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay"; |
|
13 const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval"; |
|
14 const PREF_APP_UPDATE_LOG = "app.update.log"; |
|
15 |
|
16 const CATEGORY_UPDATE_TIMER = "update-timer"; |
|
17 |
|
18 XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function tm_gLogEnabled() { |
|
19 return getPref("getBoolPref", PREF_APP_UPDATE_LOG, false); |
|
20 }); |
|
21 |
|
22 /** |
|
23 * Gets a preference value, handling the case where there is no default. |
|
24 * @param func |
|
25 * The name of the preference function to call, on nsIPrefBranch |
|
26 * @param preference |
|
27 * The name of the preference |
|
28 * @param defaultValue |
|
29 * The default value to return in the event the preference has |
|
30 * no setting |
|
31 * @returns The value of the preference, or undefined if there was no |
|
32 * user or default value. |
|
33 */ |
|
34 function getPref(func, preference, defaultValue) { |
|
35 try { |
|
36 return Services.prefs[func](preference); |
|
37 } |
|
38 catch (e) { |
|
39 } |
|
40 return defaultValue; |
|
41 } |
|
42 |
|
43 /** |
|
44 * Logs a string to the error console. |
|
45 * @param string |
|
46 * The string to write to the error console. |
|
47 */ |
|
48 function LOG(string) { |
|
49 if (gLogEnabled) { |
|
50 dump("*** UTM:SVC " + string + "\n"); |
|
51 Services.console.logStringMessage("UTM:SVC " + string); |
|
52 } |
|
53 } |
|
54 |
|
55 /** |
|
56 * A manager for timers. Manages timers that fire over long periods of time |
|
57 * (e.g. days, weeks, months). |
|
58 * @constructor |
|
59 */ |
|
60 function TimerManager() { |
|
61 Services.obs.addObserver(this, "xpcom-shutdown", false); |
|
62 } |
|
63 TimerManager.prototype = { |
|
64 /** |
|
65 * The Checker Timer |
|
66 */ |
|
67 _timer: null, |
|
68 |
|
69 /** |
|
70 * The Checker Timer minimum delay interval as specified by the |
|
71 * app.update.timerMinimumDelay pref. If the app.update.timerMinimumDelay |
|
72 * pref doesn't exist this will default to 120000. |
|
73 */ |
|
74 _timerMinimumDelay: null, |
|
75 |
|
76 /** |
|
77 * The set of registered timers. |
|
78 */ |
|
79 _timers: { }, |
|
80 |
|
81 /** |
|
82 * See nsIObserver.idl |
|
83 */ |
|
84 observe: function TM_observe(aSubject, aTopic, aData) { |
|
85 // Prevent setting the timer interval to a value of less than 30 seconds. |
|
86 var minInterval = 30000; |
|
87 // Prevent setting the first timer interval to a value of less than 10 |
|
88 // seconds. |
|
89 var minFirstInterval = 10000; |
|
90 switch (aTopic) { |
|
91 case "utm-test-init": |
|
92 // Enforce a minimum timer interval of 500 ms for tests and fall through |
|
93 // to profile-after-change to initialize the timer. |
|
94 minInterval = 500; |
|
95 minFirstInterval = 500; |
|
96 case "profile-after-change": |
|
97 // Cancel the timer if it has already been initialized. This is primarily |
|
98 // for tests. |
|
99 this._timerMinimumDelay = Math.max(1000 * getPref("getIntPref", PREF_APP_UPDATE_TIMERMINIMUMDELAY, 120), |
|
100 minInterval); |
|
101 let firstInterval = Math.max(getPref("getIntPref", PREF_APP_UPDATE_TIMERFIRSTINTERVAL, |
|
102 this._timerMinimumDelay), minFirstInterval); |
|
103 this._canEnsureTimer = true; |
|
104 this._ensureTimer(firstInterval); |
|
105 break; |
|
106 case "xpcom-shutdown": |
|
107 Services.obs.removeObserver(this, "xpcom-shutdown"); |
|
108 |
|
109 // Release everything we hold onto. |
|
110 this._cancelTimer(); |
|
111 for (var timerID in this._timers) |
|
112 delete this._timers[timerID]; |
|
113 this._timers = null; |
|
114 break; |
|
115 } |
|
116 }, |
|
117 |
|
118 /** |
|
119 * Called when the checking timer fires. |
|
120 * |
|
121 * We only fire one notification each time, so that the operations are |
|
122 * staggered. We don't want too many to happen at once, which could |
|
123 * negatively impact responsiveness. |
|
124 * |
|
125 * @param timer |
|
126 * The checking timer that fired. |
|
127 */ |
|
128 notify: function TM_notify(timer) { |
|
129 var nextDelay = null; |
|
130 function updateNextDelay(delay) { |
|
131 if (nextDelay === null || delay < nextDelay) |
|
132 nextDelay = delay; |
|
133 } |
|
134 |
|
135 // Each timer calls tryFire(), which figures out which is the one that |
|
136 // wanted to be called earliest. That one will be fired; the others are |
|
137 // skipped and will be done later. |
|
138 var now = Math.round(Date.now() / 1000); |
|
139 |
|
140 var callbackToFire = null; |
|
141 var earliestIntendedTime = null; |
|
142 var skippedFirings = false; |
|
143 function tryFire(callback, intendedTime) { |
|
144 var selected = false; |
|
145 if (intendedTime <= now) { |
|
146 if (intendedTime < earliestIntendedTime || |
|
147 earliestIntendedTime === null) { |
|
148 callbackToFire = callback; |
|
149 earliestIntendedTime = intendedTime; |
|
150 selected = true; |
|
151 } |
|
152 else if (earliestIntendedTime !== null) |
|
153 skippedFirings = true; |
|
154 } |
|
155 // We do not need to updateNextDelay for the timer that actually fires; |
|
156 // we'll update right after it fires, with the proper intended time. |
|
157 // Note that we might select one, then select another later (with an |
|
158 // earlier intended time); it is still ok that we did not update for |
|
159 // the first one, since if we have skipped firings, the next delay |
|
160 // will be the minimum delay anyhow. |
|
161 if (!selected) |
|
162 updateNextDelay(intendedTime - now); |
|
163 } |
|
164 |
|
165 var catMan = Cc["@mozilla.org/categorymanager;1"]. |
|
166 getService(Ci.nsICategoryManager); |
|
167 var entries = catMan.enumerateCategory(CATEGORY_UPDATE_TIMER); |
|
168 while (entries.hasMoreElements()) { |
|
169 let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data; |
|
170 let value = catMan.getCategoryEntry(CATEGORY_UPDATE_TIMER, entry); |
|
171 let [cid, method, timerID, prefInterval, defaultInterval] = value.split(","); |
|
172 |
|
173 defaultInterval = parseInt(defaultInterval); |
|
174 // cid and method are validated below when calling notify. |
|
175 if (!timerID || !defaultInterval || isNaN(defaultInterval)) { |
|
176 LOG("TimerManager:notify - update-timer category registered" + |
|
177 (cid ? " for " + cid : "") + " without required parameters - " + |
|
178 "skipping"); |
|
179 continue; |
|
180 } |
|
181 |
|
182 let interval = getPref("getIntPref", prefInterval, defaultInterval); |
|
183 let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, |
|
184 timerID); |
|
185 // Initialize the last update time to 0 when the preference isn't set so |
|
186 // the timer will be notified soon after a new profile's first use. |
|
187 let lastUpdateTime = getPref("getIntPref", prefLastUpdate, 0); |
|
188 |
|
189 // If the last update time is greater than the current time then reset |
|
190 // it to 0 and the timer manager will correct the value when it fires |
|
191 // next for this consumer. |
|
192 if (lastUpdateTime > now) |
|
193 lastUpdateTime = 0; |
|
194 |
|
195 if (lastUpdateTime == 0) |
|
196 Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); |
|
197 |
|
198 tryFire(function() { |
|
199 try { |
|
200 Components.classes[cid][method](Ci.nsITimerCallback).notify(timer); |
|
201 LOG("TimerManager:notify - notified " + cid); |
|
202 } |
|
203 catch (e) { |
|
204 LOG("TimerManager:notify - error notifying component id: " + |
|
205 cid + " ,error: " + e); |
|
206 } |
|
207 lastUpdateTime = now; |
|
208 Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); |
|
209 updateNextDelay(lastUpdateTime + interval - now); |
|
210 }, lastUpdateTime + interval); |
|
211 } |
|
212 |
|
213 for (let _timerID in this._timers) { |
|
214 let timerID = _timerID; // necessary for the closure to work properly |
|
215 let timerData = this._timers[timerID]; |
|
216 // If the last update time is greater than the current time then reset |
|
217 // it to 0 and the timer manager will correct the value when it fires |
|
218 // next for this consumer. |
|
219 if (timerData.lastUpdateTime > now) { |
|
220 let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, timerID); |
|
221 timerData.lastUpdateTime = 0; |
|
222 Services.prefs.setIntPref(prefLastUpdate, timerData.lastUpdateTime); |
|
223 } |
|
224 tryFire(function() { |
|
225 if (timerData.callback && timerData.callback.notify) { |
|
226 try { |
|
227 timerData.callback.notify(timer); |
|
228 LOG("TimerManager:notify - notified timerID: " + timerID); |
|
229 } |
|
230 catch (e) { |
|
231 LOG("TimerManager:notify - error notifying timerID: " + timerID + |
|
232 ", error: " + e); |
|
233 } |
|
234 } |
|
235 else { |
|
236 LOG("TimerManager:notify - timerID: " + timerID + " doesn't " + |
|
237 "implement nsITimerCallback - skipping"); |
|
238 } |
|
239 lastUpdateTime = now; |
|
240 timerData.lastUpdateTime = lastUpdateTime; |
|
241 let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, timerID); |
|
242 Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); |
|
243 updateNextDelay(timerData.lastUpdateTime + timerData.interval - now); |
|
244 }, timerData.lastUpdateTime + timerData.interval); |
|
245 } |
|
246 |
|
247 if (callbackToFire) |
|
248 callbackToFire(); |
|
249 |
|
250 if (nextDelay !== null) { |
|
251 if (skippedFirings) |
|
252 timer.delay = this._timerMinimumDelay; |
|
253 else |
|
254 timer.delay = Math.max(nextDelay * 1000, this._timerMinimumDelay); |
|
255 this.lastTimerReset = Date.now(); |
|
256 } else { |
|
257 this._cancelTimer(); |
|
258 } |
|
259 }, |
|
260 |
|
261 /** |
|
262 * Starts the timer, if necessary, and ensures that it will fire soon enough |
|
263 * to happen after time |interval| (in milliseconds). |
|
264 */ |
|
265 _ensureTimer: function(interval) { |
|
266 if (!this._canEnsureTimer) |
|
267 return; |
|
268 if (!this._timer) { |
|
269 this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
|
270 this._timer.initWithCallback(this, interval, |
|
271 Ci.nsITimer.TYPE_REPEATING_SLACK); |
|
272 this.lastTimerReset = Date.now(); |
|
273 } else { |
|
274 if (Date.now() + interval < this.lastTimerReset + this._timer.delay) |
|
275 this._timer.delay = Math.max(this.lastTimerReset + interval - Date.now(), 0); |
|
276 } |
|
277 }, |
|
278 |
|
279 /** |
|
280 * Stops the timer, if it is running. |
|
281 */ |
|
282 _cancelTimer: function() { |
|
283 if (this._timer) { |
|
284 this._timer.cancel(); |
|
285 this._timer = null; |
|
286 } |
|
287 }, |
|
288 |
|
289 /** |
|
290 * See nsIUpdateTimerManager.idl |
|
291 */ |
|
292 registerTimer: function TM_registerTimer(id, callback, interval) { |
|
293 LOG("TimerManager:registerTimer - id: " + id); |
|
294 let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, id); |
|
295 // Initialize the last update time to 0 when the preference isn't set so |
|
296 // the timer will be notified soon after a new profile's first use. |
|
297 let lastUpdateTime = getPref("getIntPref", prefLastUpdate, 0); |
|
298 let now = Math.round(Date.now() / 1000); |
|
299 if (lastUpdateTime > now) |
|
300 lastUpdateTime = 0; |
|
301 if (lastUpdateTime == 0) |
|
302 Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); |
|
303 this._timers[id] = { callback : callback, |
|
304 interval : interval, |
|
305 lastUpdateTime : lastUpdateTime }; |
|
306 |
|
307 this._ensureTimer(interval * 1000); |
|
308 }, |
|
309 |
|
310 classID: Components.ID("{B322A5C0-A419-484E-96BA-D7182163899F}"), |
|
311 QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateTimerManager, |
|
312 Ci.nsITimerCallback, |
|
313 Ci.nsIObserver]) |
|
314 }; |
|
315 |
|
316 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TimerManager]); |