diff --git a/Nuclex.Support (Xbox 360).csproj b/Nuclex.Support (Xbox 360).csproj
index d716fe7..151973f 100644
--- a/Nuclex.Support (Xbox 360).csproj
+++ b/Nuclex.Support (Xbox 360).csproj
@@ -122,12 +122,15 @@
PrototypeFactory.cs
-
- DefaultTimeSource.cs
+
+ GenericTimeSource.cs
-
+
+
+ Scheduler.cs
+
WindowsTimeSource.cs
diff --git a/Nuclex.Support.csproj b/Nuclex.Support.csproj
index 2d2affa..bf1bc76 100644
--- a/Nuclex.Support.csproj
+++ b/Nuclex.Support.csproj
@@ -104,12 +104,15 @@
PrototypeFactory.cs
-
- DefaultTimeSource.cs
+
+ GenericTimeSource.cs
-
+
+
+ Scheduler.cs
+
WindowsTimeSource.cs
diff --git a/Source/Scheduling/DefaultTimeSource.Test.cs b/Source/Scheduling/GenericTimeSource.Test.cs
similarity index 82%
rename from Source/Scheduling/DefaultTimeSource.Test.cs
rename to Source/Scheduling/GenericTimeSource.Test.cs
index 9f2fe2f..c00feaf 100644
--- a/Source/Scheduling/DefaultTimeSource.Test.cs
+++ b/Source/Scheduling/GenericTimeSource.Test.cs
@@ -28,16 +28,16 @@ using NUnit.Framework;
namespace Nuclex.Support.Scheduling {
- /// Unit Test for the default scheduler time source
+ /// Unit Test for the generic scheduler time source
[TestFixture]
- public class DefaultTimeSourceTest {
+ public class GenericTimeSourceTest {
///
/// Verifies that the time source's default constructor is working
///
[Test]
public void TestDefaultConstructor() {
- DefaultTimeSource timeSource = new DefaultTimeSource();
+ GenericTimeSource timeSource = new GenericTimeSource();
}
///
@@ -45,7 +45,7 @@ namespace Nuclex.Support.Scheduling {
///
[Test]
public void TestCurrentUtcTime() {
- DefaultTimeSource timeSource = new DefaultTimeSource();
+ GenericTimeSource timeSource = new GenericTimeSource();
Assert.That(
timeSource.CurrentUtcTime, Is.EqualTo(DateTime.UtcNow).Within(10).Seconds
@@ -58,7 +58,7 @@ namespace Nuclex.Support.Scheduling {
///
[Test]
public void TestTicksWithStopwatch() {
- DefaultTimeSource timeSource = new DefaultTimeSource(true);
+ GenericTimeSource timeSource = new GenericTimeSource(true);
long ticks1 = timeSource.Ticks;
long ticks2 = timeSource.Ticks;
@@ -71,7 +71,7 @@ namespace Nuclex.Support.Scheduling {
///
[Test]
public void TestTicksWithTickCount() {
- DefaultTimeSource timeSource = new DefaultTimeSource(false);
+ GenericTimeSource timeSource = new GenericTimeSource(false);
long ticks1 = timeSource.Ticks;
long ticks2 = timeSource.Ticks;
@@ -83,7 +83,7 @@ namespace Nuclex.Support.Scheduling {
///
[Test]
public void TestWaitOne() {
- DefaultTimeSource timeSource = new DefaultTimeSource();
+ GenericTimeSource timeSource = new GenericTimeSource();
AutoResetEvent waitEvent = new AutoResetEvent(true);
Assert.IsTrue(timeSource.WaitOne(waitEvent, TimeSpan.FromMilliseconds(1).Ticks));
diff --git a/Source/Scheduling/DefaultTimeSource.cs b/Source/Scheduling/GenericTimeSource.cs
similarity index 67%
rename from Source/Scheduling/DefaultTimeSource.cs
rename to Source/Scheduling/GenericTimeSource.cs
index 52862a1..5ab5998 100644
--- a/Source/Scheduling/DefaultTimeSource.cs
+++ b/Source/Scheduling/GenericTimeSource.cs
@@ -26,13 +26,20 @@ using System.Threading;
namespace Nuclex.Support.Scheduling {
///
- /// Default time source implementation using the Stopwatch or Environment.TickCount
+ /// Generic time source implementation using the Stopwatch or Environment.TickCount
///
- public class DefaultTimeSource : ITimeSource {
+ public class GenericTimeSource : ITimeSource {
/// Number of ticks (100 ns intervals) in a millisecond
private const long TicksPerMillisecond = 10000;
+ /// Tolerance for the detection of a date/time adjustment
+ ///
+ /// If the current system date/time jumps by more than this tolerance into any
+ /// direction, the default time source will trigger the DateTimeAdjusted event.
+ ///
+ private const long TimeAdjustmentToleranceTicks = 75 * TicksPerMillisecond;
+
/// Called when the system date/time are adjusted
///
/// An adjustment is a change out of the ordinary, eg. when a time synchronization
@@ -42,13 +49,13 @@ namespace Nuclex.Support.Scheduling {
public event EventHandler DateTimeAdjusted;
/// Initializes the static fields of the default time source
- static DefaultTimeSource() {
+ static GenericTimeSource() {
tickFrequency = 10000000.0;
tickFrequency /= (double)Stopwatch.Frequency;
}
/// Initializes the default time source
- public DefaultTimeSource() : this(Stopwatch.IsHighResolution) { }
+ public GenericTimeSource() : this(Stopwatch.IsHighResolution) { }
/// Initializes the default time source
///
@@ -67,23 +74,25 @@ namespace Nuclex.Support.Scheduling {
/// but then your won't profit from the high-resolution timer if one is available.
///
///
- public DefaultTimeSource(bool useStopwatch) {
+ public GenericTimeSource(bool useStopwatch) {
this.useStopwatch = useStopwatch;
+
+ checkForTimeAdjustment();
}
- /// Waits for an AutoResetEvent become signalled
+ /// 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
///
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);
-
}
/// Current system time in UTC format
@@ -127,6 +136,41 @@ namespace Nuclex.Support.Scheduling {
}
}
+ ///
+ /// Checks whether the system/date time have been adjusted since the last call
+ ///
+ private void checkForTimeAdjustment() {
+
+ // Grab the current date/time and timer ticks in one go
+ DateTime currentLocalTime = DateTime.Now;
+ long currentTicks = Ticks;
+
+ // Calculate the number of timer ticks that have passed since the last check and
+ // extrapolate the local date/time we should be expecting to see
+ long ticksSinceLastCheck = currentTicks - lastCheckedTicks;
+ DateTime expectedLocalTime = new DateTime(
+ lastCheckedLocalTime.Ticks + ticksSinceLastCheck, DateTimeKind.Local
+ );
+
+ // Find out by what amount the actual local date/time deviates from
+ // the extrapolated date/time and trigger the date/time adjustment event if
+ // we can reasonably assume that the system date/time have been adjusted.
+ long deviationTicks = Math.Abs(expectedLocalTime.Ticks - currentLocalTime.Ticks);
+ if(deviationTicks > TimeAdjustmentToleranceTicks) {
+ OnDateTimeAdjusted(this, EventArgs.Empty);
+ }
+
+ // Remember the current local date/time and timer ticks for the next run
+ this.lastCheckedLocalTime = currentLocalTime;
+ this.lastCheckedTicks = currentTicks;
+
+ }
+
+ /// Last local time we checked for a date/time adjustment
+ private DateTime lastCheckedLocalTime;
+ /// Timer ticks at which we last checked the local time
+ private long lastCheckedTicks;
+
/// Number of ticks per Stopwatch time unit
private static double tickFrequency;
/// Whether ot use the Stopwatch class for measuring time
diff --git a/Source/Scheduling/ITimeSource.cs b/Source/Scheduling/ITimeSource.cs
index 72dd5c6..beee0b7 100644
--- a/Source/Scheduling/ITimeSource.cs
+++ b/Source/Scheduling/ITimeSource.cs
@@ -25,7 +25,7 @@ using System.Threading;
namespace Nuclex.Support.Scheduling {
/// Provides time measurement and change notification services
- interface ITimeSource {
+ public interface ITimeSource {
/// Called when the system date/time are adjusted
///
@@ -35,7 +35,7 @@ namespace Nuclex.Support.Scheduling {
///
event EventHandler DateTimeAdjusted;
- /// Waits for an AutoResetEvent become signalled
+ /// Waits for an AutoResetEvent to become signalled
/// WaitHandle the method will wait for
/// Number of ticks to wait
///
diff --git a/Source/Scheduling/Scheduler.Test.cs b/Source/Scheduling/Scheduler.Test.cs
new file mode 100644
index 0000000..0a6d9f2
--- /dev/null
+++ b/Source/Scheduling/Scheduler.Test.cs
@@ -0,0 +1,105 @@
+#region CPL License
+/*
+Nuclex Framework
+Copyright (C) 2002-2009 Nuclex Development Labs
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the IBM Common Public License as
+published by the IBM Corporation; either version 1.0 of the
+License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+IBM Common Public License for more details.
+
+You should have received a copy of the IBM Common Public
+License along with this library
+*/
+#endregion
+
+#if UNITTEST
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+using Microsoft.Win32;
+
+using NUnit.Framework;
+
+#if false
+
+namespace Nuclex.Support.Scheduling {
+
+ /// Unit Test for the scheduler
+ [TestFixture]
+ public class SchedulerTest {
+
+ ///
+ /// Test whether the Scheduler can explicitely create a windows time source
+ ///
+ [Test]
+ public void TestCreateWindowsTimeSource() {
+ ITimeSource timeSource = Scheduler.CreateTimeSource(true);
+ try {
+ Assert.That(timeSource is WindowsTimeSource);
+ }
+ finally {
+ IDisposable disposableTimeSource = timeSource as IDisposable;
+ if(disposableTimeSource != null) {
+ disposableTimeSource.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Test whether the Scheduler can explicitely create a generic time source
+ ///
+ [Test]
+ public void TestCreateGenericTimeSource() {
+ ITimeSource timeSource = Scheduler.CreateTimeSource(false);
+ try {
+ Assert.That(timeSource is GenericTimeSource);
+ }
+ finally {
+ IDisposable disposableTimeSource = timeSource as IDisposable;
+ if(disposableTimeSource != null) {
+ disposableTimeSource.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Test whether the Scheduler can automatically choose the right time source
+ ///
+ [Test]
+ public void TestCreateDefaultTimeSource() {
+ ITimeSource timeSource = Scheduler.CreateDefaultTimeSource();
+ try {
+ Assert.IsNotNull(timeSource);
+ }
+ finally {
+ IDisposable disposableTimeSource = timeSource as IDisposable;
+ if(disposableTimeSource != null) {
+ disposableTimeSource.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Verifies that the default constructor of the scheduler is working
+ ///
+ [Test]
+ public void TestDefaultConstructor() {
+ using(Scheduler scheduler = new Scheduler()) { }
+ }
+
+ }
+
+} // namespace Nuclex.Support.Scheduling
+
+#endif
+
+#endif // UNITTEST
diff --git a/Source/Scheduling/Scheduler.cs b/Source/Scheduling/Scheduler.cs
index f13fa24..0363553 100644
--- a/Source/Scheduling/Scheduler.cs
+++ b/Source/Scheduling/Scheduler.cs
@@ -23,20 +23,355 @@ using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;
-namespace Nuclex.Support.Scheduling {
+using Nuclex.Support.Collections;
#if false
+namespace Nuclex.Support.Scheduling {
+
/// Schedules actions for execution at a future point in time
public class Scheduler : IDisposable {
- /// Immediately releases all resources owned by the instance
- public void Dispose() {
+ /// One tick is 100 ns, meaning 10000 ticks equal 1 ms
+ private const long TicksPerMillisecond = 10000;
+
+#region class TimeSourceSingleton
+
+ ///
+ /// Manages the singleton instance of the scheduler's default time source
+ ///
+ private class TimeSourceSingleton {
+
+ ///
+ /// Explicit static constructor to guarantee the singleton is initialized only
+ /// when a static member of this class is accessed.
+ ///
+ static TimeSourceSingleton() { } // Do not remove!
+
+ /// The singleton instance of the default time source
+ internal static readonly ITimeSource Instance = Scheduler.CreateDefaultTimeSource();
+
}
+ #endregion // class TimeSourceSingleton
+
+#region class Notification
+
+ /// Scheduled notification
+ private class Notification {
+
+ /// Initializes a new notification
+ ///
+ /// Interval in which the notification will re-executed
+ ///
+ ///
+ /// Time source ticks the notification is next due at
+ ///
+ ///
+ /// Absolute time in UTC at which the notification is due
+ ///
+ ///
+ /// Callback to be invoked when the notification is due
+ ///
+ public Notification(
+ long intervalTicks,
+ long nextDueTicks,
+ DateTime absoluteUtcTime,
+ Delegate callback
+ ) {
+ this.IntervalTicks = intervalTicks;
+ this.NextDueTicks = nextDueTicks;
+ this.AbsoluteUtcTime = absoluteUtcTime;
+ this.Callback = callback;
+ this.Cancelled = false;
+ }
+
+ ///
+ /// Ticks specifying the interval in which the notification will be re-executed
+ ///
+ public long IntervalTicks;
+
+ /// Next due time for this notification
+ public long NextDueTicks;
+ /// Absolute time in UTC at which the notification is due
+ ///
+ /// Only stored for notifications scheduled in absolute time, meaning they
+ /// have to be adjusted if the system date/time changes
+ ///
+ public DateTime AbsoluteUtcTime;
+ /// Callback that will be invoked when the notification is due
+ public Delegate Callback;
+ /// Whether the notification has been cancelled
+ public bool Cancelled;
+
+ }
+
+ #endregion // class Notification
+
+ /// 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.timeSource = timeSource;
+ this.notifications = new PriorityQueue();
+ this.notificationWaitEvent = new AutoResetEvent(false);
+
+ this.timerThread = new Thread(new ThreadStart(runTimerThread));
+ this.timerThread.Name = "Nuclex.Support.Scheduling.Scheduler";
+ this.timerThread.Priority = ThreadPriority.Highest;
+ this.timerThread.IsBackground = true;
+ this.timerThread.Start();
+ }
+
+ /// Immediately releases all resources owned by the instance
+ public void Dispose() {
+ if(this.timerThread != null) {
+ this.endRequested = true;
+ this.notificationWaitEvent.Set();
+
+ // Wait for the timer thread to exit. If it doesn't exit in 10 seconds (which is
+ // a lot of time given that it doesn't do any real work), forcefully abort
+ // the thread. This may risk some leaks, but it's the only thing we can do.
+ Trace.Assert(
+ this.timerThread.Join(2500), "Scheduler timer thread did not exit in time"
+ );
+
+ // Get rid of the notification wait event now that we've made sure that
+ // the timer thread is down.
+ this.notificationWaitEvent.Close();
+
+ // 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.
+ this.timerThread = null;
+ }
+ }
+
+ /// Schedules a notification at the specified absolute time
+ ///
+ /// Absolute time at which the notification will occur
+ ///
+ ///
+ /// Callback that will be invoked when the notification is due
+ ///
+ /// A handle that can be used to cancel the notification
+ ///
+ /// The notification is scheduled for the indicated absolute time. If the system
+ /// enters/leaves daylight saving time or the date/time is changed (for example
+ /// when the system synchronizes with an NTP server), this will affect
+ /// 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) {
+ if(notificationTime.Kind == DateTimeKind.Unspecified) {
+ throw new ArgumentException(
+ "Notification time is neither UTC or local", "notificationTime"
+ );
+ }
+
+ DateTime notificationTimeUtc = notificationTime.ToUniversalTime();
+ long remainingTicks = notificationTimeUtc.Ticks - DateTime.UtcNow.Ticks;
+ long nextDueTicks = this.timeSource.Ticks + remainingTicks;
+
+ return scheduleNotification(
+ new Notification(
+ 0,
+ nextDueTicks,
+ notificationTimeUtc,
+ callback
+ )
+ );
+ }
+
+ /// Schedules a notification after the specified time span
+ /// Delay after which the notification will occur
+ ///
+ /// 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) {
+ return scheduleNotification(
+ new Notification(
+ 0,
+ delay.Ticks,
+ DateTime.MinValue,
+ callback
+ )
+ );
+ }
+
+ ///
+ /// Schedules a notification after the specified amount of milliseconds
+ ///
+ ///
+ /// Number of milliseconds after which the notification will occur
+ ///
+ ///
+ /// 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) {
+ return scheduleNotification(
+ new Notification(
+ 0,
+ (long)delayMilliseconds * TicksPerMillisecond,
+ DateTime.MinValue,
+ callback
+ )
+ );
+ }
+
+ ///
+ /// Schedules a recurring notification after the specified time span
+ ///
+ /// Delay after which the first notification will occur
+ /// Interval at which the notification will be repeated
+ ///
+ /// 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) {
+ return scheduleNotification(
+ new Notification(
+ interval.Ticks,
+ delay.Ticks,
+ DateTime.MinValue,
+ callback
+ )
+ );
+ }
+
+ ///
+ /// Schedules a recurring notification after the specified amount of milliseconds
+ ///
+ ///
+ /// Milliseconds after which the first notification will occur
+ ///
+ ///
+ /// Interval in milliseconds at which the notification will be repeated
+ ///
+ ///
+ /// Callback that will be invoked when the notification is due
+ ///
+ /// A handle that can be used to cancel the notification
+ public object NotifyEach(
+ int delayMilliseconds, int intervalMilliseconds, Delegate callback
+ ) {
+ return scheduleNotification(
+ new Notification(
+ (long)intervalMilliseconds * TicksPerMillisecond,
+ (long)delayMilliseconds * TicksPerMillisecond,
+ DateTime.MinValue,
+ callback
+ )
+ );
+ }
+
+ /// Returns the default time source for the scheduler
+ public static ITimeSource DefaultTimeSource {
+ get { return TimeSourceSingleton.Instance; }
+ }
+
+ /// Creates a new default time source for the scheduler
+ ///
+ /// Whether the specialized windows time source should be used
+ ///
+ /// The newly created time source
+ internal static ITimeSource CreateTimeSource(bool useWindowsTimeSource) {
+ if(useWindowsTimeSource) {
+ return new WindowsTimeSource();
+ } else {
+ return new GenericTimeSource();
+ }
+ }
+
+ /// Creates a new default time source for the scheduler
+ /// The newly created time source
+ internal static ITimeSource CreateDefaultTimeSource() {
+ return CreateTimeSource(WindowsTimeSource.Available);
+ }
+
+ /// 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) {
+ this.notifications.Enqueue(notification);
+
+ // If this notification has become that next due notification, wake up
+ // the timer thread so it can adjust its sleep period.
+ if(ReferenceEquals(this.notifications.Peek(), notification)) {
+ this.notificationWaitEvent.Set();
+ }
+ }
+
+ return notification;
+ }
+
+ /// Executes the timer thread
+ private void runTimerThread() {
+
+ 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();
+ if(nextDueNotification == null) {
+ this.notificationWaitEvent.WaitOne();
+ } else {
+ long remainingTicks = nextDueNotification.NextDueTicks - this.timeSource.Ticks;
+ this.timeSource.WaitOne(this.notificationWaitEvent, remainingTicks);
+ }
+
+ // Have we been woken up because the Scheduler is being disposed?
+ if(this.endRequested) {
+ break;
+ }
+
+
+ //if(nextDueNotification.AbsoluteUtcTime !=
+
+ }
+
+ }
+
+ /// 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;
+ }
+ }
+ }
+
+ /// Time source used by the scheduler
+ private ITimeSource timeSource;
+ /// Thread that will wait for the next scheduled event
+ private Thread timerThread;
+ /// Notifications in the scheduler's queue
+ private PriorityQueue notifications;
+
+ /// Event used by the timer thread to wait for the next notification
+ private AutoResetEvent notificationWaitEvent;
+ /// Whether the timer thread should end
+ private volatile bool endRequested;
}
-#endif
-
} // namespace Nuclex.Support.Scheduling
+
+#endif
diff --git a/Source/Scheduling/WindowsTimeSource.cs b/Source/Scheduling/WindowsTimeSource.cs
index 0747c87..fc446b5 100644
--- a/Source/Scheduling/WindowsTimeSource.cs
+++ b/Source/Scheduling/WindowsTimeSource.cs
@@ -31,7 +31,7 @@ namespace Nuclex.Support.Scheduling {
///
/// Time source that makes use of additional features only available on Windows
///
- public class WindowsTimeSource : DefaultTimeSource, IDisposable {
+ public class WindowsTimeSource : GenericTimeSource, IDisposable {
/// Number of ticks (100 ns intervals) in a millisecond
private const long TicksPerMillisecond = 10000;
@@ -50,7 +50,7 @@ namespace Nuclex.Support.Scheduling {
}
}
- /// Waits for an AutoResetEvent become signalled
+ /// Waits for an AutoResetEvent to become signalled
/// WaitHandle the method will wait for
/// Number of ticks to wait
///