forked from gui-cs/Terminal.Gui
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request gui-cs#3791 from tznind/ansi-parser
Fixes gui-cs#3767 - Adds Ansi parser and scheduler.
- Loading branch information
Showing
58 changed files
with
2,838 additions
and
593 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
#nullable enable | ||
namespace Terminal.Gui; | ||
|
||
/// <summary> | ||
/// Describes an Ansi escape sequence. This is a 'blueprint'. If you | ||
/// want to send the sequence you should instead use <see cref="AnsiEscapeSequenceRequest"/> | ||
/// </summary> | ||
public class AnsiEscapeSequence | ||
{ | ||
/// <summary> | ||
/// Request to send e.g. see | ||
/// <see> | ||
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref> | ||
/// </see> | ||
/// </summary> | ||
public required string Request { get; init; } | ||
|
||
/// <summary> | ||
/// <para> | ||
/// The terminator that uniquely identifies the type of response as responded | ||
/// by the console. e.g. for | ||
/// <see> | ||
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref> | ||
/// </see> | ||
/// the terminator is | ||
/// <see> | ||
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref> | ||
/// </see> | ||
/// . | ||
/// </para> | ||
/// <para> | ||
/// After sending a request, the first response with matching terminator will be matched | ||
/// to the oldest outstanding request. | ||
/// </para> | ||
/// </summary> | ||
public required string Terminator { get; init; } | ||
|
||
|
||
|
||
/// <summary> | ||
/// The value expected in the response e.g. | ||
/// <see> | ||
/// <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref> | ||
/// </see> | ||
/// which will have a 't' as terminator but also other different request may return the same terminator with a | ||
/// different value. | ||
/// </summary> | ||
public string? Value { get; init; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
#nullable enable | ||
namespace Terminal.Gui; | ||
|
||
/// <summary> | ||
/// Describes an ongoing ANSI request sent to the console. | ||
/// Use <see cref="ResponseReceived"/> to handle the response | ||
/// when console answers the request. | ||
/// </summary> | ||
public class AnsiEscapeSequenceRequest : AnsiEscapeSequence | ||
{ | ||
/// <summary> | ||
/// Invoked when the console responds with an ANSI response code that matches the | ||
/// <see cref="AnsiEscapeSequence.Terminator"/> | ||
/// </summary> | ||
public required Action<string> ResponseReceived { get; init; } | ||
|
||
/// <summary> | ||
/// Invoked if the console fails to responds to the ANSI response code | ||
/// </summary> | ||
public Action? Abandoned { get; init; } | ||
|
||
|
||
/// <summary> | ||
/// Sends the <see cref="Request"/> to the raw output stream of the current <see cref="ConsoleDriver"/>. | ||
/// Only call this method from the main UI thread. You should use <see cref="AnsiRequestScheduler"/> if | ||
/// sending many requests. | ||
/// </summary> | ||
public void Send () { Application.Driver?.WriteRaw (Request); } | ||
|
||
} |
216 changes: 216 additions & 0 deletions
216
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
#nullable enable | ||
using System.Collections.Concurrent; | ||
|
||
namespace Terminal.Gui; | ||
|
||
/// <summary> | ||
/// Manages <see cref="AnsiEscapeSequenceRequest"/> made to an <see cref="IAnsiResponseParser"/>. | ||
/// 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). | ||
/// </summary> | ||
public class AnsiRequestScheduler | ||
{ | ||
private readonly IAnsiResponseParser _parser; | ||
|
||
/// <summary> | ||
/// Function for returning the current time. Use in unit tests to | ||
/// ensure repeatable tests. | ||
/// </summary> | ||
internal Func<DateTime> Now { get; set; } | ||
|
||
private readonly HashSet<Tuple<AnsiEscapeSequenceRequest, DateTime>> _queuedRequests = new (); | ||
|
||
internal IReadOnlyCollection<AnsiEscapeSequenceRequest> QueuedRequests => _queuedRequests.Select (r => r.Item1).ToList (); | ||
|
||
/// <summary> | ||
/// <para> | ||
/// Dictionary where key is ansi request terminator and value is when we last sent a request for | ||
/// this terminator. Combined with <see cref="_throttle"/> 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. | ||
/// </para> | ||
/// <para> | ||
/// When user exceeds the throttle, new requests accumulate in <see cref="_queuedRequests"/> (i.e. remain | ||
/// queued). | ||
/// </para> | ||
/// </summary> | ||
private readonly ConcurrentDictionary<string, DateTime> _lastSend = new (); | ||
|
||
/// <summary> | ||
/// Number of milliseconds after sending a request that we allow | ||
/// another request to go out. | ||
/// </summary> | ||
private readonly TimeSpan _throttle = TimeSpan.FromMilliseconds (100); | ||
|
||
private readonly TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100); | ||
|
||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
private readonly TimeSpan _staleTimeout = TimeSpan.FromSeconds (1); | ||
|
||
private readonly DateTime _lastRun; | ||
|
||
/// <summary> | ||
/// Creates a new instance. | ||
/// </summary> | ||
/// <param name="parser"></param> | ||
/// <param name="now"></param> | ||
public AnsiRequestScheduler (IAnsiResponseParser parser, Func<DateTime>? now = null) | ||
{ | ||
_parser = parser; | ||
Now = now ?? (() => DateTime.Now); | ||
_lastRun = Now (); | ||
} | ||
|
||
/// <summary> | ||
/// Sends the <paramref name="request"/> immediately or queues it if there is already | ||
/// an outstanding request for the given <see cref="AnsiEscapeSequenceRequest.Terminator"/>. | ||
/// </summary> | ||
/// <param name="request"></param> | ||
/// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns> | ||
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; } | ||
|
||
/// <summary> | ||
/// Looks to see if the last time we sent <paramref name="withTerminator"/> | ||
/// 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 <see langword="true"/>). | ||
/// </summary> | ||
/// <param name="withTerminator"></param> | ||
/// <returns></returns> | ||
private bool EvictStaleRequests (string withTerminator) | ||
{ | ||
if (_lastSend.TryGetValue (withTerminator, out DateTime dt)) | ||
{ | ||
if (IsStale (dt)) | ||
{ | ||
_parser.StopExpecting (withTerminator, false); | ||
|
||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/// <summary> | ||
/// Identifies and runs any <see cref="_queuedRequests"/> that can be sent based on the | ||
/// current outstanding requests of the parser. | ||
/// </summary> | ||
/// <param name="force"> | ||
/// Repeated requests to run the schedule over short period of time will be ignored. | ||
/// Pass <see langword="true"/> to override this behaviour and force evaluation of outstanding requests. | ||
/// </param> | ||
/// <returns> | ||
/// <see langword="true"/> if a request was found and run. <see langword="false"/> | ||
/// if no outstanding requests or all have existing outstanding requests underway in parser. | ||
/// </returns> | ||
public bool RunSchedule (bool force = false) | ||
{ | ||
if (!force && Now () - _lastRun < _runScheduleThrottle) | ||
{ | ||
return false; | ||
} | ||
|
||
// Get oldest request | ||
Tuple<AnsiEscapeSequenceRequest, DateTime>? 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; | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#nullable enable | ||
namespace Terminal.Gui; | ||
|
||
internal record AnsiResponseExpectation (string Terminator, Action<IHeld> Response, Action? Abandoned) | ||
{ | ||
public bool Matches (string cur) { return cur.EndsWith (Terminator); } | ||
} |
Oops, something went wrong.