diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/timermanager | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/timermanager')
7 files changed, 1286 insertions, 0 deletions
diff --git a/toolkit/components/timermanager/UpdateTimerManager.jsm b/toolkit/components/timermanager/UpdateTimerManager.jsm new file mode 100644 index 0000000000..7747c99a06 --- /dev/null +++ b/toolkit/components/timermanager/UpdateTimerManager.jsm @@ -0,0 +1,411 @@ +/* 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/. */ + +const { XPCOMUtils } = ChromeUtils.importESModule( + "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 + */ +function TimerManager() { + Services.obs.addObserver(this, "profile-before-change"); +} +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; + // 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 callbackToFire = null; + var earliestIntendedTime = null; + var skippedFirings = false; + var lastUpdateTime = null; + var timerIDToFire = null; + function tryFire(timerID, callback, intendedTime) { + var selected = false; + if (intendedTime <= now) { + if ( + intendedTime < earliestIntendedTime || + earliestIntendedTime === null + ) { + timerIDToFire = timerID; + 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); + } + } + + 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. + 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( + timerID, + function() { + try { + Cc[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( + timerID, + function() { + if (timerData.callback && timerData.callback.notify) { + ChromeUtils.idleDispatch(() => { + 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) { + LOG( + `TimerManager:notify - fire timerID: ${timerIDToFire} ` + + `intended time: ${earliestIntendedTime} (${new Date( + earliestIntendedTime * 1000 + ).toISOString()})` + ); + 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(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) { + 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) { + 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([ + "nsIUpdateTimerManager", + "nsITimerCallback", + "nsIObserver", + ]), +}; + +var EXPORTED_SYMBOLS = ["TimerManager"]; diff --git a/toolkit/components/timermanager/components.conf b/toolkit/components/timermanager/components.conf new file mode 100644 index 0000000000..e91bfa6811 --- /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'], + 'jsm': 'resource://gre/modules/UpdateTimerManager.jsm', + '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..bb3003c224 --- /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.ini"] + +XPIDL_SOURCES += [ + "nsIUpdateTimerManager.idl", +] + +EXTRA_JS_MODULES += [ + "UpdateTimerManager.jsm", +] + +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..2ebdc1d2d9 --- /dev/null +++ b/toolkit/components/timermanager/tests/unit/consumerNotifications.js @@ -0,0 +1,721 @@ +/* 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: 86400, + prefInterval: "test1.timer.interval", + contractID: "@mozilla.org/test2/timercallback;1", + method: "createInstance", + classID: Components.ID("512834f3-05bb-46be-84e0-81d881a140b7"), + notified: false, + }, + { + desc: "Test Timer Callback 2", + timerID: "test2-update-timer", + defaultInterval: CONSUMER_TIMER_INTERVAL, + 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; + +XPCOMUtils.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(gTest9TimerCallback); + }); + }, + 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 'staggering' has happened: even though the two events wanted to fire at + // the same time, we waited a full MAIN_TIMER_INTERVAL between them. + // (to avoid sensitivity to random timing issues, we fudge by a factor of 0.5 here) + Assert.ok( + Math.abs(TESTS[8].notifyTime - TESTS[9].notifyTime) >= + MAIN_TIMER_INTERVAL * 0.5, + "staggering between two timers that want to fire at the same " + + "time should have occured" + ); + + 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..bfdaa40de4 --- /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.ini b/toolkit/components/timermanager/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..423eb51ae8 --- /dev/null +++ b/toolkit/components/timermanager/tests/unit/xpcshell.ini @@ -0,0 +1,9 @@ +# 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/. + +[DEFAULT] +head = + +[consumerNotifications.js] +[test_skipFirst.js] |