diff --git a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs index cc825ca6a..d6a5bd3bb 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui; public class AnsiRequestScheduler(IAnsiResponseParser parser) { - private readonly List _requests = new (); + private readonly List> _requests = new (); /// /// @@ -24,6 +24,13 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser) private TimeSpan _throttle = TimeSpan.FromMilliseconds (100); private TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100); + /// + /// 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. + /// + private TimeSpan _staleTimeout = TimeSpan.FromSeconds (5); + /// /// Sends the immediately or queues it if there is already /// an outstanding request for the given . @@ -32,17 +39,51 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser) /// if request was sent immediately. if it was queued. 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; + } + + /// + /// Looks to see if the last time we sent + /// 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 ). + /// + /// + /// + 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; } - return !parser.IsExpecting (r.Terminator); + if (parser.IsExpecting (r.Terminator)) + { + reason = ReasonCannotSend.OutstandingRequest; + return false; + } + + reason = default; + return true; } private bool ShouldThrottle (AnsiEscapeSequenceRequest r) @@ -102,3 +151,22 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser) return false; } } + +internal enum ReasonCannotSend +{ + /// + /// No reason given. + /// + None = 0, + + /// + /// The parser is already waiting for a request to complete with the given terminator. + /// + OutstandingRequest, + + /// + /// There have been too many requests sent recently, new requests will be put into + /// queue to prevent console becoming unresponsive. + /// + TooManyRequests +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index d0c43acc0..ab142226c 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -224,10 +224,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser ResetState (); } - /// - /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is - /// completed. - /// + /// public void ExpectResponse (string terminator, Action response) { expectedResponses.Add ((terminator, response)); } /// @@ -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()); } + + /// + public void StopExpecting (string requestTerminator) + { + expectedResponses.RemoveAll (r => r.terminator == requestTerminator); + } } internal class AnsiResponseParser : AnsiResponseParserBase diff --git a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs index 00eac8208..4a6ee635b 100644 --- a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs @@ -1,9 +1,25 @@ #nullable enable namespace Terminal.Gui; +/// +/// When implemented in a derived class, allows watching an input stream of characters +/// (i.e. console input) for ANSI response sequences. +/// public interface IAnsiResponseParser { + /// + /// Current state of the parser based on what sequence of characters it has + /// read from the console input stream. + /// AnsiResponseParserState State { get; } + + /// + /// Notifies the parser that you are expecting a response to come in + /// with the given (i.e. because you have + /// sent an ANSI request out). + /// + /// The terminator you expect to see on response. + /// Callback to invoke when the response is seen in console input. void ExpectResponse (string terminator, Action response); /// @@ -13,4 +29,12 @@ public interface IAnsiResponseParser /// /// bool IsExpecting (string requestTerminator); + + /// + /// Removes callback and expectation that we will get a response for the + /// given . Use to give up on very old + /// requests e.g. if you want to send a different one with the same terminator. + /// + /// + void StopExpecting (string requestTerminator); }