#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui; public class AnsiRequestScheduler (IAnsiResponseParser parser) { private readonly List> _requests = new (); /// /// /// Dictionary where key is ansi request terminator and value is when we last sent a request for /// this terminator. Combined with this prevents hammering the console /// with too many requests in sequence which can cause console to freeze as there is no space for /// regular screen drawing / mouse events etc to come in. /// /// /// When user exceeds the throttle, new requests accumulate in (i.e. remain /// queued). /// /// private ConcurrentDictionary _lastSend = new (); 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 . /// /// /// if request was sent immediately. if it was queued. public bool SendOrSchedule (AnsiEscapeSequenceRequest request) { if (CanSend (request, out var reason)) { Send (request); return true; } if (reason == ReasonCannotSend.OutstandingRequest) { 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)) { if (DateTime.Now - dt > _staleTimeout) { parser.StopExpecting (withTerminator, false); return true; } } return false; } private DateTime _lastRun = DateTime.Now; /// /// Identifies and runs any that can be sent based on the /// current outstanding requests of the parser. /// /// Repeated requests to run the schedule over short period of time will be ignored. /// Pass to override this behaviour and force evaluation of outstanding requests. /// if a request was found and run. /// if no outstanding requests or all have existing outstanding requests underway in parser. public bool RunSchedule (bool force = false) { if (!force && DateTime.Now - _lastRun < _runScheduleThrottle) { return false; } var opportunity = _requests.FirstOrDefault (r => CanSend (r.Item1, out _)); if (opportunity != null) { _requests.Remove (opportunity); Send (opportunity.Item1); return true; } return false; } private void Send (AnsiEscapeSequenceRequest r) { _lastSend.AddOrUpdate (r.Terminator, (s) => DateTime.Now, (s, v) => DateTime.Now); parser.ExpectResponse (r.Terminator, r.ResponseReceived, false); r.Send (); } 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; } reason = default; return true; } private bool ShouldThrottle (AnsiEscapeSequenceRequest r) { if (_lastSend.TryGetValue (r.Terminator, out DateTime value)) { return DateTime.Now - value < _throttle; } 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 }