From a6f77491219d6d6521b135e336e61bef523c3b5b Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Tue, 9 Jun 2009 19:28:11 +0000 Subject: [PATCH] Moved the checkForTimeAdjustment() method in the generic time source behind the WaitOne() call because that's the most likely point for a date/time adjustment will have occurred at; WindowsTimeSource now exists on the XBox 360 as well, but will throw in its constructor and report Available as false (this simplifies things in other places); Scheduler class is fully working and 100% testable with simulated time (instead of waiting for scheduled notifications to be delivered in real time, which would make testing slow); added unit tests for Scheduler class and reached 100% coverage git-svn-id: file:///srv/devel/repo-conversion/nusu@145 d2e56fa2-650e-0410-a79f-9358c0239efd --- Source/Scheduling/GenericTimeSource.cs | 10 +- Source/Scheduling/Scheduler.Test.cs | 359 ++++++++++++++++++++++++- Source/Scheduling/Scheduler.cs | 183 ++++++++++--- Source/Scheduling/WindowsTimeSource.cs | 16 +- 4 files changed, 525 insertions(+), 43 deletions(-) diff --git a/Source/Scheduling/GenericTimeSource.cs b/Source/Scheduling/GenericTimeSource.cs index 5ab5998..47acab5 100644 --- a/Source/Scheduling/GenericTimeSource.cs +++ b/Source/Scheduling/GenericTimeSource.cs @@ -87,12 +87,18 @@ namespace Nuclex.Support.Scheduling { /// True if the WaitHandle was signalled, false if the timeout was reached /// public virtual bool WaitOne(AutoResetEvent waitHandle, long ticks) { - checkForTimeAdjustment(); // Force a timeout at least each second so the caller can check the system time // since we're not able to provide the DateTimeAdjusted notification int milliseconds = (int)(ticks / TicksPerMillisecond); - return waitHandle.WaitOne(Math.Min(1000, milliseconds), false); + bool signalled = waitHandle.WaitOne(Math.Min(1000, milliseconds), false); + + // See whether the system date/time have been adjusted while we were asleep. + checkForTimeAdjustment(); + + // Now tell the caller whether his even was signalled + return signalled; + } /// Current system time in UTC format diff --git a/Source/Scheduling/Scheduler.Test.cs b/Source/Scheduling/Scheduler.Test.cs index 0a6d9f2..1cdf595 100644 --- a/Source/Scheduling/Scheduler.Test.cs +++ b/Source/Scheduling/Scheduler.Test.cs @@ -29,14 +29,175 @@ using Microsoft.Win32; using NUnit.Framework; -#if false - namespace Nuclex.Support.Scheduling { /// Unit Test for the scheduler [TestFixture] public class SchedulerTest { + #region class MockTimeSource + + /// Mocked time source + private class MockTimeSource : ITimeSource { + + /// Called when the system date/time are adjusted + public event EventHandler DateTimeAdjusted; + + /// Initializes a new mocked time source + /// Start time in UTC format + public MockTimeSource(DateTime utcStartTime) { + this.currentTime = utcStartTime; + } + + /// Waits for an AutoResetEvent to become signalled + /// WaitHandle the method will wait for + /// Number of ticks to wait + /// + /// True if the WaitHandle was signalled, false if the timeout was reached + /// or the time source thinks its time to recheck the system date/time. + /// + public bool WaitOne(AutoResetEvent waitHandle, long ticks) { + long currentTicks; + long eventDueTicks; + lock(this) { + this.autoResetEvent = waitHandle; + this.eventDueTicks += ticks; + + currentTicks = this.currentTicks; + eventDueTicks = this.eventDueTicks; + } + + // If we need to wait, use the wait handle. We do not use the wait handle's + // return value (or even its timeout) because we might trigger it ourselves + // to simulate the passing of time. + if(eventDueTicks > 0) { + this.autoResetEvent = waitHandle; + waitHandle.WaitOne(); + this.autoResetEvent = null; + } + + // Do not use the cached values here -- we might have used the WaitHandle and + // the simulation time could have been advanced while we were waiting. + lock(this) { + return (this.eventDueTicks > 0); // True = signalled, false = timed out + } + } + + /// Current system time in UTC format + public DateTime CurrentUtcTime { + get { lock(this) { return this.currentTime; } } + } + + /// How long the time source has been running + public long Ticks { + get { + lock(this) { + this.eventDueTicks = 0; + return this.currentTicks; + } + } + } + + /// Advances the time of the time source + /// + /// Time span by which to advance the time source's time + /// + public void AdvanceTime(TimeSpan timeSpan) { + lock(this) { + this.currentTicks += timeSpan.Ticks; + this.currentTime += timeSpan; + + // Problem: The Scheduler has just calculated the remaining ticks until + // notification occurs. Next, another thread advances simulation time + // and then the scheduler calls this. It will wait, even though + // the simultion time has progressed. + // To compensate this, we track the remaining time until the event is due + // and allow for a negative time budget if AdvanceTime() is called after + // the scheduler has just queried the current tick count. + this.eventDueTicks -= timeSpan.Ticks; + + if(this.eventDueTicks <= 0) { + AutoResetEvent copy = this.autoResetEvent; + if(copy != null) { + copy.Set(); + } + } + } + } + + /// Manually triggers the date time adjusted event + /// New simulation time to jump to + public void AdjustTime(DateTime newUtcTime) { + lock(this) { + this.currentTime = newUtcTime; + } + EventHandler copy = DateTimeAdjusted; + if(copy != null) { + copy(this, EventArgs.Empty); + } + } + + /// Auto reset event the time source is currently waiting on + private volatile AutoResetEvent autoResetEvent; + /// Ticks at which the auto reset event will be due + private long eventDueTicks; + /// Current time source tick counter + private long currentTicks; + /// Current system time and date + private DateTime currentTime; + + } + + #endregion // class MockTimeSource + + #region class TestSubscriber + + /// Subscriber used to test the scheduler notifications + private class TestSubscriber : IDisposable { + + /// Initializes a new test subscriber + public TestSubscriber() { + this.waitHandle = new AutoResetEvent(false); + } + + /// Immediately releases all resources owned by the instance + public void Dispose() { + if(this.waitHandle != null) { + this.waitHandle.Close(); + this.waitHandle = null; + } + } + + /// Callback method that can be subscribed to the scheduler + /// Not used + public void Callback(object state) { + Interlocked.Increment(ref this.callbackCount); + this.waitHandle.Set(); + } + + /// Blocks ther caller until the callback is invoked + /// + /// Maximum number of milliseconds to wait for the callback + /// + /// True if the callback was invoked, false if the call timed out + public bool WaitForCallback(int milliseconds) { + return this.waitHandle.WaitOne(milliseconds); + } + + /// Number of times the callback has been invoked + public int CallbackCount { + get { return Thread.VolatileRead(ref this.callbackCount); } + } + + /// Callback invocation count + private int callbackCount; + /// WaitHandle used to wait for the callback + private AutoResetEvent waitHandle; + + } + + #endregion // class TestSubscriber + /// /// Test whether the Scheduler can explicitely create a windows time source /// @@ -96,10 +257,200 @@ namespace Nuclex.Support.Scheduling { using(Scheduler scheduler = new Scheduler()) { } } + /// + /// Verifies that the default constructor of the scheduler is working + /// + [Test] + public void TestThrowOnNotifyAtWithUnspecifiedDateTimeKind() { + using(TestSubscriber subscriber = new TestSubscriber()) { + using(Scheduler scheduler = new Scheduler()) { + Assert.Throws( + delegate() { + scheduler.NotifyAt(new DateTime(2000, 1, 1), subscriber.Callback); + } + ); + } + } + } + + /// + /// Tests whether the NotifyAt() method invokes the callback at the right time + /// + [Test] + public void TestNotifyAt() { + MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1)); + using(TestSubscriber subscriber = new TestSubscriber()) { + using(Scheduler scheduler = new Scheduler(mockTimeSource)) { + scheduler.NotifyAt(makeUtc(new DateTime(2010, 1, 2)), subscriber.Callback); + + mockTimeSource.AdvanceTime(TimeSpan.FromDays(1)); + + Assert.IsTrue(subscriber.WaitForCallback(1000)); + } + } + } + + /// + /// Verifies that a notification at an absolute time is processed correctly + /// if a time synchronization occurs during the wait. + /// + [Test] + public void TestNotifyAtWithDateTimeAdjustment() { + MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1)); + using(TestSubscriber subscriber = new TestSubscriber()) { + using(Scheduler scheduler = new Scheduler(mockTimeSource)) { + scheduler.NotifyAt(makeUtc(new DateTime(2010, 1, 2)), subscriber.Callback); + + // Let 12 hours pass, after that, we simulate a time synchronization + // that puts the system 12 hours ahead of the original time. + mockTimeSource.AdvanceTime(TimeSpan.FromHours(12)); + mockTimeSource.AdjustTime(new DateTime(2010, 1, 2)); + + Assert.IsTrue(subscriber.WaitForCallback(1000)); + } + } + } + + /// Tests whether the scheduler's Cancel() method is working + [Test] + public void TestCancelNotification() { + MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1)); + using(TestSubscriber subscriber1 = new TestSubscriber()) { + using(TestSubscriber subscriber2 = new TestSubscriber()) { + using(Scheduler scheduler = new Scheduler(mockTimeSource)) { + object handle = scheduler.NotifyIn( + TimeSpan.FromHours(24), subscriber1.Callback + ); + scheduler.NotifyIn(TimeSpan.FromHours(36), subscriber2.Callback); + + mockTimeSource.AdvanceTime(TimeSpan.FromHours(12)); + scheduler.Cancel(handle); + mockTimeSource.AdvanceTime(TimeSpan.FromHours(24)); + + // Wait for the second subscriber to be notified. This is still a race + // condition (there's no guarantee the thread pool will run tasks in + // the order they were added), but it's the best we can do without + // relying on an ugly Thread.Sleep() in this test. + Assert.IsTrue(subscriber2.WaitForCallback(1000)); + Assert.AreEqual(0, subscriber1.CallbackCount); + } + } + } + } + + /// + /// Tests the scheduler with two notifications that are scheduled in inverse + /// order of their due time. + /// + [Test] + public void TestInverseOrderNotification() { + MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1)); + using(TestSubscriber subscriber1 = new TestSubscriber()) { + using(TestSubscriber subscriber2 = new TestSubscriber()) { + using(Scheduler scheduler = new Scheduler(mockTimeSource)) { + scheduler.NotifyIn(TimeSpan.FromHours(24), subscriber1.Callback); + scheduler.NotifyIn(TimeSpan.FromHours(12), subscriber2.Callback); + + mockTimeSource.AdvanceTime(TimeSpan.FromHours(18)); + + Assert.IsTrue(subscriber2.WaitForCallback(1000)); + Assert.AreEqual(0, subscriber1.CallbackCount); + + mockTimeSource.AdvanceTime(TimeSpan.FromHours(18)); + + Assert.IsTrue(subscriber1.WaitForCallback(1000)); + } + } + } + } + + /// + /// Tests the scheduler with two notifications that are scheduled to + /// occur at the exact same time + /// + [Test] + public void TestTwoNotificationsAtSameTime() { + MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1)); + using(TestSubscriber subscriber1 = new TestSubscriber()) { + using(TestSubscriber subscriber2 = new TestSubscriber()) { + using(Scheduler scheduler = new Scheduler(mockTimeSource)) { + scheduler.NotifyIn(60000, subscriber1.Callback); + scheduler.NotifyIn(60000, subscriber2.Callback); + + mockTimeSource.AdvanceTime(TimeSpan.FromMilliseconds(60000)); + + Assert.IsTrue(subscriber1.WaitForCallback(1000)); + Assert.IsTrue(subscriber2.WaitForCallback(1000)); + } + } + } + } + + /// + /// Verifies that the scheduler's NotifyEach() method is working correctly + /// + [Test] + public void TestNotifyEachWithMilliseconds() { + MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1)); + using(TestSubscriber subscriber = new TestSubscriber()) { + using(Scheduler scheduler = new Scheduler(mockTimeSource)) { + scheduler.NotifyEach(1000, 1000, subscriber.Callback); + + mockTimeSource.AdvanceTime(TimeSpan.FromMilliseconds(4000)); + + // Wait for 4 invocations of the callback. We might not catch each trigger + // of the AutoResetEvent, but we know that it will be 4 at most. + for(int invocation = 0; invocation < 4; ++invocation) { + Assert.IsTrue(subscriber.WaitForCallback(1000)); + + if(subscriber.CallbackCount == 4) { + break; + } + } + } + } + } + + /// + /// Verifies that the scheduler's NotifyEach() method is working correctly + /// + [Test] + public void TestNotifyEachWithTimespan() { + MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1)); + using(TestSubscriber subscriber = new TestSubscriber()) { + using(Scheduler scheduler = new Scheduler(mockTimeSource)) { + scheduler.NotifyEach( + TimeSpan.FromHours(12), TimeSpan.FromHours(1), subscriber.Callback + ); + + mockTimeSource.AdvanceTime(TimeSpan.FromHours(14)); + + // Wait for 3 invocations of the callback. We might not catch each trigger + // of the AutoResetEvent, but we know that it will be 3 at most. + for(int invocation = 0; invocation < 3; ++invocation) { + Assert.IsTrue(subscriber.WaitForCallback(1000)); + + if(subscriber.CallbackCount == 3) { + break; + } + } + } + } + } + + /// Returns the provided date/time value as a utc time value + /// Date/time value that will be returned as UTC + /// The provided date/time value as UTC + /// + /// This doesn't convert the time, it just returns the exact same date and time + /// but tags it as UTC by setting the DateTimeKind to UTC. + /// + private static DateTime makeUtc(DateTime dateTime) { + return new DateTime(dateTime.Ticks, DateTimeKind.Utc); + } + } } // namespace Nuclex.Support.Scheduling -#endif - #endif // UNITTEST diff --git a/Source/Scheduling/Scheduler.cs b/Source/Scheduling/Scheduler.cs index 0363553..be4d038 100644 --- a/Source/Scheduling/Scheduler.cs +++ b/Source/Scheduling/Scheduler.cs @@ -25,8 +25,6 @@ using System.Diagnostics; using Nuclex.Support.Collections; -#if false - namespace Nuclex.Support.Scheduling { /// Schedules actions for execution at a future point in time @@ -35,7 +33,7 @@ namespace Nuclex.Support.Scheduling { /// One tick is 100 ns, meaning 10000 ticks equal 1 ms private const long TicksPerMillisecond = 10000; -#region class TimeSourceSingleton + #region class TimeSourceSingleton /// /// Manages the singleton instance of the scheduler's default time source @@ -55,7 +53,7 @@ namespace Nuclex.Support.Scheduling { #endregion // class TimeSourceSingleton -#region class Notification + #region class Notification /// Scheduled notification private class Notification { @@ -77,7 +75,7 @@ namespace Nuclex.Support.Scheduling { long intervalTicks, long nextDueTicks, DateTime absoluteUtcTime, - Delegate callback + WaitCallback callback ) { this.IntervalTicks = intervalTicks; this.NextDueTicks = nextDueTicks; @@ -100,7 +98,7 @@ namespace Nuclex.Support.Scheduling { /// public DateTime AbsoluteUtcTime; /// Callback that will be invoked when the notification is due - public Delegate Callback; + public WaitCallback Callback; /// Whether the notification has been cancelled public bool Cancelled; @@ -108,14 +106,44 @@ namespace Nuclex.Support.Scheduling { #endregion // class Notification + #region class NotificationComparer + + /// Compares two notifications to each other + private class NotificationComparer : IComparer { + + /// The default instance of the notification comparer + public static readonly NotificationComparer Default = new NotificationComparer(); + + /// Compares two notifications to each other based on their time + /// Notification that will be compared on the left side + /// Notification that will be comapred on the right side + /// The relation of the two notification's times to each other + public int Compare(Notification left, Notification right) { + if(left.NextDueTicks > right.NextDueTicks) { + return -1; + } else if(left.NextDueTicks < right.NextDueTicks) { + return 1; + } else { + return 0; + } + } + + #endregion // class NotificationComparer + + } + /// Initializes a new scheduler using the default time source public Scheduler() : this(DefaultTimeSource) { } /// Initializes a new scheduler using the specified time source /// Source source the scheduler will use public Scheduler(ITimeSource timeSource) { + this.dateTimeAdjustedDelegate = new EventHandler(dateTimeAdjusted); + this.timeSource = timeSource; - this.notifications = new PriorityQueue(); + this.timeSource.DateTimeAdjusted += this.dateTimeAdjustedDelegate; + + this.notifications = new PriorityQueue(NotificationComparer.Default); this.notificationWaitEvent = new AutoResetEvent(false); this.timerThread = new Thread(new ThreadStart(runTimerThread)); @@ -138,6 +166,13 @@ namespace Nuclex.Support.Scheduling { this.timerThread.Join(2500), "Scheduler timer thread did not exit in time" ); + // Unsubscribe from the time source to avoid surprise events during or + // after shutdown + if(this.timeSource != null) { + this.timeSource.DateTimeAdjusted -= this.dateTimeAdjustedDelegate; + this.timeSource = null; + } + // Get rid of the notification wait event now that we've made sure that // the timer thread is down. this.notificationWaitEvent.Close(); @@ -145,7 +180,6 @@ namespace Nuclex.Support.Scheduling { // Help the GC a bit this.notificationWaitEvent = null; this.notifications = null; - this.timeSource = null; // Set to null so we don't attempt to end the thread again if Dispose() is // called multiple times. @@ -168,7 +202,7 @@ namespace Nuclex.Support.Scheduling { /// the notification. So if you need to be notified after a fixed time, use /// the NotifyIn() method instead. /// - public object NotifyAt(DateTime notificationTime, Delegate callback) { + public object NotifyAt(DateTime notificationTime, WaitCallback callback) { if(notificationTime.Kind == DateTimeKind.Unspecified) { throw new ArgumentException( "Notification time is neither UTC or local", "notificationTime" @@ -176,7 +210,9 @@ namespace Nuclex.Support.Scheduling { } DateTime notificationTimeUtc = notificationTime.ToUniversalTime(); - long remainingTicks = notificationTimeUtc.Ticks - DateTime.UtcNow.Ticks; + DateTime now = this.timeSource.CurrentUtcTime; + + long remainingTicks = notificationTimeUtc.Ticks - now.Ticks; long nextDueTicks = this.timeSource.Ticks + remainingTicks; return scheduleNotification( @@ -195,7 +231,7 @@ namespace Nuclex.Support.Scheduling { /// Callback that will be invoked when the notification is due /// /// A handle that can be used to cancel the notification - public object NotifyIn(TimeSpan delay, Delegate callback) { + public object NotifyIn(TimeSpan delay, WaitCallback callback) { return scheduleNotification( new Notification( 0, @@ -216,7 +252,7 @@ namespace Nuclex.Support.Scheduling { /// Callback that will be invoked when the notification is due /// /// A handle that can be used to cancel the notification - public object NotifyIn(int delayMilliseconds, Delegate callback) { + public object NotifyIn(int delayMilliseconds, WaitCallback callback) { return scheduleNotification( new Notification( 0, @@ -236,7 +272,7 @@ namespace Nuclex.Support.Scheduling { /// Callback that will be invoked when the notification is due /// /// A handle that can be used to cancel the notification - public object NotifyEach(TimeSpan delay, TimeSpan interval, Delegate callback) { + public object NotifyEach(TimeSpan delay, TimeSpan interval, WaitCallback callback) { return scheduleNotification( new Notification( interval.Ticks, @@ -261,7 +297,7 @@ namespace Nuclex.Support.Scheduling { /// /// A handle that can be used to cancel the notification public object NotifyEach( - int delayMilliseconds, int intervalMilliseconds, Delegate callback + int delayMilliseconds, int intervalMilliseconds, WaitCallback callback ) { return scheduleNotification( new Notification( @@ -273,6 +309,17 @@ namespace Nuclex.Support.Scheduling { ); } + /// Cancels a scheduled notification + /// + /// Handle of the notification that will be cancelled + /// + public void Cancel(object notificationHandle) { + Notification notification = notificationHandle as Notification; + if(notification != null) { + notification.Cancelled = true; + } + } + /// Returns the default time source for the scheduler public static ITimeSource DefaultTimeSource { get { return TimeSourceSingleton.Instance; } @@ -297,11 +344,54 @@ namespace Nuclex.Support.Scheduling { return CreateTimeSource(WindowsTimeSource.Available); } + /// Called when the system date/time have been adjusted + /// Time source which detected the adjustment + /// Not used + private void dateTimeAdjusted(object sender, EventArgs arguments) { + lock(this.timerThread) { + long currentTicks = this.timeSource.Ticks; + DateTime currentTime = this.timeSource.CurrentUtcTime; + + PriorityQueue updatedQueue = new PriorityQueue( + NotificationComparer.Default + ); + + // Copy all notifications from the original queue to a new one, adjusting + // those with an absolute notification time along the way to a new due tick + while(this.notifications.Count > 0) { + Notification notification = this.notifications.Dequeue(); + if(!notification.Cancelled) { + + // If this notification has an absolute due time, adjust its due tick + if(notification.AbsoluteUtcTime != DateTime.MinValue) { + + // Combining recurrent notifications with absolute time isn't allowed + Debug.Assert(notification.IntervalTicks == 0); + + // Make the adjustment + long remainingTicks = (notification.AbsoluteUtcTime - currentTime).Ticks; + notification.NextDueTicks = currentTicks + remainingTicks; + + } + + // Notification processed, move it over to the next priority queue + updatedQueue.Enqueue(notification); + + } + } + + // Replace the working queue with the update queue + this.notifications = updatedQueue; + } + + this.notificationWaitEvent.Set(); + } + /// Schedules a notification for processing by the timer thread /// Notification that will be scheduled /// The scheduled notification private object scheduleNotification(Notification notification) { - lock(this.notifications) { + lock(this.timerThread) { this.notifications.Enqueue(notification); // If this notification has become that next due notification, wake up @@ -316,12 +406,15 @@ namespace Nuclex.Support.Scheduling { /// Executes the timer thread private void runTimerThread() { + Notification nextDueNotification; + lock(this.timerThread) { + nextDueNotification = getNextDueNotification(); + } + // Keep processing notifications until we're told to quit for(; ; ) { - // Get the notification that is due next and wait for it. When no notifications - // are queued, wait indefinitely until we're signalled - Notification nextDueNotification = getNextDueNotification(); + // Wait until the nextmost notification is due or something else wakes us up if(nextDueNotification == null) { this.notificationWaitEvent.WaitOne(); } else { @@ -334,27 +427,52 @@ namespace Nuclex.Support.Scheduling { break; } + // Process all notifications that are due by handing them over to the thread pool. + // The notification queue might have been updated while we were sleeping, so + // look for the notification that is due next again + long ticks = this.timeSource.Ticks; + lock(this.timerThread) { + for(; ; ) { + nextDueNotification = getNextDueNotification(); + if(nextDueNotification == null) { + break; + } - //if(nextDueNotification.AbsoluteUtcTime != + // If the next notification is more than a millisecond away, we've reached + // the end of the notifications we need to process. + long remainingTicks = (nextDueNotification.NextDueTicks - ticks); + if(remainingTicks >= TicksPerMillisecond) { + break; + } - } + if(!nextDueNotification.Cancelled) { + ThreadPool.QueueUserWorkItem(nextDueNotification.Callback); + } + this.notifications.Dequeue(); + if(nextDueNotification.IntervalTicks != 0) { + nextDueNotification.NextDueTicks += nextDueNotification.IntervalTicks; + this.notifications.Enqueue(nextDueNotification); + } + } // for + } // lock + + } // for } /// Retrieves the notification that is due next /// The notification that is due next private Notification getNextDueNotification() { - lock(this.notifications) { - if(this.notifications.Count == 0) { - return null; - } else { - Notification nextDueNotification = this.notifications.Peek(); - while(nextDueNotification.Cancelled) { - this.notifications.Dequeue(); - nextDueNotification = this.notifications.Peek(); - } - return nextDueNotification; + if(this.notifications.Count == 0) { + return null; + } else { + Notification nextDueNotification = this.notifications.Peek(); + while(nextDueNotification.Cancelled) { + this.notifications.Dequeue(); + nextDueNotification = this.notifications.Peek(); } + + return nextDueNotification; } } @@ -370,8 +488,9 @@ namespace Nuclex.Support.Scheduling { /// Whether the timer thread should end private volatile bool endRequested; + /// Delegate for the dateTimeAdjusted() method + private EventHandler dateTimeAdjustedDelegate; + } } // namespace Nuclex.Support.Scheduling - -#endif diff --git a/Source/Scheduling/WindowsTimeSource.cs b/Source/Scheduling/WindowsTimeSource.cs index fc446b5..cb6f362 100644 --- a/Source/Scheduling/WindowsTimeSource.cs +++ b/Source/Scheduling/WindowsTimeSource.cs @@ -18,13 +18,13 @@ License along with this library */ #endregion -#if !XBOX360 - using System; using System.Collections.Generic; using System.Threading; +#if !XBOX360 using Microsoft.Win32; +#endif namespace Nuclex.Support.Scheduling { @@ -38,16 +38,24 @@ namespace Nuclex.Support.Scheduling { /// Initializes a new Windows time source public WindowsTimeSource() { +#if XBOX360 + throw new InvalidOperationException( + "Windows time source is not available on the XBox 360" + ); +#else this.onDateTimeAdjustedDelegate = new EventHandler(OnDateTimeAdjusted); SystemEvents.TimeChanged += this.onDateTimeAdjustedDelegate; +#endif } /// Immediately releases all resources owned by the instance public void Dispose() { +#if !XBOX360 if(this.onDateTimeAdjustedDelegate != null) { SystemEvents.TimeChanged -= this.onDateTimeAdjustedDelegate; this.onDateTimeAdjustedDelegate = null; } +#endif } /// Waits for an AutoResetEvent to become signalled @@ -57,7 +65,7 @@ namespace Nuclex.Support.Scheduling { /// True if the WaitHandle was signalled, false if the timeout was reached /// public override bool WaitOne(AutoResetEvent waitHandle, long ticks) { - return waitHandle.WaitOne((int)(ticks / TicksPerMillisecond)); + return waitHandle.WaitOne((int)(ticks / TicksPerMillisecond), false); } /// @@ -73,5 +81,3 @@ namespace Nuclex.Support.Scheduling { } } // namespace Nuclex.Support.Scheduling - -#endif // !XBOX360