summaryrefslogtreecommitdiff
path: root/components/timermanager
diff options
context:
space:
mode:
Diffstat (limited to 'components/timermanager')
-rw-r--r--components/timermanager/moz.build13
-rw-r--r--components/timermanager/nsIUpdateTimerManager.idl54
-rw-r--r--components/timermanager/nsUpdateTimerManager.js319
-rw-r--r--components/timermanager/nsUpdateTimerManager.manifest3
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