AnsiRequestScheduler.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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. private readonly Func<DateTime> _now;
  14. private readonly List<Tuple<AnsiEscapeSequenceRequest, DateTime>> _requests = new ();
  15. /// <summary>
  16. /// <para>
  17. /// Dictionary where key is ansi request terminator and value is when we last sent a request for
  18. /// this terminator. Combined with <see cref="_throttle"/> this prevents hammering the console
  19. /// with too many requests in sequence which can cause console to freeze as there is no space for
  20. /// regular screen drawing / mouse events etc to come in.
  21. /// </para>
  22. /// <para>
  23. /// When user exceeds the throttle, new requests accumulate in <see cref="_requests"/> (i.e. remain
  24. /// queued).
  25. /// </para>
  26. /// </summary>
  27. private readonly ConcurrentDictionary<string, DateTime> _lastSend = new ();
  28. /// <summary>
  29. /// Number of milliseconds after sending a request that we allow
  30. /// another request to go out.
  31. /// </summary>
  32. private readonly TimeSpan _throttle = TimeSpan.FromMilliseconds (100);
  33. private readonly TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100);
  34. /// <summary>
  35. /// If console has not responded to a request after this period of time, we assume that it is never going
  36. /// to respond. Only affects when we try to send a new request with the same terminator - at which point
  37. /// we tell the parser to stop expecting the old request and start expecting the new request.
  38. /// </summary>
  39. private readonly TimeSpan _staleTimeout = TimeSpan.FromSeconds (5);
  40. private readonly DateTime _lastRun;
  41. public AnsiRequestScheduler (IAnsiResponseParser parser, Func<DateTime>? now = null)
  42. {
  43. _parser = parser;
  44. _now = now ?? (() => DateTime.Now);
  45. _lastRun = _now ();
  46. }
  47. /// <summary>
  48. /// Sends the <paramref name="request"/> immediately or queues it if there is already
  49. /// an outstanding request for the given <see cref="AnsiEscapeSequenceRequest.Terminator"/>.
  50. /// </summary>
  51. /// <param name="request"></param>
  52. /// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns>
  53. public bool SendOrSchedule (AnsiEscapeSequenceRequest request)
  54. {
  55. if (CanSend (request, out ReasonCannotSend reason))
  56. {
  57. Send (request);
  58. return true;
  59. }
  60. if (reason == ReasonCannotSend.OutstandingRequest)
  61. {
  62. EvictStaleRequests (request.Terminator);
  63. // Try again after evicting
  64. if (CanSend (request, out _))
  65. {
  66. Send (request);
  67. return true;
  68. }
  69. }
  70. _requests.Add (Tuple.Create (request, _now ()));
  71. return false;
  72. }
  73. /// <summary>
  74. /// Looks to see if the last time we sent <paramref name="withTerminator"/>
  75. /// is a long time ago. If so we assume that we will never get a response and
  76. /// can proceed with a new request for this terminator (returning <see langword="true"/>).
  77. /// </summary>
  78. /// <param name="withTerminator"></param>
  79. /// <returns></returns>
  80. private bool EvictStaleRequests (string withTerminator)
  81. {
  82. if (_lastSend.TryGetValue (withTerminator, out DateTime dt))
  83. {
  84. if (_now () - dt > _staleTimeout)
  85. {
  86. _parser.StopExpecting (withTerminator, false);
  87. return true;
  88. }
  89. }
  90. return false;
  91. }
  92. /// <summary>
  93. /// Identifies and runs any <see cref="_requests"/> that can be sent based on the
  94. /// current outstanding requests of the parser.
  95. /// </summary>
  96. /// <param name="force">
  97. /// Repeated requests to run the schedule over short period of time will be ignored.
  98. /// Pass <see langword="true"/> to override this behaviour and force evaluation of outstanding requests.
  99. /// </param>
  100. /// <returns>
  101. /// <see langword="true"/> if a request was found and run. <see langword="false"/>
  102. /// if no outstanding requests or all have existing outstanding requests underway in parser.
  103. /// </returns>
  104. public bool RunSchedule (bool force = false)
  105. {
  106. if (!force && _now () - _lastRun < _runScheduleThrottle)
  107. {
  108. return false;
  109. }
  110. Tuple<AnsiEscapeSequenceRequest, DateTime>? opportunity = _requests.FirstOrDefault (r => CanSend (r.Item1, out _));
  111. if (opportunity != null)
  112. {
  113. _requests.Remove (opportunity);
  114. Send (opportunity.Item1);
  115. return true;
  116. }
  117. return false;
  118. }
  119. private void Send (AnsiEscapeSequenceRequest r)
  120. {
  121. _lastSend.AddOrUpdate (r.Terminator, _ => _now (), (_, _) => _now ());
  122. _parser.ExpectResponse (r.Terminator, r.ResponseReceived, false);
  123. r.Send ();
  124. }
  125. private bool CanSend (AnsiEscapeSequenceRequest r, out ReasonCannotSend reason)
  126. {
  127. if (ShouldThrottle (r))
  128. {
  129. reason = ReasonCannotSend.TooManyRequests;
  130. return false;
  131. }
  132. if (_parser.IsExpecting (r.Terminator))
  133. {
  134. reason = ReasonCannotSend.OutstandingRequest;
  135. return false;
  136. }
  137. reason = default (ReasonCannotSend);
  138. return true;
  139. }
  140. private bool ShouldThrottle (AnsiEscapeSequenceRequest r)
  141. {
  142. if (_lastSend.TryGetValue (r.Terminator, out DateTime value))
  143. {
  144. return _now () - value < _throttle;
  145. }
  146. return false;
  147. }
  148. }
  149. internal enum ReasonCannotSend
  150. {
  151. /// <summary>
  152. /// No reason given.
  153. /// </summary>
  154. None = 0,
  155. /// <summary>
  156. /// The parser is already waiting for a request to complete with the given terminator.
  157. /// </summary>
  158. OutstandingRequest,
  159. /// <summary>
  160. /// There have been too many requests sent recently, new requests will be put into
  161. /// queue to prevent console becoming unresponsive.
  162. /// </summary>
  163. TooManyRequests
  164. }