TimedEvents.cs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. #nullable enable
  2. namespace Terminal.Gui.App;
  3. /// <summary>
  4. /// Manages scheduled timeouts (timed callbacks) for the application.
  5. /// <para>
  6. /// Allows scheduling of callbacks to be invoked after a specified delay, with optional repetition.
  7. /// Timeouts are stored in a sorted list by their scheduled execution time (UTC ticks).
  8. /// Thread-safe for concurrent access.
  9. /// </para>
  10. /// <para>
  11. /// Typical usage:
  12. /// <list type="number">
  13. /// <item>
  14. /// <description>Call <see cref="Add(TimeSpan, Func{bool})"/> to schedule a callback.</description>
  15. /// </item>
  16. /// <item>
  17. /// <description>
  18. /// Call <see cref="RunTimers"/> periodically (e.g., from the main loop) to execute due
  19. /// callbacks.
  20. /// </description>
  21. /// </item>
  22. /// <item>
  23. /// <description>Call <see cref="Remove"/> to cancel a scheduled timeout.</description>
  24. /// </item>
  25. /// </list>
  26. /// </para>
  27. /// </summary>
  28. public class TimedEvents : ITimedEvents
  29. {
  30. internal SortedList<long, Timeout> _timeouts = new ();
  31. private readonly object _timeoutsLockToken = new ();
  32. /// <summary>
  33. /// Gets the list of all timeouts sorted by the <see cref="TimeSpan"/> time ticks. A shorter limit time can be
  34. /// added at the end, but it will be called before an earlier addition that has a longer limit time.
  35. /// </summary>
  36. public SortedList<long, Timeout> Timeouts => _timeouts;
  37. /// <inheritdoc/>
  38. public event EventHandler<TimeoutEventArgs>? Added;
  39. /// <inheritdoc/>
  40. public void RunTimers ()
  41. {
  42. lock (_timeoutsLockToken)
  43. {
  44. if (_timeouts.Count > 0)
  45. {
  46. RunTimersImpl ();
  47. }
  48. }
  49. }
  50. /// <inheritdoc/>
  51. public bool Remove (object token)
  52. {
  53. lock (_timeoutsLockToken)
  54. {
  55. int idx = _timeouts.IndexOfValue ((token as Timeout)!);
  56. if (idx == -1)
  57. {
  58. return false;
  59. }
  60. _timeouts.RemoveAt (idx);
  61. }
  62. return true;
  63. }
  64. /// <inheritdoc/>
  65. public object Add (TimeSpan time, Func<bool> callback)
  66. {
  67. ArgumentNullException.ThrowIfNull (callback);
  68. var timeout = new Timeout { Span = time, Callback = callback };
  69. AddTimeout (time, timeout);
  70. return timeout;
  71. }
  72. /// <inheritdoc/>
  73. public object Add (Timeout timeout)
  74. {
  75. AddTimeout (timeout.Span, timeout);
  76. return timeout;
  77. }
  78. /// <inheritdoc/>
  79. public bool CheckTimers (out int waitTimeout)
  80. {
  81. long now = DateTime.UtcNow.Ticks;
  82. waitTimeout = 0;
  83. lock (_timeoutsLockToken)
  84. {
  85. if (_timeouts.Count > 0)
  86. {
  87. waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond);
  88. if (waitTimeout < 0)
  89. {
  90. // This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected
  91. // This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0
  92. // and no event occurred in elapsed time when the 'poll' is start running again.
  93. waitTimeout = 0;
  94. }
  95. return true;
  96. }
  97. // ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if
  98. // the timeout is -1.
  99. waitTimeout = -1;
  100. }
  101. return false;
  102. }
  103. private void AddTimeout (TimeSpan time, Timeout timeout)
  104. {
  105. lock (_timeoutsLockToken)
  106. {
  107. long k = (DateTime.UtcNow + time).Ticks;
  108. // if user wants to run as soon as possible set timer such that it expires right away (no race conditions)
  109. if (time == TimeSpan.Zero)
  110. {
  111. k -= 100;
  112. }
  113. _timeouts.Add (NudgeToUniqueKey (k), timeout);
  114. Added?.Invoke (this, new (timeout, k));
  115. }
  116. }
  117. /// <summary>
  118. /// Finds the closest number to <paramref name="k"/> that is not present in <see cref="_timeouts"/>
  119. /// (incrementally).
  120. /// </summary>
  121. /// <param name="k"></param>
  122. /// <returns></returns>
  123. private long NudgeToUniqueKey (long k)
  124. {
  125. lock (_timeoutsLockToken)
  126. {
  127. while (_timeouts.ContainsKey (k))
  128. {
  129. k++;
  130. }
  131. }
  132. return k;
  133. }
  134. private void RunTimersImpl ()
  135. {
  136. long now = DateTime.UtcNow.Ticks;
  137. SortedList<long, Timeout> copy;
  138. // lock prevents new timeouts being added
  139. // after we have taken the copy but before
  140. // we have allocated a new list (which would
  141. // result in lost timeouts or errors during enumeration)
  142. lock (_timeoutsLockToken)
  143. {
  144. copy = _timeouts;
  145. _timeouts = new ();
  146. }
  147. foreach ((long k, Timeout timeout) in copy)
  148. {
  149. if (k < now)
  150. {
  151. if (timeout.Callback ())
  152. {
  153. AddTimeout (timeout.Span, timeout);
  154. }
  155. }
  156. else
  157. {
  158. lock (_timeoutsLockToken)
  159. {
  160. _timeouts.Add (NudgeToUniqueKey (k), timeout);
  161. }
  162. }
  163. }
  164. }
  165. }