using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Neo.Afx.Services { /// /// A scheduler. /// public class ServiceTaskScheduler { /// /// Raised when a task is completed. /// public event EventHandler TaskCompleted; /// /// Raised when a task is busy. /// public event EventHandler TaskBusy; /// /// Raised when an unhandled error is thrown. /// public event EventHandler UnhandledError; /// /// This is here to enhance accuracy. Even if nothing is scheduled the timer sleeps for a maximum of 1 minute. /// static readonly TimeSpan MaxInterval = new TimeSpan(0, 1, 0); Timer _timer; bool _stopped; DateTime _lastTime; readonly List _serviceTasks; #region Initialization public ServiceTaskScheduler(List serviceTasks) { _stopped = true; _serviceTasks = serviceTasks; } #endregion #region Start public void Start() { _stopped = false; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; _timer = new Timer(OnTimerElapsed); QueueNextTime(DateTime.Now, true); } #endregion #region Stop public void Stop() { _stopped = true; TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException; if(_timer != null) { _timer.Dispose(); _timer = null; } } #endregion #region OnUnobservedTaskException void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { // This is a last line of defense and is used to handle completely unexpected exceptions. // The handler runs on the finalizer thread so it’s probably too late to really recover from this. var ex = e.Exception.Flatten(); // prevent the process from terminating e.SetObserved(); if(UnhandledError != null) { UnhandledError(this, new ErrorEventArgs(ex)); } } #endregion #region OnTimerElapsed void OnTimerElapsed(object state) { var signalTime = DateTime.Now; StopTimer((Timer)state); foreach(var serviceTask in _serviceTasks.Where(serviceTask => serviceTask.Schedule.IsDue(_lastTime, signalTime))) { if(serviceTask.IsBusy) { if(TaskBusy != null) { TaskBusy(this, new ServiceTaskEventArgs(serviceTask, signalTime)); } } else { // Set the LastRunTime before starting the // task so that the task will calculate the correct // NextRunTime when QueueNextTime is called below. // // Tasks are started asynchronously which means // that if you put this logic in the Run method // QueueNextTime will use the current LastRunTime value. // serviceTask.Schedule.LastRunTime = signalTime; Task.Factory.StartNew(taskState => ((ServiceTask)taskState).Run(), serviceTask) .ContinueWith(task => { var st = (ServiceTask)task.AsyncState; if(task.Exception != null) { var tex = task.Exception.Flatten(); st.Exceptions = tex.InnerExceptions; } if(TaskCompleted != null) { TaskCompleted(this, new ServiceTaskEventArgs(st, st.Schedule.LastRunTime)); } }); } } if(!_stopped) { QueueNextTime(signalTime); } } #endregion #region QueueNextTime void QueueNextTime(DateTime thisTime, bool isStarting = false) { _lastTime = thisTime; if(isStarting) { foreach(var t in _serviceTasks) { t.Schedule.LastRunTime = thisTime; } } var next = _serviceTasks.Min(st => st.Schedule.NextRunTime(thisTime)); var interval = next - thisTime; //Handles an invalid wait time: the interval property requires a duration > 0. if(interval > MaxInterval || interval.TotalMilliseconds <= 0) { interval = MaxInterval; } StartTimer(_timer, interval.TotalMilliseconds); } #endregion #region Start/Stop Timer Methods static void StopTimer(Timer timer) { timer.Change(Timeout.Infinite, 0); } static void StartTimer(Timer timer, double interval) { timer.Change((long)interval, 0); } #endregion } }