AnsiRequestScheduler.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. using System.Collections.Concurrent;
  2. namespace Terminal.Gui.Drivers;
  3. /// <summary>
  4. /// Manages <see cref="AnsiEscapeSequenceRequest"/> made to an <see cref="IAnsiResponseParser"/>.
  5. /// Ensures there are not 2+ outstanding requests with the same terminator, throttles request sends
  6. /// to prevent console becoming unresponsive and handles evicting ignored requests (no reply from
  7. /// terminal).
  8. /// </summary>
  9. public class AnsiRequestScheduler
  10. {
  11. private readonly IAnsiResponseParser _parser;
  12. /// <summary>
  13. /// Function for returning the current time. Use in unit tests to
  14. /// ensure repeatable tests.
  15. /// </summary>
  16. internal Func<DateTime> Now { get; set; }
  17. private readonly HashSet<Tuple<AnsiEscapeSequenceRequest, DateTime>> _queuedRequests = new ();
  18. internal IReadOnlyCollection<AnsiEscapeSequenceRequest> QueuedRequests => _queuedRequests.Select (r => r.Item1).ToList ();
  19. /// <summary>
  20. /// <para>
  21. /// Dictionary where key is ansi request terminator and value is when we last sent a request for
  22. /// this terminator. Combined with <see cref="_throttle"/> this prevents hammering the console
  23. /// with too many requests in sequence which can cause console to freeze as there is no space for
  24. /// regular screen drawing / mouse events etc to come in.
  25. /// </para>
  26. /// <para>
  27. /// When user exceeds the throttle, new requests accumulate in <see cref="_queuedRequests"/> (i.e. remain
  28. /// queued).
  29. /// </para>
  30. /// </summary>
  31. private readonly ConcurrentDictionary<string, DateTime> _lastSend = new ();
  32. /// <summary>
  33. /// Number of milliseconds after sending a request that we allow
  34. /// another request to go out.
  35. /// </summary>
  36. private readonly TimeSpan _throttle = TimeSpan.FromMilliseconds (100);
  37. private readonly TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100);
  38. /// <summary>
  39. /// If console has not responded to a request after this period of time, we assume that it is never going
  40. /// to respond. Only affects when we try to send a new request with the same terminator - at which point
  41. /// we tell the parser to stop expecting the old request and start expecting the new request.
  42. /// </summary>
  43. private readonly TimeSpan _staleTimeout = TimeSpan.FromSeconds (1);
  44. private readonly DateTime _lastRun;
  45. /// <summary>
  46. /// Creates a new instance.
  47. /// </summary>
  48. /// <param name="parser"></param>
  49. /// <param name="now"></param>
  50. public AnsiRequestScheduler (IAnsiResponseParser parser, Func<DateTime>? now = null)
  51. {
  52. _parser = parser;
  53. Now = now ?? (() => DateTime.Now);
  54. _lastRun = Now ();
  55. }
  56. /// <summary>
  57. /// Sends the <paramref name="request"/> immediately or queues it if there is already
  58. /// an outstanding request for the given <see cref="AnsiEscapeSequence.Terminator"/>.
  59. /// </summary>
  60. /// <param name="driverImpl"></param>
  61. /// <param name="driver"></param>
  62. /// <param name="request"></param>
  63. /// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns>
  64. public bool SendOrSchedule (IDriver? driver, AnsiEscapeSequenceRequest request) { return SendOrSchedule (driver, request, true); }
  65. private bool SendOrSchedule (IDriver? driver, AnsiEscapeSequenceRequest request, bool addToQueue)
  66. {
  67. if (CanSend (request, out ReasonCannotSend reason))
  68. {
  69. Send (driver, request);
  70. return true;
  71. }
  72. if (reason == ReasonCannotSend.OutstandingRequest)
  73. {
  74. // If we can evict an old request (no response from terminal after ages)
  75. if (EvictStaleRequests (request.Terminator))
  76. {
  77. // Try again after evicting
  78. if (CanSend (request, out _))
  79. {
  80. Send (driver, request);
  81. return true;
  82. }
  83. }
  84. }
  85. if (addToQueue)
  86. {
  87. _queuedRequests.Add (Tuple.Create (request, Now ()));
  88. }
  89. return false;
  90. }
  91. private void EvictStaleRequests ()
  92. {
  93. foreach (string? stale in _lastSend.Where (v => IsStale (v.Value)).Select (k => k.Key))
  94. {
  95. EvictStaleRequests (stale);
  96. }
  97. }
  98. private bool IsStale (DateTime dt) { return Now () - dt > _staleTimeout; }
  99. /// <summary>
  100. /// Looks to see if the last time we sent <paramref name="withTerminator"/>
  101. /// is a long time ago. If so we assume that we will never get a response and
  102. /// can proceed with a new request for this terminator (returning <see langword="true"/>).
  103. /// </summary>
  104. /// <param name="withTerminator"></param>
  105. /// <returns></returns>
  106. private bool EvictStaleRequests (string? withTerminator)
  107. {
  108. if (_lastSend.TryGetValue (withTerminator!, out DateTime dt))
  109. {
  110. if (IsStale (dt))
  111. {
  112. _parser.StopExpecting (withTerminator, false);
  113. return true;
  114. }
  115. }
  116. return false;
  117. }
  118. /// <summary>
  119. /// Identifies and runs any <see cref="_queuedRequests"/> that can be sent based on the
  120. /// current outstanding requests of the parser.
  121. /// </summary>
  122. /// <param name="driver"></param>
  123. /// <param name="force">
  124. /// Repeated requests to run the schedule over short period of time will be ignored.
  125. /// Pass <see langword="true"/> to override this behaviour and force evaluation of outstanding requests.
  126. /// </param>
  127. /// <returns>
  128. /// <see langword="true"/> if a request was found and run. <see langword="false"/>
  129. /// if no outstanding requests or all have existing outstanding requests underway in parser.
  130. /// </returns>
  131. public bool RunSchedule (IDriver? driver, bool force = false)
  132. {
  133. if (!force && Now () - _lastRun < _runScheduleThrottle)
  134. {
  135. return false;
  136. }
  137. // Get oldest request
  138. Tuple<AnsiEscapeSequenceRequest, DateTime>? opportunity = _queuedRequests.MinBy (r => r.Item2);
  139. if (opportunity != null)
  140. {
  141. // Give it another go
  142. if (SendOrSchedule (driver, opportunity.Item1, false))
  143. {
  144. _queuedRequests.Remove (opportunity);
  145. return true;
  146. }
  147. }
  148. EvictStaleRequests ();
  149. return false;
  150. }
  151. private void Send (IDriver? driver, AnsiEscapeSequenceRequest r)
  152. {
  153. _lastSend.AddOrUpdate (r.Terminator!, _ => Now (), (_, _) => Now ());
  154. _parser.ExpectResponse (r.Terminator, r.ResponseReceived, r.Abandoned, false);
  155. r.Send (driver);
  156. }
  157. private bool CanSend (AnsiEscapeSequenceRequest r, out ReasonCannotSend reason)
  158. {
  159. if (ShouldThrottle (r))
  160. {
  161. reason = ReasonCannotSend.TooManyRequests;
  162. return false;
  163. }
  164. if (_parser.IsExpecting (r.Terminator))
  165. {
  166. reason = ReasonCannotSend.OutstandingRequest;
  167. return false;
  168. }
  169. reason = default (ReasonCannotSend);
  170. return true;
  171. }
  172. private bool ShouldThrottle (AnsiEscapeSequenceRequest r)
  173. {
  174. if (_lastSend.TryGetValue (r.Terminator!, out DateTime value))
  175. {
  176. return Now () - value < _throttle;
  177. }
  178. return false;
  179. }
  180. }