Browse Source

Add evicting and xmldoc

tznind 9 months ago
parent
commit
23af0d9fd3

+ 78 - 10
Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs

@@ -5,7 +5,7 @@ namespace Terminal.Gui;
 
 public class AnsiRequestScheduler(IAnsiResponseParser parser)
 {
-    private readonly List<AnsiEscapeSequenceRequest> _requests = new  ();
+    private readonly List<Tuple<AnsiEscapeSequenceRequest,DateTime>> _requests = new  ();
 
     /// <summary>
     ///<para>
@@ -24,6 +24,13 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
     private TimeSpan _throttle = TimeSpan.FromMilliseconds (100);
     private TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100);
 
+    /// <summary>
+    /// If console has not responded to a request after this period of time, we assume that it is never going
+    /// to respond. Only affects when we try to send a new request with the same terminator - at which point
+    /// we tell the parser to stop expecting the old request and start expecting the new request.
+    /// </summary>
+    private TimeSpan _staleTimeout = TimeSpan.FromSeconds (5);
+
     /// <summary>
     /// Sends the <paramref name="request"/> immediately or queues it if there is already
     /// an outstanding request for the given <see cref="AnsiEscapeSequenceRequest.Terminator"/>.
@@ -32,17 +39,51 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
     /// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns>
     public bool SendOrSchedule (AnsiEscapeSequenceRequest request )
     {
-        if (CanSend(request))
+
+        if (CanSend(request, out var reason))
         {
             Send (request);
-
             return true;
         }
-        else
+
+        if (reason == ReasonCannotSend.OutstandingRequest)
         {
-            _requests.Add (request);
-            return false;
+            EvictStaleRequests (request.Terminator);
+
+            // Try again after 
+            if (CanSend (request, out _))
+            {
+                Send (request);
+                return true;
+            }
         }
+
+        _requests.Add (Tuple.Create(request,DateTime.Now));
+        return false;
+    }
+
+    /// <summary>
+    /// Looks to see if the last time we sent <paramref name="withTerminator"/>
+    /// is a long time ago. If so we assume that we will never get a response and
+    /// can proceed with a new request for this terminator (returning <see langword="true"/>).
+    /// </summary>
+    /// <param name="withTerminator"></param>
+    /// <returns></returns>
+    private bool EvictStaleRequests (string withTerminator)
+    {
+        if (_lastSend.TryGetValue (withTerminator, out var dt))
+        {
+            // TODO: If debugging this can cause problem becuase we stop expecting response but one comes in anyway
+            // causing parser to ignore and it to fall through to default console iteration which typically crashes.
+            if (DateTime.Now - dt > _staleTimeout)
+            {
+                parser.StopExpecting (withTerminator);
+
+                return true;
+            }
+        }
+
+        return false;
     }
 
     private DateTime _lastRun = DateTime.Now;
@@ -62,12 +103,12 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
             return false;
         }
 
-        var opportunity = _requests.FirstOrDefault (CanSend);
+        var opportunity = _requests.FirstOrDefault (r=>CanSend(r.Item1, out _));
 
         if (opportunity != null)
         {
             _requests.Remove (opportunity);
-            Send (opportunity);
+            Send (opportunity.Item1);
 
             return true;
         }
@@ -82,14 +123,22 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
         r.Send ();
     }
 
-    public bool CanSend (AnsiEscapeSequenceRequest r)
+    private bool CanSend (AnsiEscapeSequenceRequest r, out ReasonCannotSend reason)
     {
         if (ShouldThrottle (r))
         {
+            reason = ReasonCannotSend.TooManyRequests;
+            return false;
+        }
+
+        if (parser.IsExpecting (r.Terminator))
+        {
+            reason = ReasonCannotSend.OutstandingRequest;
             return false;
         }
 
-        return !parser.IsExpecting (r.Terminator);
+        reason = default;
+        return true;
     }
 
     private bool ShouldThrottle (AnsiEscapeSequenceRequest r)
@@ -102,3 +151,22 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
         return false;
     }
 }
+
+internal enum ReasonCannotSend
+{
+    /// <summary>
+    /// No reason given.
+    /// </summary>
+    None = 0,
+
+    /// <summary>
+    /// The parser is already waiting for a request to complete with the given terminator.
+    /// </summary>
+    OutstandingRequest,
+
+    /// <summary>
+    /// There have been too many requests sent recently, new requests will be put into
+    /// queue to prevent console becoming unresponsive.
+    /// </summary>
+    TooManyRequests
+}

+ 7 - 4
Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs

@@ -224,10 +224,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         ResetState ();
     }
 
-    /// <summary>
-    ///     Registers a new expected ANSI response with a specific terminator and a callback for when the response is
-    ///     completed.
-    /// </summary>
+    /// <inheritdoc />
     public void ExpectResponse (string terminator, Action<string> response) { expectedResponses.Add ((terminator, response)); }
 
     /// <inheritdoc />
@@ -236,6 +233,12 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         // If any of the new terminator matches any existing terminators characters it's a collision so true.
         return expectedResponses.Any (r => r.terminator.Intersect (requestTerminator).Any());
     }
+
+    /// <inheritdoc />
+    public void StopExpecting (string requestTerminator)
+    {
+        expectedResponses.RemoveAll (r => r.terminator == requestTerminator);
+    }
 }
 
 internal class AnsiResponseParser<T> : AnsiResponseParserBase

+ 24 - 0
Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs

@@ -1,9 +1,25 @@
 #nullable enable
 namespace Terminal.Gui;
 
+/// <summary>
+/// When implemented in a derived class, allows watching an input stream of characters
+/// (i.e. console input) for ANSI response sequences.
+/// </summary>
 public interface IAnsiResponseParser
 {
+    /// <summary>
+    /// Current state of the parser based on what sequence of characters it has
+    /// read from the console input stream.
+    /// </summary>
     AnsiResponseParserState State { get; }
+
+    /// <summary>
+    /// Notifies the parser that you are expecting a response to come in
+    /// with the given <paramref name="terminator"/> (i.e. because you have
+    /// sent an ANSI request out).
+    /// </summary>
+    /// <param name="terminator">The terminator you expect to see on response.</param>
+    /// <param name="response">Callback to invoke when the response is seen in console input.</param>
     void ExpectResponse (string terminator, Action<string> response);
 
     /// <summary>
@@ -13,4 +29,12 @@ public interface IAnsiResponseParser
     /// <param name="requestTerminator"></param>
     /// <returns></returns>
     bool IsExpecting (string requestTerminator);
+
+    /// <summary>
+    /// Removes callback and expectation that we will get a response for the
+    /// given <pararef name="requestTerminator"/>. Use to give up on very old
+    /// requests e.g. if you want to send a different one with the same terminator.
+    /// </summary>
+    /// <param name="requestTerminator"></param>
+    void StopExpecting (string requestTerminator);
 }