diff options
Diffstat (limited to 'components/timermanager')
-rw-r--r-- | components/timermanager/moz.build | 13 | ||||
-rw-r--r-- | components/timermanager/nsIUpdateTimerManager.idl | 54 | ||||
-rw-r--r-- | components/timermanager/nsUpdateTimerManager.js | 319 | ||||
-rw-r--r-- | components/timermanager/nsUpdateTimerManager.manifest | 3 |
4 files changed, 389 insertions, 0 deletions
diff --git a/components/timermanager/moz.build b/components/timermanager/moz.build new file mode 100644 index 000000000..c75945528 --- /dev/null +++ b/components/timermanager/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +XPIDL_MODULE = 'update' + +XPIDL_SOURCES += ['nsIUpdateTimerManager.idl'] + +EXTRA_COMPONENTS += [ + 'nsUpdateTimerManager.js', + 'nsUpdateTimerManager.manifest', +] diff --git a/components/timermanager/nsIUpdateTimerManager.idl b/components/timermanager/nsIUpdateTimerManager.idl new file mode 100644 index 000000000..6f9e2d169 --- /dev/null +++ b/components/timermanager/nsIUpdateTimerManager.idl @@ -0,0 +1,54 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "nsISupports.idl" + +interface nsITimerCallback; + +/** + * An interface describing a global application service that allows long + * duration (e.g. 1-7 or more days, weeks or months) timers to be registered + * and then fired. + */ +[scriptable, uuid(0765c92c-6145-4253-9db4-594d8023087e)] +interface nsIUpdateTimerManager : nsISupports +{ + /** + * Register an interval with the timer manager. The timer manager + * periodically checks to see if the interval has expired and if it has + * calls the specified callback. This is persistent across application + * restarts and can handle intervals of long durations. + * @param id + * An id that identifies the interval, used for persistence + * @param callback + * A nsITimerCallback object that is notified when the interval + * expires + * @param interval + * The length of time, in seconds, of the interval + * + * Note: to avoid having to instantiate a component to call registerTimer + * the component can intead register an update-timer category with comma + * separated values as a single string representing the timer as follows. + * + * _xpcom_categories: [{ category: "update-timer", + * value: "contractID," + + * "method," + + * "id," + + * "preference," + + * "interval" }], + * the values are as follows + * contractID : the contract ID for the component. + * method : the method used to instantiate the interface. This should be + * either getService or createInstance depending on your + * component. + * id : the id that identifies the interval, used for persistence. + * preference : the preference to for timer interval. This value can be + * optional by specifying an empty string for the value. + * interval : the default interval in seconds for the timer. + */ + void registerTimer(in AString id, + in nsITimerCallback callback, + in unsigned long interval); +}; diff --git a/components/timermanager/nsUpdateTimerManager.js b/components/timermanager/nsUpdateTimerManager.js new file mode 100644 index 000000000..3ba82f8c0 --- /dev/null +++ b/components/timermanager/nsUpdateTimerManager.js @@ -0,0 +1,319 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm", this); +Components.utils.import("resource://gre/modules/Services.jsm", this); + +const Cc = Components.classes; +const Ci = Components.interfaces; + +const PREF_APP_UPDATE_LASTUPDATETIME_FMT = "app.update.lastUpdateTime.%ID%"; +const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay"; +const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval"; +const PREF_APP_UPDATE_LOG = "app.update.log"; + +const CATEGORY_UPDATE_TIMER = "update-timer"; + +XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function tm_gLogEnabled() { + return Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false); +}); + +/** + * Logs a string to the error console. + * @param string + * The string to write to the error console. + */ +function LOG(string) { + if (gLogEnabled) { + dump("*** UTM:SVC " + string + "\n"); + Services.console.logStringMessage("UTM:SVC " + string); + } +} + +/** + * A manager for timers. Manages timers that fire over long periods of time + * (e.g. days, weeks, months). + * @constructor + */ +function TimerManager() { + Services.obs.addObserver(this, "xpcom-shutdown", false); +} +TimerManager.prototype = { + /** + * The Checker Timer + */ + _timer: null, + + /** + * The Checker Timer minimum delay interval as specified by the + * app.update.timerMinimumDelay pref. If the app.update.timerMinimumDelay + * pref doesn't exist this will default to 120000. + */ + _timerMinimumDelay: null, + + /** + * The set of registered timers. + */ + _timers: { }, + + /** + * See nsIObserver.idl + */ + observe: function TM_observe(aSubject, aTopic, aData) { + // Prevent setting the timer interval to a value of less than 30 seconds. + var minInterval = 30000; + // Prevent setting the first timer interval to a value of less than 10 + // seconds. + var minFirstInterval = 10000; + switch (aTopic) { + case "utm-test-init": + // Enforce a minimum timer interval of 500 ms for tests and fall through + // to profile-after-change to initialize the timer. + minInterval = 500; + minFirstInterval = 500; + case "profile-after-change": + this._timerMinimumDelay = Math.max(1000 * Services.prefs.getIntPref(PREF_APP_UPDATE_TIMERMINIMUMDELAY, 120), + minInterval); + // Prevent the timer delay between notifications to other consumers from + // being greater than 5 minutes which is 300000 milliseconds. + this._timerMinimumDelay = Math.min(this._timerMinimumDelay, 300000); + // Prevent the first interval from being less than the value of minFirstInterval + let firstInterval = Math.max(Services.prefs.getIntPref(PREF_APP_UPDATE_TIMERFIRSTINTERVAL, + 30000), minFirstInterval); + // Prevent the first interval from being greater than 2 minutes which is + // 120000 milliseconds. + firstInterval = Math.min(firstInterval, 120000); + // Cancel the timer if it has already been initialized. This is primarily + // for tests. + this._canEnsureTimer = true; + this._ensureTimer(firstInterval); + break; + case "xpcom-shutdown": + Services.obs.removeObserver(this, "xpcom-shutdown"); + + // Release everything we hold onto. + this._cancelTimer(); + for (var timerID in this._timers) { + delete this._timers[timerID]; + } + this._timers = null; + break; + } + }, + + /** + * Called when the checking timer fires. + * + * We only fire one notification each time, so that the operations are + * staggered. We don't want too many to happen at once, which could + * negatively impact responsiveness. + * + * @param timer + * The checking timer that fired. + */ + notify: function TM_notify(timer) { + var nextDelay = null; + function updateNextDelay(delay) { + if (nextDelay === null || delay < nextDelay) { + nextDelay = delay; + } + } + + // Each timer calls tryFire(), which figures out which is the one that + // wanted to be called earliest. That one will be fired; the others are + // skipped and will be done later. + var now = Math.round(Date.now() / 1000); + + var callbackToFire = null; + var earliestIntendedTime = null; + var skippedFirings = false; + var lastUpdateTime = null; + function tryFire(callback, intendedTime) { + var selected = false; + if (intendedTime <= now) { + if (intendedTime < earliestIntendedTime || + earliestIntendedTime === null) { + callbackToFire = callback; + earliestIntendedTime = intendedTime; + selected = true; + } else if (earliestIntendedTime !== null) { + skippedFirings = true; + } + } + // We do not need to updateNextDelay for the timer that actually fires; + // we'll update right after it fires, with the proper intended time. + // Note that we might select one, then select another later (with an + // earlier intended time); it is still ok that we did not update for + // the first one, since if we have skipped firings, the next delay + // will be the minimum delay anyhow. + if (!selected) { + updateNextDelay(intendedTime - now); + } + } + + var catMan = Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager); + var entries = catMan.enumerateCategory(CATEGORY_UPDATE_TIMER); + while (entries.hasMoreElements()) { + let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data; + let value = catMan.getCategoryEntry(CATEGORY_UPDATE_TIMER, entry); + let [cid, method, timerID, prefInterval, defaultInterval, maxInterval] = value.split(","); + + defaultInterval = parseInt(defaultInterval); + // cid and method are validated below when calling notify. + if (!timerID || !defaultInterval || isNaN(defaultInterval)) { + LOG("TimerManager:notify - update-timer category registered" + + (cid ? " for " + cid : "") + " without required parameters - " + + "skipping"); + continue; + } + + let interval = Services.prefs.getIntPref(prefInterval, defaultInterval); + // Allow the update-timer category to specify a maximum value to prevent + // values larger than desired. + maxInterval = parseInt(maxInterval); + if (maxInterval && !isNaN(maxInterval)) { + interval = Math.min(interval, maxInterval); + } + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, + timerID); + // Initialize the last update time to 0 when the preference isn't set so + // the timer will be notified soon after a new profile's first use. + lastUpdateTime = Services.prefs.getIntPref(prefLastUpdate, 0); + + // If the last update time is greater than the current time then reset + // it to 0 and the timer manager will correct the value when it fires + // next for this consumer. + if (lastUpdateTime > now) { + lastUpdateTime = 0; + } + + if (lastUpdateTime == 0) { + Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); + } + + tryFire(function () { + try { + Components.classes[cid][method](Ci.nsITimerCallback).notify(timer); + LOG("TimerManager:notify - notified " + cid); + } catch (e) { + LOG("TimerManager:notify - error notifying component id: " + + cid + " ,error: " + e); + } + lastUpdateTime = now; + Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); + updateNextDelay(lastUpdateTime + interval - now); + }, lastUpdateTime + interval); + } + + for (let _timerID in this._timers) { + let timerID = _timerID; // necessary for the closure to work properly + let timerData = this._timers[timerID]; + // If the last update time is greater than the current time then reset + // it to 0 and the timer manager will correct the value when it fires + // next for this consumer. + if (timerData.lastUpdateTime > now) { + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, timerID); + timerData.lastUpdateTime = 0; + Services.prefs.setIntPref(prefLastUpdate, timerData.lastUpdateTime); + } + tryFire(function () { + if (timerData.callback && timerData.callback.notify) { + try { + timerData.callback.notify(timer); + LOG("TimerManager:notify - notified timerID: " + timerID); + } catch (e) { + LOG("TimerManager:notify - error notifying timerID: " + timerID + + ", error: " + e); + } + } else { + LOG("TimerManager:notify - timerID: " + timerID + " doesn't " + + "implement nsITimerCallback - skipping"); + } + lastUpdateTime = now; + timerData.lastUpdateTime = lastUpdateTime; + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, timerID); + Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); + updateNextDelay(timerData.lastUpdateTime + timerData.interval - now); + }, timerData.lastUpdateTime + timerData.interval); + } + + if (callbackToFire) { + callbackToFire(); + } + + if (nextDelay !== null) { + if (skippedFirings) { + timer.delay = this._timerMinimumDelay; + } else { + timer.delay = Math.max(nextDelay * 1000, this._timerMinimumDelay); + } + this.lastTimerReset = Date.now(); + } else { + this._cancelTimer(); + } + }, + + /** + * Starts the timer, if necessary, and ensures that it will fire soon enough + * to happen after time |interval| (in milliseconds). + */ + _ensureTimer: function (interval) { + if (!this._canEnsureTimer) { + return; + } + if (!this._timer) { + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._timer.initWithCallback(this, interval, + Ci.nsITimer.TYPE_REPEATING_SLACK); + this.lastTimerReset = Date.now(); + } else if (Date.now() + interval < this.lastTimerReset + this._timer.delay) { + this._timer.delay = Math.max(this.lastTimerReset + interval - Date.now(), 0); + } + }, + + /** + * Stops the timer, if it is running. + */ + _cancelTimer: function () { + if (this._timer) { + this._timer.cancel(); + this._timer = null; + } + }, + + /** + * See nsIUpdateTimerManager.idl + */ + registerTimer: function TM_registerTimer(id, callback, interval) { + LOG("TimerManager:registerTimer - id: " + id); + if (id in this._timers && callback != this._timers[id].callback) { + LOG("TimerManager:registerTimer - Ignoring second registration for " + id); + return; + } + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, id); + // Initialize the last update time to 0 when the preference isn't set so + // the timer will be notified soon after a new profile's first use. + let lastUpdateTime = Services.prefs.getIntPref(prefLastUpdate, 0); + let now = Math.round(Date.now() / 1000); + if (lastUpdateTime > now) { + lastUpdateTime = 0; + } + if (lastUpdateTime == 0) { + Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); + } + this._timers[id] = {callback: callback, + interval: interval, + lastUpdateTime: lastUpdateTime}; + + this._ensureTimer(interval * 1000); + }, + + classID: Components.ID("{B322A5C0-A419-484E-96BA-D7182163899F}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateTimerManager, + Ci.nsITimerCallback, + Ci.nsIObserver]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TimerManager]); diff --git a/components/timermanager/nsUpdateTimerManager.manifest b/components/timermanager/nsUpdateTimerManager.manifest new file mode 100644 index 000000000..73cb11e35 --- /dev/null +++ b/components/timermanager/nsUpdateTimerManager.manifest @@ -0,0 +1,3 @@ +component {B322A5C0-A419-484E-96BA-D7182163899F} nsUpdateTimerManager.js +contract @mozilla.org/updates/timer-manager;1 {B322A5C0-A419-484E-96BA-D7182163899F} +category profile-after-change nsUpdateTimerManager @mozilla.org/updates/timer-manager;1 |