AnsiRequestScheduler.cs 7.5 KB

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