#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).
///
internal class AnsiRequestScheduler
{
private readonly IAnsiResponseParser _parser;
private readonly Func _now;
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 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 (5);
private readonly DateTime _lastRun;
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)
{
if (CanSend (request, out ReasonCannotSend reason))
{
Send (request);
return true;
}
if (reason == ReasonCannotSend.OutstandingRequest)
{
EvictStaleRequests (request.Terminator);
// Try again after evicting
if (CanSend (request, out _))
{
Send (request);
return true;
}
}
_requests.Add (Tuple.Create (request, _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 DateTime dt))
{
if (_now () - dt > _staleTimeout)
{
_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;
}
Tuple? 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, _ => _now (), (_, _) => _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 (ReasonCannotSend);
return true;
}
private bool ShouldThrottle (AnsiEscapeSequenceRequest r)
{
if (_lastSend.TryGetValue (r.Terminator, out DateTime value))
{
return _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
}