#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui; /// /// Manages made to an . /// Ensures there are not 2+ outstanding requests with the same terminator, throttles request sends /// to prevent console becoming unresponsive and handles evicting ignored requests (no reply from /// terminal). /// public class AnsiRequestScheduler { private readonly IAnsiResponseParser _parser; /// /// Function for returning the current time. Use in unit tests to /// ensure repeatable tests. /// internal Func Now { get; set; } private readonly HashSet> _queuedRequests = new (); internal IReadOnlyCollection QueuedRequests => _queuedRequests.Select (r => r.Item1).ToList (); /// /// /// 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 readonly ConcurrentDictionary _lastSend = new (); /// /// Number of milliseconds after sending a request that we allow /// another request to go out. /// private readonly TimeSpan _throttle = TimeSpan.FromMilliseconds (100); private readonly 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 readonly TimeSpan _staleTimeout = TimeSpan.FromSeconds (1); private readonly DateTime _lastRun; /// /// Creates a new instance. /// /// /// public AnsiRequestScheduler (IAnsiResponseParser parser, Func? now = null) { _parser = parser; Now = now ?? (() => DateTime.Now); _lastRun = Now (); } /// /// 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) { return SendOrSchedule (request, true); } private bool SendOrSchedule (AnsiEscapeSequenceRequest request, bool addToQueue) { if (CanSend (request, out ReasonCannotSend reason)) { Send (request); return true; } if (reason == ReasonCannotSend.OutstandingRequest) { // If we can evict an old request (no response from terminal after ages) if (EvictStaleRequests (request.Terminator)) { // Try again after evicting if (CanSend (request, out _)) { Send (request); return true; } } } if (addToQueue) { _queuedRequests.Add (Tuple.Create (request, Now ())); } return false; } private void EvictStaleRequests () { foreach (string stale in _lastSend.Where (v => IsStale (v.Value)).Select (k => k.Key)) { EvictStaleRequests (stale); } } private bool IsStale (DateTime dt) { return Now () - dt > _staleTimeout; } /// /// 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 DateTime dt)) { if (IsStale (dt)) { _parser.StopExpecting (withTerminator, false); return true; } } return false; } /// /// 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 && Now () - _lastRun < _runScheduleThrottle) { return false; } // Get oldest request Tuple? opportunity = _queuedRequests.MinBy (r => r.Item2); if (opportunity != null) { // Give it another go if (SendOrSchedule (opportunity.Item1, false)) { _queuedRequests.Remove (opportunity); return true; } } EvictStaleRequests (); return false; } private void Send (AnsiEscapeSequenceRequest r) { _lastSend.AddOrUpdate (r.Terminator, _ => Now (), (_, _) => Now ()); _parser.ExpectResponse (r.Terminator, r.ResponseReceived, r.Abandoned, 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 (ReasonCannotSend); return true; } private bool ShouldThrottle (AnsiEscapeSequenceRequest r) { if (_lastSend.TryGetValue (r.Terminator, out DateTime value)) { return Now () - value < _throttle; } return false; } }