summaryrefslogtreecommitdiffstats
path: root/toolkit/components/timermanager
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/components/timermanager/UpdateTimerManager.sys.mjs408
-rw-r--r--toolkit/components/timermanager/components.conf15
-rw-r--r--toolkit/components/timermanager/moz.build24
-rw-r--r--toolkit/components/timermanager/nsIUpdateTimerManager.idl62
-rw-r--r--toolkit/components/timermanager/tests/unit/consumerNotifications.js720
-rw-r--r--toolkit/components/timermanager/tests/unit/test_skipFirst.js44
-rw-r--r--toolkit/components/timermanager/tests/unit/xpcshell.toml6
7 files changed, 1279 insertions, 0 deletions
diff --git a/toolkit/components/timermanager/UpdateTimerManager.sys.mjs b/toolkit/components/timermanager/UpdateTimerManager.sys.mjs
new file mode 100644
index 0000000000..607a3b0e71
--- /dev/null
+++ b/toolkit/components/timermanager/UpdateTimerManager.sys.mjs
@@ -0,0 +1,408 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+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";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "gLogEnabled",
+ PREF_APP_UPDATE_LOG,
+ false
+);
+
+/**
+ * Logs a string to the error console.
+ * @param string
+ * The string to write to the error console.
+ * @param bool
+ * Whether to log even if logging is disabled.
+ */
+function LOG(string, alwaysLog = false) {
+ if (alwaysLog || lazy.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
+ */
+export function TimerManager() {
+ Services.obs.addObserver(this, "profile-before-change");
+}
+
+TimerManager.prototype = {
+ /**
+ * nsINamed
+ */
+ name: "UpdateTimerManager",
+
+ /**
+ * 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;
+ // fall through
+ 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 "profile-before-change":
+ Services.obs.removeObserver(this, "profile-before-change");
+
+ // 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 callbacksToFire = [];
+ function tryFire(timerID, callback, intendedTime) {
+ if (intendedTime <= now) {
+ callbacksToFire.push({ timerID, callback, intendedTime });
+ } else {
+ updateNextDelay(intendedTime - now);
+ }
+ }
+
+ for (let { value } of Services.catMan.enumerateCategory(
+ CATEGORY_UPDATE_TIMER
+ )) {
+ 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.
+ let 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;
+ Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
+ }
+
+ tryFire(
+ timerID,
+ function () {
+ ChromeUtils.idleDispatch(() => {
+ try {
+ let startTime = Cu.now();
+ Cc[cid][method](Ci.nsITimerCallback).notify(timer);
+ ChromeUtils.addProfilerMarker(
+ "UpdateTimer",
+ { category: "Timer", startTime },
+ timerID
+ );
+ LOG("TimerManager:notify - notified " + cid);
+ } catch (e) {
+ LOG(
+ "TimerManager:notify - error notifying component id: " +
+ cid +
+ " ,error: " +
+ e
+ );
+ }
+ });
+ Services.prefs.setIntPref(prefLastUpdate, now);
+ updateNextDelay(interval);
+ },
+ 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(
+ timerID,
+ function () {
+ if (timerData.callback && timerData.callback.notify) {
+ ChromeUtils.idleDispatch(() => {
+ try {
+ let startTime = Cu.now();
+ timerData.callback.notify(timer);
+ ChromeUtils.addProfilerMarker(
+ "UpdateTimer",
+ { category: "Timer", startTime },
+ timerID
+ );
+ 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`
+ );
+ }
+ timerData.lastUpdateTime = now;
+ let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(
+ /%ID%/,
+ timerID
+ );
+ Services.prefs.setIntPref(prefLastUpdate, now);
+ updateNextDelay(timerData.interval);
+ },
+ timerData.lastUpdateTime + timerData.interval
+ );
+ }
+
+ if (callbacksToFire.length) {
+ callbacksToFire.sort((a, b) => a.intendedTime - b.intendedTime);
+ for (let { intendedTime, timerID, callback } of callbacksToFire) {
+ LOG(
+ `TimerManager:notify - fire timerID: ${timerID} ` +
+ `intended time: ${intendedTime} (${new Date(
+ intendedTime * 1000
+ ).toISOString()})`
+ );
+ callback();
+ }
+ }
+
+ if (nextDelay !== null) {
+ 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(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() {
+ if (this._timer) {
+ this._timer.cancel();
+ this._timer = null;
+ }
+ },
+
+ /**
+ * See nsIUpdateTimerManager.idl
+ */
+ registerTimer: function TM_registerTimer(id, callback, interval, skipFirst) {
+ let markerText = `timerID: ${id} interval: ${interval}s`;
+ if (skipFirst) {
+ markerText += " skipFirst";
+ }
+ ChromeUtils.addProfilerMarker(
+ "RegisterUpdateTimer",
+ { category: "Timer" },
+ markerText
+ );
+ LOG(
+ `TimerManager:registerTimer - timerID: ${id} interval: ${interval} skipFirst: ${skipFirst}`
+ );
+ if (this._timers === null) {
+ // Use normal logging since reportError is not available while shutting
+ // down.
+ LOG(
+ "TimerManager:registerTimer called after profile-before-change " +
+ "notification. Ignoring timer registration for id: " +
+ id,
+ true
+ );
+ return;
+ }
+ 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) {
+ if (skipFirst) {
+ lastUpdateTime = now;
+ }
+ Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
+ }
+ this._timers[id] = { callback, interval, lastUpdateTime };
+
+ this._ensureTimer(interval * 1000);
+ },
+
+ unregisterTimer: function TM_unregisterTimer(id) {
+ ChromeUtils.addProfilerMarker(
+ "UnregisterUpdateTimer",
+ { category: "Timer" },
+ id
+ );
+ LOG("TimerManager:unregisterTimer - id: " + id);
+ if (id in this._timers) {
+ delete this._timers[id];
+ } else {
+ LOG(
+ "TimerManager:unregisterTimer - Ignoring unregistration request for " +
+ "unknown id: " +
+ id
+ );
+ }
+ },
+
+ classID: Components.ID("{B322A5C0-A419-484E-96BA-D7182163899F}"),
+ QueryInterface: ChromeUtils.generateQI([
+ "nsINamed",
+ "nsIObserver",
+ "nsITimerCallback",
+ "nsIUpdateTimerManager",
+ ]),
+};
diff --git a/toolkit/components/timermanager/components.conf b/toolkit/components/timermanager/components.conf
new file mode 100644
index 0000000000..c4356477c6
--- /dev/null
+++ b/toolkit/components/timermanager/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{B322A5C0-A419-484E-96BA-D7182163899F}',
+ 'contract_ids': ['@mozilla.org/updates/timer-manager;1'],
+ 'esModule': 'resource://gre/modules/UpdateTimerManager.sys.mjs',
+ 'constructor': 'TimerManager',
+ 'categories': {'profile-after-change': 'nsUpdateTimerManager'},
+ },
+]
diff --git a/toolkit/components/timermanager/moz.build b/toolkit/components/timermanager/moz.build
new file mode 100644
index 0000000000..8bd0914cd9
--- /dev/null
+++ b/toolkit/components/timermanager/moz.build
@@ -0,0 +1,24 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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"
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.toml"]
+
+XPIDL_SOURCES += [
+ "nsIUpdateTimerManager.idl",
+]
+
+EXTRA_JS_MODULES += [
+ "UpdateTimerManager.sys.mjs",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Application Update")
diff --git a/toolkit/components/timermanager/nsIUpdateTimerManager.idl b/toolkit/components/timermanager/nsIUpdateTimerManager.idl
new file mode 100644
index 0000000000..c0ac4266f6
--- /dev/null
+++ b/toolkit/components/timermanager/nsIUpdateTimerManager.idl
@@ -0,0 +1,62 @@
+/* -*- 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. The callback will be
+ * called soon after the first registration unless you ask to skip it.
+ * @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
+ * @param skipFirst
+ * Whether to skip the initial callback on first registration.
+ *
+ * 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:
+ *
+ * contractID,method,id,preference,interval
+ *
+ * via a manifest entry. 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,
+ [optional] in boolean skipFirst);
+
+ /**
+ * Unregister an existing interval from the timer manager.
+ *
+ * @param id
+ * An id that identifies the interval.
+ */
+ void unregisterTimer(in AString id);
+};
diff --git a/toolkit/components/timermanager/tests/unit/consumerNotifications.js b/toolkit/components/timermanager/tests/unit/consumerNotifications.js
new file mode 100644
index 0000000000..1e09207043
--- /dev/null
+++ b/toolkit/components/timermanager/tests/unit/consumerNotifications.js
@@ -0,0 +1,720 @@
+/* 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/.
+ */
+
+/* General Update Timer Manager Tests */
+
+"use strict";
+
+const Cm = Components.manager;
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const CATEGORY_UPDATE_TIMER = "update-timer";
+
+const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay";
+const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval";
+const PREF_APP_UPDATE_LOG_ALL = "app.update.log.all";
+const PREF_BRANCH_LAST_UPDATE_TIME = "app.update.lastUpdateTime.";
+
+const MAIN_TIMER_INTERVAL = 1000; // milliseconds
+const CONSUMER_TIMER_INTERVAL = 1; // seconds
+
+const TESTS = [
+ {
+ desc: "Test Timer Callback 0",
+ timerID: "test0-update-timer",
+ defaultInterval: "bogus",
+ prefInterval: "test0.timer.interval",
+ contractID: "@mozilla.org/test0/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("9c7ce81f-98bb-4729-adb4-4d0deb0f59e5"),
+ notified: false,
+ },
+ {
+ desc: "Test Timer Callback 1",
+ timerID: "test1-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ prefInterval: "test1.timer.interval",
+ contractID: "@mozilla.org/test1/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("512834f3-05bb-46be-84e0-81d881a140b7"),
+ notified: false,
+ },
+ {
+ desc: "Test Timer Callback 2",
+ timerID: "test2-update-timer",
+ defaultInterval: 86400,
+ prefInterval: "test2.timer.interval",
+ contractID: "@mozilla.org/test2/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("c8ac5027-8d11-4471-9d7c-fd692501b437"),
+ notified: false,
+ },
+ {
+ desc: "Test Timer Callback 3",
+ timerID: "test3-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ prefInterval: "test3.timer.interval",
+ contractID: "@mozilla.org/test3/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("6b0e79f3-4ab8-414c-8f14-dde10e185727"),
+ notified: false,
+ },
+ {
+ desc: "Test Timer Callback 4",
+ timerID: "test4-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ prefInterval: "test4.timer.interval",
+ contractID: "@mozilla.org/test4/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("2f6b7b92-e40f-4874-bfbb-eeb2412c959d"),
+ notified: false,
+ },
+ {
+ desc: "Test Timer Callback 5",
+ timerID: "test5-update-timer",
+ defaultInterval: 86400,
+ prefInterval: "test5.timer.interval",
+ contractID: "@mozilla.org/test5/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("8a95f611-b2ac-4c7e-8b73-9748c4839731"),
+ notified: false,
+ },
+ {
+ desc: "Test Timer Callback 6",
+ timerID: "test6-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ prefInterval: "test6.timer.interval",
+ contractID: "@mozilla.org/test6/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("2d091020-e23c-11e2-a28f-0800200c9a66"),
+ notified: false,
+ },
+ {
+ desc: "Test Timer Callback 7",
+ timerID: "test7-update-timer",
+ defaultInterval: 86400,
+ maxInterval: CONSUMER_TIMER_INTERVAL,
+ prefInterval: "test7.timer.interval",
+ contractID: "@mozilla.org/test7/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("8e8633ae-1d70-4a7a-8bea-6e1e6c5d7742"),
+ notified: false,
+ },
+ {
+ desc: "Test Timer Callback 8",
+ timerID: "test8-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ contractID: "@mozilla.org/test8/timercallback;1",
+ classID: Components.ID("af878d4b-1d12-41f6-9a90-4e687367ecc1"),
+ notified: false,
+ lastUpdateTime: 0,
+ },
+ {
+ desc: "Test Timer Callback 9",
+ timerID: "test9-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ contractID: "@mozilla.org/test9/timercallback;1",
+ classID: Components.ID("5136b201-d64c-4328-8cf1-1a63491cc117"),
+ notified: false,
+ lastUpdateTime: 0,
+ },
+ {
+ desc: "Test Timer Callback 10",
+ timerID: "test10-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ contractID: "@mozilla.org/test9/timercallback;1",
+ classID: Components.ID("1f42bbb3-d116-4012-8491-3ec4797a97ee"),
+ notified: false,
+ lastUpdateTime: 0,
+ },
+];
+
+var gUTM;
+var gNextFunc;
+
+ChromeUtils.defineLazyGetter(this, "gCompReg", function () {
+ return Cm.QueryInterface(Ci.nsIComponentRegistrar);
+});
+
+const gTest0TimerCallback = {
+ notify: function T0CB_notify(aTimer) {
+ // This can happen when another notification fails and this timer having
+ // time to fire so check other timers are successful.
+ do_throw("gTest0TimerCallback notify method should not have been called");
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+};
+
+const gTest0Factory = {
+ createInstance: function T0F_createInstance(aIID) {
+ return gTest0TimerCallback.QueryInterface(aIID);
+ },
+};
+
+const gTest1TimerCallback = {
+ notify: function T1CB_notify(aTimer) {
+ // This can happen when another notification fails and this timer having
+ // time to fire so check other timers are successful.
+ do_throw("gTest1TimerCallback notify method should not have been called");
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimer"]),
+};
+
+const gTest1Factory = {
+ createInstance: function T1F_createInstance(aIID) {
+ return gTest1TimerCallback.QueryInterface(aIID);
+ },
+};
+
+const gTest2TimerCallback = {
+ notify: function T2CB_notify(aTimer) {
+ // This can happen when another notification fails and this timer having
+ // time to fire so check other timers are successful.
+ do_throw("gTest2TimerCallback notify method should not have been called");
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+};
+
+const gTest2Factory = {
+ createInstance: function T2F_createInstance(aIID) {
+ return gTest2TimerCallback.QueryInterface(aIID);
+ },
+};
+
+const gTest3TimerCallback = {
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+};
+
+const gTest3Factory = {
+ createInstance: function T3F_createInstance(aIID) {
+ return gTest3TimerCallback.QueryInterface(aIID);
+ },
+};
+
+const gTest4TimerCallback = {
+ notify: function T4CB_notify(aTimer) {
+ Services.catMan.deleteCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[4].desc,
+ true
+ );
+ TESTS[4].notified = true;
+ finished_test0thru7();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+};
+
+const gTest4Factory = {
+ createInstance: function T4F_createInstance(aIID) {
+ return gTest4TimerCallback.QueryInterface(aIID);
+ },
+};
+
+const gTest5TimerCallback = {
+ notify: function T5CB_notify(aTimer) {
+ Services.catMan.deleteCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[5].desc,
+ true
+ );
+ TESTS[5].notified = true;
+ finished_test0thru7();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+};
+
+const gTest5Factory = {
+ createInstance: function T5F_createInstance(aIID) {
+ return gTest5TimerCallback.QueryInterface(aIID);
+ },
+};
+
+const gTest6TimerCallback = {
+ notify: function T6CB_notify(aTimer) {
+ Services.catMan.deleteCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[6].desc,
+ true
+ );
+ TESTS[6].notified = true;
+ finished_test0thru7();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+};
+
+const gTest6Factory = {
+ createInstance: function T6F_createInstance(aIID) {
+ return gTest6TimerCallback.QueryInterface(aIID);
+ },
+};
+
+const gTest7TimerCallback = {
+ notify: function T7CB_notify(aTimer) {
+ Services.catMan.deleteCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[7].desc,
+ true
+ );
+ TESTS[7].notified = true;
+ finished_test0thru7();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+};
+
+const gTest7Factory = {
+ createInstance: function T7F_createInstance(aIID) {
+ return gTest7TimerCallback.QueryInterface(aIID);
+ },
+};
+
+const gTest8TimerCallback = {
+ notify: function T8CB_notify(aTimer) {
+ TESTS[8].notified = true;
+ TESTS[8].notifyTime = Date.now();
+ executeSoon(function () {
+ check_test8thru10(gTest8TimerCallback);
+ });
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+};
+
+const gTest8Factory = {
+ createInstance: function T8F_createInstance(aIID) {
+ return gTest8TimerCallback.QueryInterface(aIID);
+ },
+};
+
+const gTest9TimerCallback = {
+ notify: function T9CB_notify(aTimer) {
+ TESTS[9].notified = true;
+ TESTS[9].notifyTime = Date.now();
+ executeSoon(function () {
+ check_test8thru10(gTest8TimerCallback);
+ });
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+};
+
+const gTest10TimerCallback = {
+ notify: function T9CB_notify(aTimer) {
+ // The timer should have been unregistered before this could
+ // be called.
+ do_throw("gTest10TimerCallback notify method should not have been called");
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+};
+
+const gTest9Factory = {
+ createInstance: function T9F_createInstance(aIID) {
+ return gTest9TimerCallback.QueryInterface(aIID);
+ },
+};
+
+function run_test() {
+ do_test_pending();
+
+ // Set the timer to fire every second
+ Services.prefs.setIntPref(
+ PREF_APP_UPDATE_TIMERMINIMUMDELAY,
+ MAIN_TIMER_INTERVAL / 1000
+ );
+ Services.prefs.setIntPref(
+ PREF_APP_UPDATE_TIMERFIRSTINTERVAL,
+ MAIN_TIMER_INTERVAL
+ );
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG_ALL, true);
+
+ // Remove existing update timers to prevent them from being notified
+ for (let { data: entry } of Services.catMan.enumerateCategory(
+ CATEGORY_UPDATE_TIMER
+ )) {
+ Services.catMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, entry, false);
+ }
+
+ gUTM = Cc["@mozilla.org/updates/timer-manager;1"]
+ .getService(Ci.nsIUpdateTimerManager)
+ .QueryInterface(Ci.nsIObserver);
+ gUTM.observe(null, "utm-test-init", "");
+
+ executeSoon(run_test0thru7);
+}
+
+function end_test() {
+ gUTM.observe(null, "profile-before-change", "");
+ do_test_finished();
+}
+
+function run_test0thru7() {
+ gNextFunc = check_test0thru7;
+ // bogus default interval
+ gCompReg.registerFactory(
+ TESTS[0].classID,
+ TESTS[0].desc,
+ TESTS[0].contractID,
+ gTest0Factory
+ );
+ Services.catMan.addCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[0].desc,
+ [
+ TESTS[0].contractID,
+ TESTS[0].method,
+ TESTS[0].timerID,
+ TESTS[0].prefInterval,
+ TESTS[0].defaultInterval,
+ ].join(","),
+ false,
+ true
+ );
+
+ // doesn't implement nsITimerCallback
+ gCompReg.registerFactory(
+ TESTS[1].classID,
+ TESTS[1].desc,
+ TESTS[1].contractID,
+ gTest1Factory
+ );
+ Services.catMan.addCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[1].desc,
+ [
+ TESTS[1].contractID,
+ TESTS[1].method,
+ TESTS[1].timerID,
+ TESTS[1].prefInterval,
+ TESTS[1].defaultInterval,
+ ].join(","),
+ false,
+ true
+ );
+
+ // has a last update time of now - 43200 which is half of its interval
+ let lastUpdateTime = Math.round(Date.now() / 1000) - 43200;
+ Services.prefs.setIntPref(
+ PREF_BRANCH_LAST_UPDATE_TIME + TESTS[2].timerID,
+ lastUpdateTime
+ );
+ gCompReg.registerFactory(
+ TESTS[2].classID,
+ TESTS[2].desc,
+ TESTS[2].contractID,
+ gTest2Factory
+ );
+ Services.catMan.addCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[2].desc,
+ [
+ TESTS[2].contractID,
+ TESTS[2].method,
+ TESTS[2].timerID,
+ TESTS[2].prefInterval,
+ TESTS[2].defaultInterval,
+ ].join(","),
+ false,
+ true
+ );
+
+ // doesn't have a notify method
+ gCompReg.registerFactory(
+ TESTS[3].classID,
+ TESTS[3].desc,
+ TESTS[3].contractID,
+ gTest3Factory
+ );
+ Services.catMan.addCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[3].desc,
+ [
+ TESTS[3].contractID,
+ TESTS[3].method,
+ TESTS[3].timerID,
+ TESTS[3].prefInterval,
+ TESTS[3].defaultInterval,
+ ].join(","),
+ false,
+ true
+ );
+
+ // already has a last update time
+ Services.prefs.setIntPref(PREF_BRANCH_LAST_UPDATE_TIME + TESTS[4].timerID, 1);
+ gCompReg.registerFactory(
+ TESTS[4].classID,
+ TESTS[4].desc,
+ TESTS[4].contractID,
+ gTest4Factory
+ );
+ Services.catMan.addCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[4].desc,
+ [
+ TESTS[4].contractID,
+ TESTS[4].method,
+ TESTS[4].timerID,
+ TESTS[4].prefInterval,
+ TESTS[4].defaultInterval,
+ ].join(","),
+ false,
+ true
+ );
+
+ // has an interval preference that overrides the default
+ Services.prefs.setIntPref(TESTS[5].prefInterval, CONSUMER_TIMER_INTERVAL);
+ gCompReg.registerFactory(
+ TESTS[5].classID,
+ TESTS[5].desc,
+ TESTS[5].contractID,
+ gTest5Factory
+ );
+ Services.catMan.addCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[5].desc,
+ [
+ TESTS[5].contractID,
+ TESTS[5].method,
+ TESTS[5].timerID,
+ TESTS[5].prefInterval,
+ TESTS[5].defaultInterval,
+ ].join(","),
+ false,
+ true
+ );
+
+ // has a next update time 24 hours from now
+ let nextUpdateTime = Math.round(Date.now() / 1000) + 86400;
+ Services.prefs.setIntPref(
+ PREF_BRANCH_LAST_UPDATE_TIME + TESTS[6].timerID,
+ nextUpdateTime
+ );
+ gCompReg.registerFactory(
+ TESTS[6].classID,
+ TESTS[6].desc,
+ TESTS[6].contractID,
+ gTest6Factory
+ );
+ Services.catMan.addCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[6].desc,
+ [
+ TESTS[6].contractID,
+ TESTS[6].method,
+ TESTS[6].timerID,
+ TESTS[6].prefInterval,
+ TESTS[6].defaultInterval,
+ ].join(","),
+ false,
+ true
+ );
+
+ // has a maximum interval set by the value of MAIN_TIMER_INTERVAL
+ Services.prefs.setIntPref(TESTS[7].prefInterval, 86400);
+ gCompReg.registerFactory(
+ TESTS[7].classID,
+ TESTS[7].desc,
+ TESTS[7].contractID,
+ gTest7Factory
+ );
+ Services.catMan.addCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[7].desc,
+ [
+ TESTS[7].contractID,
+ TESTS[7].method,
+ TESTS[7].timerID,
+ TESTS[7].prefInterval,
+ TESTS[7].defaultInterval,
+ TESTS[7].maxInterval,
+ ].join(","),
+ false,
+ true
+ );
+}
+
+function finished_test0thru7() {
+ if (
+ TESTS[4].notified &&
+ TESTS[5].notified &&
+ TESTS[6].notified &&
+ TESTS[7].notified
+ ) {
+ executeSoon(gNextFunc);
+ }
+}
+
+function check_test0thru7() {
+ Assert.ok(
+ !TESTS[0].notified,
+ "a category registered timer didn't fire due to an invalid " +
+ "default interval"
+ );
+
+ Assert.ok(
+ !TESTS[1].notified,
+ "a category registered timer didn't fire due to not implementing " +
+ "nsITimerCallback"
+ );
+
+ Assert.ok(
+ !TESTS[2].notified,
+ "a category registered timer didn't fire due to the next update " +
+ "time being in the future"
+ );
+
+ Assert.ok(
+ !TESTS[3].notified,
+ "a category registered timer didn't fire due to not having a " +
+ "notify method"
+ );
+
+ Assert.ok(TESTS[4].notified, "a category registered timer has fired");
+
+ Assert.ok(
+ TESTS[5].notified,
+ "a category registered timer fired that has an interval " +
+ "preference that overrides a default that wouldn't have fired yet"
+ );
+
+ Assert.ok(
+ TESTS[6].notified,
+ "a category registered timer has fired due to the next update " +
+ "time being reset due to a future last update time"
+ );
+
+ Assert.ok(
+ Services.prefs.prefHasUserValue(
+ PREF_BRANCH_LAST_UPDATE_TIME + TESTS[4].timerID
+ ),
+ "first of two category registered timers last update time has " +
+ "a user value"
+ );
+ Assert.ok(
+ Services.prefs.prefHasUserValue(
+ PREF_BRANCH_LAST_UPDATE_TIME + TESTS[5].timerID
+ ),
+ "second of two category registered timers last update time has " +
+ "a user value"
+ );
+
+ // Remove the category timers that should have failed
+ Services.catMan.deleteCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[0].desc,
+ true
+ );
+ Services.catMan.deleteCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[1].desc,
+ true
+ );
+ Services.catMan.deleteCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[2].desc,
+ true
+ );
+ Services.catMan.deleteCategoryEntry(
+ CATEGORY_UPDATE_TIMER,
+ TESTS[3].desc,
+ true
+ );
+ for (let { data: entry } of Services.catMan.enumerateCategory(
+ CATEGORY_UPDATE_TIMER
+ )) {
+ Services.catMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, entry, false);
+ }
+
+ let entries = Services.catMan.enumerateCategory(CATEGORY_UPDATE_TIMER);
+ Assert.ok(
+ !entries.hasMoreElements(),
+ "no " +
+ CATEGORY_UPDATE_TIMER +
+ " categories should still be " +
+ "registered"
+ );
+
+ executeSoon(run_test8thru10);
+}
+
+function run_test8thru10() {
+ Services.prefs.setIntPref(PREF_BRANCH_LAST_UPDATE_TIME + TESTS[8].timerID, 1);
+ gCompReg.registerFactory(
+ TESTS[8].classID,
+ TESTS[8].desc,
+ TESTS[8].contractID,
+ gTest8Factory
+ );
+ gUTM.registerTimer(
+ TESTS[8].timerID,
+ gTest8TimerCallback,
+ TESTS[8].defaultInterval
+ );
+ Services.prefs.setIntPref(PREF_BRANCH_LAST_UPDATE_TIME + TESTS[9].timerID, 1);
+ gCompReg.registerFactory(
+ TESTS[9].classID,
+ TESTS[9].desc,
+ TESTS[9].contractID,
+ gTest9Factory
+ );
+ gUTM.registerTimer(
+ TESTS[9].timerID,
+ gTest9TimerCallback,
+ TESTS[9].defaultInterval
+ );
+ gUTM.registerTimer(
+ TESTS[10].timerID,
+ gTest10TimerCallback,
+ TESTS[10].defaultInterval
+ );
+ gUTM.unregisterTimer(TESTS[10].timerID);
+}
+
+function check_test8thru10(aTestTimerCallback) {
+ aTestTimerCallback.timesCalled = (aTestTimerCallback.timesCalled || 0) + 1;
+ if (aTestTimerCallback.timesCalled < 2) {
+ return;
+ }
+
+ Assert.ok(
+ TESTS[8].notified,
+ "first registerTimer registered timer should have fired"
+ );
+
+ Assert.ok(
+ TESTS[9].notified,
+ "second registerTimer registered timer should have fired"
+ );
+
+ // Check that the two events that wanted to fire at the same time
+ // happened in the expected order.
+ Assert.lessOrEqual(
+ TESTS[8].notifyTime,
+ TESTS[9].notifyTime,
+ "two timers that want to fire at the same " +
+ "should fire in the expected order"
+ );
+
+ let time = Services.prefs.getIntPref(
+ PREF_BRANCH_LAST_UPDATE_TIME + TESTS[8].timerID
+ );
+ Assert.notEqual(
+ time,
+ 1,
+ "first registerTimer registered timer last update time " +
+ "should have been updated"
+ );
+
+ time = Services.prefs.getIntPref(
+ PREF_BRANCH_LAST_UPDATE_TIME + TESTS[9].timerID
+ );
+ Assert.notEqual(
+ time,
+ 1,
+ "second registerTimer registered timer last update time " +
+ "should have been updated"
+ );
+
+ end_test();
+}
diff --git a/toolkit/components/timermanager/tests/unit/test_skipFirst.js b/toolkit/components/timermanager/tests/unit/test_skipFirst.js
new file mode 100644
index 0000000000..fec1a795ed
--- /dev/null
+++ b/toolkit/components/timermanager/tests/unit/test_skipFirst.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gUpdateTimerManager",
+ "@mozilla.org/updates/timer-manager;1",
+ "nsIUpdateTimerManager"
+);
+
+const PREF_APP_UPDATE_LASTUPDATETIME_FMT = "app.update.lastUpdateTime.%ID%";
+
+add_task(async function () {
+ const testId = "test_timer_id";
+ const testPref = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, testId);
+ const testInterval = 100000000; // Just needs to be longer than the test.
+
+ Services.prefs.clearUserPref(testPref);
+ gUpdateTimerManager.registerTimer(
+ testId,
+ {},
+ testInterval,
+ true /* skipFirst */
+ );
+ let prefValue = Services.prefs.getIntPref(testPref, 0);
+ Assert.notEqual(
+ prefValue,
+ 0,
+ "Last update time for test timer must not be 0."
+ );
+ let nowSeconds = Date.now() / 1000; // update timer lastUpdate prefs are set in seconds.
+ Assert.ok(
+ Math.abs(nowSeconds - prefValue) < 2,
+ "Last update time for test timer must be now-ish."
+ );
+
+ gUpdateTimerManager.unregisterTimer(testId);
+});
diff --git a/toolkit/components/timermanager/tests/unit/xpcshell.toml b/toolkit/components/timermanager/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..87b7822327
--- /dev/null
+++ b/toolkit/components/timermanager/tests/unit/xpcshell.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+head = ""
+
+["consumerNotifications.js"]
+
+["test_skipFirst.js"]