1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/mozapps/update/nsUpdateTimerManager.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,316 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.9 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.10 + 1.11 +const Cc = Components.classes; 1.12 +const Ci = Components.interfaces; 1.13 + 1.14 +const PREF_APP_UPDATE_LASTUPDATETIME_FMT = "app.update.lastUpdateTime.%ID%"; 1.15 +const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay"; 1.16 +const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval"; 1.17 +const PREF_APP_UPDATE_LOG = "app.update.log"; 1.18 + 1.19 +const CATEGORY_UPDATE_TIMER = "update-timer"; 1.20 + 1.21 +XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function tm_gLogEnabled() { 1.22 + return getPref("getBoolPref", PREF_APP_UPDATE_LOG, false); 1.23 +}); 1.24 + 1.25 +/** 1.26 + * Gets a preference value, handling the case where there is no default. 1.27 + * @param func 1.28 + * The name of the preference function to call, on nsIPrefBranch 1.29 + * @param preference 1.30 + * The name of the preference 1.31 + * @param defaultValue 1.32 + * The default value to return in the event the preference has 1.33 + * no setting 1.34 + * @returns The value of the preference, or undefined if there was no 1.35 + * user or default value. 1.36 + */ 1.37 +function getPref(func, preference, defaultValue) { 1.38 + try { 1.39 + return Services.prefs[func](preference); 1.40 + } 1.41 + catch (e) { 1.42 + } 1.43 + return defaultValue; 1.44 +} 1.45 + 1.46 +/** 1.47 + * Logs a string to the error console. 1.48 + * @param string 1.49 + * The string to write to the error console. 1.50 + */ 1.51 +function LOG(string) { 1.52 + if (gLogEnabled) { 1.53 + dump("*** UTM:SVC " + string + "\n"); 1.54 + Services.console.logStringMessage("UTM:SVC " + string); 1.55 + } 1.56 +} 1.57 + 1.58 +/** 1.59 + * A manager for timers. Manages timers that fire over long periods of time 1.60 + * (e.g. days, weeks, months). 1.61 + * @constructor 1.62 + */ 1.63 +function TimerManager() { 1.64 + Services.obs.addObserver(this, "xpcom-shutdown", false); 1.65 +} 1.66 +TimerManager.prototype = { 1.67 + /** 1.68 + * The Checker Timer 1.69 + */ 1.70 + _timer: null, 1.71 + 1.72 + /** 1.73 + * The Checker Timer minimum delay interval as specified by the 1.74 + * app.update.timerMinimumDelay pref. If the app.update.timerMinimumDelay 1.75 + * pref doesn't exist this will default to 120000. 1.76 + */ 1.77 + _timerMinimumDelay: null, 1.78 + 1.79 + /** 1.80 + * The set of registered timers. 1.81 + */ 1.82 + _timers: { }, 1.83 + 1.84 + /** 1.85 + * See nsIObserver.idl 1.86 + */ 1.87 + observe: function TM_observe(aSubject, aTopic, aData) { 1.88 + // Prevent setting the timer interval to a value of less than 30 seconds. 1.89 + var minInterval = 30000; 1.90 + // Prevent setting the first timer interval to a value of less than 10 1.91 + // seconds. 1.92 + var minFirstInterval = 10000; 1.93 + switch (aTopic) { 1.94 + case "utm-test-init": 1.95 + // Enforce a minimum timer interval of 500 ms for tests and fall through 1.96 + // to profile-after-change to initialize the timer. 1.97 + minInterval = 500; 1.98 + minFirstInterval = 500; 1.99 + case "profile-after-change": 1.100 + // Cancel the timer if it has already been initialized. This is primarily 1.101 + // for tests. 1.102 + this._timerMinimumDelay = Math.max(1000 * getPref("getIntPref", PREF_APP_UPDATE_TIMERMINIMUMDELAY, 120), 1.103 + minInterval); 1.104 + let firstInterval = Math.max(getPref("getIntPref", PREF_APP_UPDATE_TIMERFIRSTINTERVAL, 1.105 + this._timerMinimumDelay), minFirstInterval); 1.106 + this._canEnsureTimer = true; 1.107 + this._ensureTimer(firstInterval); 1.108 + break; 1.109 + case "xpcom-shutdown": 1.110 + Services.obs.removeObserver(this, "xpcom-shutdown"); 1.111 + 1.112 + // Release everything we hold onto. 1.113 + this._cancelTimer(); 1.114 + for (var timerID in this._timers) 1.115 + delete this._timers[timerID]; 1.116 + this._timers = null; 1.117 + break; 1.118 + } 1.119 + }, 1.120 + 1.121 + /** 1.122 + * Called when the checking timer fires. 1.123 + * 1.124 + * We only fire one notification each time, so that the operations are 1.125 + * staggered. We don't want too many to happen at once, which could 1.126 + * negatively impact responsiveness. 1.127 + * 1.128 + * @param timer 1.129 + * The checking timer that fired. 1.130 + */ 1.131 + notify: function TM_notify(timer) { 1.132 + var nextDelay = null; 1.133 + function updateNextDelay(delay) { 1.134 + if (nextDelay === null || delay < nextDelay) 1.135 + nextDelay = delay; 1.136 + } 1.137 + 1.138 + // Each timer calls tryFire(), which figures out which is the one that 1.139 + // wanted to be called earliest. That one will be fired; the others are 1.140 + // skipped and will be done later. 1.141 + var now = Math.round(Date.now() / 1000); 1.142 + 1.143 + var callbackToFire = null; 1.144 + var earliestIntendedTime = null; 1.145 + var skippedFirings = false; 1.146 + function tryFire(callback, intendedTime) { 1.147 + var selected = false; 1.148 + if (intendedTime <= now) { 1.149 + if (intendedTime < earliestIntendedTime || 1.150 + earliestIntendedTime === null) { 1.151 + callbackToFire = callback; 1.152 + earliestIntendedTime = intendedTime; 1.153 + selected = true; 1.154 + } 1.155 + else if (earliestIntendedTime !== null) 1.156 + skippedFirings = true; 1.157 + } 1.158 + // We do not need to updateNextDelay for the timer that actually fires; 1.159 + // we'll update right after it fires, with the proper intended time. 1.160 + // Note that we might select one, then select another later (with an 1.161 + // earlier intended time); it is still ok that we did not update for 1.162 + // the first one, since if we have skipped firings, the next delay 1.163 + // will be the minimum delay anyhow. 1.164 + if (!selected) 1.165 + updateNextDelay(intendedTime - now); 1.166 + } 1.167 + 1.168 + var catMan = Cc["@mozilla.org/categorymanager;1"]. 1.169 + getService(Ci.nsICategoryManager); 1.170 + var entries = catMan.enumerateCategory(CATEGORY_UPDATE_TIMER); 1.171 + while (entries.hasMoreElements()) { 1.172 + let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data; 1.173 + let value = catMan.getCategoryEntry(CATEGORY_UPDATE_TIMER, entry); 1.174 + let [cid, method, timerID, prefInterval, defaultInterval] = value.split(","); 1.175 + 1.176 + defaultInterval = parseInt(defaultInterval); 1.177 + // cid and method are validated below when calling notify. 1.178 + if (!timerID || !defaultInterval || isNaN(defaultInterval)) { 1.179 + LOG("TimerManager:notify - update-timer category registered" + 1.180 + (cid ? " for " + cid : "") + " without required parameters - " + 1.181 + "skipping"); 1.182 + continue; 1.183 + } 1.184 + 1.185 + let interval = getPref("getIntPref", prefInterval, defaultInterval); 1.186 + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, 1.187 + timerID); 1.188 + // Initialize the last update time to 0 when the preference isn't set so 1.189 + // the timer will be notified soon after a new profile's first use. 1.190 + let lastUpdateTime = getPref("getIntPref", prefLastUpdate, 0); 1.191 + 1.192 + // If the last update time is greater than the current time then reset 1.193 + // it to 0 and the timer manager will correct the value when it fires 1.194 + // next for this consumer. 1.195 + if (lastUpdateTime > now) 1.196 + lastUpdateTime = 0; 1.197 + 1.198 + if (lastUpdateTime == 0) 1.199 + Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); 1.200 + 1.201 + tryFire(function() { 1.202 + try { 1.203 + Components.classes[cid][method](Ci.nsITimerCallback).notify(timer); 1.204 + LOG("TimerManager:notify - notified " + cid); 1.205 + } 1.206 + catch (e) { 1.207 + LOG("TimerManager:notify - error notifying component id: " + 1.208 + cid + " ,error: " + e); 1.209 + } 1.210 + lastUpdateTime = now; 1.211 + Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); 1.212 + updateNextDelay(lastUpdateTime + interval - now); 1.213 + }, lastUpdateTime + interval); 1.214 + } 1.215 + 1.216 + for (let _timerID in this._timers) { 1.217 + let timerID = _timerID; // necessary for the closure to work properly 1.218 + let timerData = this._timers[timerID]; 1.219 + // If the last update time is greater than the current time then reset 1.220 + // it to 0 and the timer manager will correct the value when it fires 1.221 + // next for this consumer. 1.222 + if (timerData.lastUpdateTime > now) { 1.223 + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, timerID); 1.224 + timerData.lastUpdateTime = 0; 1.225 + Services.prefs.setIntPref(prefLastUpdate, timerData.lastUpdateTime); 1.226 + } 1.227 + tryFire(function() { 1.228 + if (timerData.callback && timerData.callback.notify) { 1.229 + try { 1.230 + timerData.callback.notify(timer); 1.231 + LOG("TimerManager:notify - notified timerID: " + timerID); 1.232 + } 1.233 + catch (e) { 1.234 + LOG("TimerManager:notify - error notifying timerID: " + timerID + 1.235 + ", error: " + e); 1.236 + } 1.237 + } 1.238 + else { 1.239 + LOG("TimerManager:notify - timerID: " + timerID + " doesn't " + 1.240 + "implement nsITimerCallback - skipping"); 1.241 + } 1.242 + lastUpdateTime = now; 1.243 + timerData.lastUpdateTime = lastUpdateTime; 1.244 + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, timerID); 1.245 + Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); 1.246 + updateNextDelay(timerData.lastUpdateTime + timerData.interval - now); 1.247 + }, timerData.lastUpdateTime + timerData.interval); 1.248 + } 1.249 + 1.250 + if (callbackToFire) 1.251 + callbackToFire(); 1.252 + 1.253 + if (nextDelay !== null) { 1.254 + if (skippedFirings) 1.255 + timer.delay = this._timerMinimumDelay; 1.256 + else 1.257 + timer.delay = Math.max(nextDelay * 1000, this._timerMinimumDelay); 1.258 + this.lastTimerReset = Date.now(); 1.259 + } else { 1.260 + this._cancelTimer(); 1.261 + } 1.262 + }, 1.263 + 1.264 + /** 1.265 + * Starts the timer, if necessary, and ensures that it will fire soon enough 1.266 + * to happen after time |interval| (in milliseconds). 1.267 + */ 1.268 + _ensureTimer: function(interval) { 1.269 + if (!this._canEnsureTimer) 1.270 + return; 1.271 + if (!this._timer) { 1.272 + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.273 + this._timer.initWithCallback(this, interval, 1.274 + Ci.nsITimer.TYPE_REPEATING_SLACK); 1.275 + this.lastTimerReset = Date.now(); 1.276 + } else { 1.277 + if (Date.now() + interval < this.lastTimerReset + this._timer.delay) 1.278 + this._timer.delay = Math.max(this.lastTimerReset + interval - Date.now(), 0); 1.279 + } 1.280 + }, 1.281 + 1.282 + /** 1.283 + * Stops the timer, if it is running. 1.284 + */ 1.285 + _cancelTimer: function() { 1.286 + if (this._timer) { 1.287 + this._timer.cancel(); 1.288 + this._timer = null; 1.289 + } 1.290 + }, 1.291 + 1.292 + /** 1.293 + * See nsIUpdateTimerManager.idl 1.294 + */ 1.295 + registerTimer: function TM_registerTimer(id, callback, interval) { 1.296 + LOG("TimerManager:registerTimer - id: " + id); 1.297 + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, id); 1.298 + // Initialize the last update time to 0 when the preference isn't set so 1.299 + // the timer will be notified soon after a new profile's first use. 1.300 + let lastUpdateTime = getPref("getIntPref", prefLastUpdate, 0); 1.301 + let now = Math.round(Date.now() / 1000); 1.302 + if (lastUpdateTime > now) 1.303 + lastUpdateTime = 0; 1.304 + if (lastUpdateTime == 0) 1.305 + Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); 1.306 + this._timers[id] = { callback : callback, 1.307 + interval : interval, 1.308 + lastUpdateTime : lastUpdateTime }; 1.309 + 1.310 + this._ensureTimer(interval * 1000); 1.311 + }, 1.312 + 1.313 + classID: Components.ID("{B322A5C0-A419-484E-96BA-D7182163899F}"), 1.314 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateTimerManager, 1.315 + Ci.nsITimerCallback, 1.316 + Ci.nsIObserver]) 1.317 +}; 1.318 + 1.319 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TimerManager]);