#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;
}
}