using System; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; namespace AMWD.Common.Utilities { // originally inspired by a code from Yves Goergen (unclassified.software). /// /// Implements an awaitable task that runs after a specified delay. The delay can be reset. /// By resetting the delay, the task can be executed multiple times. /// public class DelayedTask { #region Data private Timer timer; private bool nextRunPending; /// /// The synchronisation object. /// protected readonly object syncLock = new(); /// /// The exception handler. /// protected Action exceptionHandler; /// /// Provides the for the method. /// protected TaskCompletionSourceWrapper tcs; /// /// Gets or sets the action to execute. /// protected Action Action { get; set; } /// /// Gets a value indicating whether the timer is running and an execution is scheduled. This /// is mutually exclusive to . /// public bool IsWaitingToRun { get; private set; } /// /// Gets a value indicating whether the action is currently running. This is mutually /// exclusive to . /// public bool IsRunning { get; private set; } /// /// Gets or sets the delay to wait before executing the action. /// public TimeSpan Delay { get; protected set; } #endregion Data #region Static methods /// /// Creates a new task instance that executes the specified action after the delay, but does /// not start it yet. /// /// The action to execute. /// The delay. /// public static DelayedTask Create(Action action, TimeSpan delay) => new() { Action = action, Delay = delay }; /// /// Creates a new task instance that executes the specified action after the delay, but does /// not start it yet. /// /// The action to execute. /// The delay. /// public static DelayedTaskWithResult Create(Func action, TimeSpan delay) => DelayedTaskWithResult.Create(action, delay); /// /// Executes the specified action after the delay. /// /// The action to execute. /// The delay. /// public static DelayedTask Run(Action action, TimeSpan delay) => new DelayedTask { Action = action, Delay = delay }.Start(); /// /// Executes the specified action after the delay. /// /// The action to execute. /// The delay. /// public static DelayedTaskWithResult Run(Func action, TimeSpan delay) => DelayedTaskWithResult.Run(action, delay); #endregion Static methods #region Constructors /// /// Initializes a new instance of the class. /// protected DelayedTask() { tcs = CreateTcs(); SetLastResult(tcs); } #endregion Constructors #region Public instance methods /// /// Resets the delay and restarts the timer. If an execution is currently pending, it is /// postponed until the full delay has elapsed again. If no execution is pending, the action /// will be executed again after the delay. /// public void Reset() { lock (syncLock) { if (!IsWaitingToRun && !IsRunning) { // Let callers wait for the next execution tcs = CreateTcs(); } IsWaitingToRun = true; if (timer != null) { timer.Change(Delay, Timeout.InfiniteTimeSpan); } else { timer = new Timer(OnTimerCallback, null, Delay, Timeout.InfiniteTimeSpan); } } } /// /// Cancels the delay. Any pending execution is cleared. If the action was pending but not /// yet executing, this task is cancelled. If the action was not pending or is already /// executing, this task will be completed successfully after the action has completed. /// public void Cancel() { TaskCompletionSourceWrapper localTcs = null; lock (syncLock) { IsWaitingToRun = false; nextRunPending = false; timer?.Dispose(); timer = null; if (!IsRunning) { localTcs = tcs; } } // Complete the task (as cancelled) so that nobody needs to wait for an execution that // isn't currently scheduled localTcs?.TrySetCanceled(); } /// /// Starts a pending execution immediately, not waiting for the timer to elapse. /// /// true, if an execution was started; otherwise, false. public bool ExecutePending() { lock (syncLock) { if (!IsWaitingToRun && !IsRunning) { return false; } IsWaitingToRun = true; if (timer != null) { timer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan); } else { timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); } return true; } } /// /// Gets an awaiter used to await this . /// /// An awaiter instance. public TaskAwaiter GetAwaiter() { lock (syncLock) { return tcs.Task.GetAwaiter(); } } /// /// Gets a that represents the current awaitable /// operation. /// public Task Task { get { lock (syncLock) { return tcs.Task; } } } /// /// Performs an implicit conversion from to /// . /// /// The instance to cast. /// A that represents the current /// awaitable operation. public static implicit operator Task(DelayedTask delayedTask) => delayedTask.Task; /// /// Gets the exception of the last execution. If the action has not yet thrown any /// exceptions, this will return null. /// public Exception Exception => Task.Exception; /// /// Adds an unhandled exception handler to this instance. /// /// The action that handles an exception. /// The current instance. public DelayedTask WithExceptionHandler(Action exceptionHandler) { this.exceptionHandler = exceptionHandler; return this; } #endregion Public instance methods #region Non-public methods /// /// Starts the current instance after creating it. /// /// The current instance. protected DelayedTask Start() { tcs = CreateTcs(); IsWaitingToRun = true; timer = new Timer(OnTimerCallback, null, Delay, Timeout.InfiniteTimeSpan); return this; } /// /// Creates a instance. /// /// protected virtual TaskCompletionSourceWrapper CreateTcs() => new TaskCompletionSourceWrapper(); /// /// Called when the timer has elapsed. /// /// Unused. protected void OnTimerCallback(object state) { lock (syncLock) { if (!IsWaitingToRun) { // Already cancelled, do nothing return; } IsWaitingToRun = false; if (IsRunning) { // Currently running, remember and do nothing for now nextRunPending = true; return; } IsRunning = true; } // Run as long as there are pending executions and the instance has not been disposed of bool runAgain; TaskCompletionSourceWrapper localTcs = null; Exception exception = null; do { try { Run(); } catch (Exception ex) { exception = ex; lock (syncLock) { runAgain = false; IsRunning = false; nextRunPending = false; localTcs = tcs; if (!IsWaitingToRun) { timer?.Dispose(); timer = null; } } exceptionHandler?.Invoke(ex); } finally { lock (syncLock) { runAgain = nextRunPending; IsRunning = runAgain; nextRunPending = false; if (!runAgain) { if (!IsWaitingToRun) { localTcs = tcs; timer?.Dispose(); timer = null; } } } } } while (runAgain); // Unblock waiters if not already waiting for the next execution. // This task can be awaited again after the Reset method has been called. if (exception != null) { localTcs?.TrySetException(exception); } else { SetLastResult(localTcs); } } /// /// Runs the action of the task. /// protected virtual void Run() => Action(); /// /// Sets the result from the last action. /// /// The to set the result of. protected virtual void SetLastResult(TaskCompletionSourceWrapper tcs) { var myTcs = (TaskCompletionSourceWrapper)tcs; myTcs?.TrySetResult(default); } #endregion Non-public methods #region Internal TaskCompletionSourceWrapper classes /// /// Wraps a instance in a non-generic way to /// allow sharing it in the non-generic base class. /// protected abstract class TaskCompletionSourceWrapper { /// /// Gets the of the . /// public abstract Task Task { get; } /// /// Attempts to transition the underlying into the /// state and binds it to a specified exception. /// /// The exception to bind to this . /// public abstract void TrySetException(Exception exception); /// /// Attempts to transition the underlying into the /// state. /// /// public abstract void TrySetCanceled(); } /// /// A that provides a result value. /// /// The type of the result value. protected class TaskCompletionSourceWrapper : TaskCompletionSourceWrapper { private readonly TaskCompletionSource tcs; /// /// Gets the of the . /// public override Task Task => tcs.Task; /// /// Initializes a new instance of the class. /// public TaskCompletionSourceWrapper() { tcs = new TaskCompletionSource(); } /// /// Attempts to transition the underlying into the /// state. /// /// The result value to bind to this . /// public void TrySetResult(TResult result) => tcs.TrySetResult(result); /// public override void TrySetException(Exception exception) => tcs.TrySetException(exception); /// public override void TrySetCanceled() => tcs.TrySetCanceled(); } #endregion Internal TaskCompletionSourceWrapper classes } #region Generic derived classes /// /// Implements an awaitable task that runs after a specified delay. The delay can be reset /// before and after the task has run. /// /// The type of the return value of the action. public class DelayedTaskWithResult : DelayedTask { /// /// The result of the last execution of the action. /// protected TResult lastResult; /// /// Gets or sets the action to execute. /// protected new Func Action { get; set; } internal static DelayedTaskWithResult Create(Func action, TimeSpan delay) => new() { Action = action, Delay = delay }; internal static DelayedTaskWithResult Run(Func action, TimeSpan delay) => (DelayedTaskWithResult)new DelayedTaskWithResult { Action = action, Delay = delay }.Start(); /// protected override TaskCompletionSourceWrapper CreateTcs() => new TaskCompletionSourceWrapper(); /// protected override void Run() { lastResult = Action(); } /// protected override void SetLastResult(TaskCompletionSourceWrapper tcs) { var myTcs = (TaskCompletionSourceWrapper)tcs; myTcs?.TrySetResult(lastResult); } /// /// Gets an awaiter used to await this . /// /// An awaiter instance. public new TaskAwaiter GetAwaiter() { lock (syncLock) { var myTcs = (TaskCompletionSourceWrapper)tcs; var myTask = (Task)myTcs.Task; return myTask.GetAwaiter(); } } /// /// Gets a that represents the current awaitable operation. /// public new Task Task { get { lock (syncLock) { var myTcs = (TaskCompletionSourceWrapper)tcs; var myTask = (Task)myTcs.Task; return myTask; } } } /// /// Performs an implicit conversion from to /// . /// /// The instance to cast. /// A that represents the current awaitable operation. public static implicit operator Task(DelayedTaskWithResult delayedTask) => delayedTask.Task; /// /// Adds an unhandled exception handler to this instance. /// /// The action that handles an exception. /// The current instance. public new DelayedTaskWithResult WithExceptionHandler(Action exceptionHandler) { this.exceptionHandler = exceptionHandler; return this; } } #endregion Generic derived classes }