1
+ #nullable enable
2
+ using System . Collections . Concurrent ;
3
+
4
+ namespace Terminal . Gui ;
5
+
6
+ /// <summary>
7
+ /// Manages <see cref="AnsiEscapeSequenceRequest"/> made to an <see cref="IAnsiResponseParser"/>.
8
+ /// Ensures there are not 2+ outstanding requests with the same terminator, throttles request sends
9
+ /// to prevent console becoming unresponsive and handles evicting ignored requests (no reply from
10
+ /// terminal).
11
+ /// </summary>
12
+ public class AnsiRequestScheduler
13
+ {
14
+ private readonly IAnsiResponseParser _parser ;
15
+
16
+ /// <summary>
17
+ /// Function for returning the current time. Use in unit tests to
18
+ /// ensure repeatable tests.
19
+ /// </summary>
20
+ internal Func < DateTime > Now { get ; set ; }
21
+
22
+ private readonly HashSet < Tuple < AnsiEscapeSequenceRequest , DateTime > > _queuedRequests = new ( ) ;
23
+
24
+ internal IReadOnlyCollection < AnsiEscapeSequenceRequest > QueuedRequests => _queuedRequests . Select ( r => r . Item1 ) . ToList ( ) ;
25
+
26
+ /// <summary>
27
+ /// <para>
28
+ /// Dictionary where key is ansi request terminator and value is when we last sent a request for
29
+ /// this terminator. Combined with <see cref="_throttle"/> this prevents hammering the console
30
+ /// with too many requests in sequence which can cause console to freeze as there is no space for
31
+ /// regular screen drawing / mouse events etc to come in.
32
+ /// </para>
33
+ /// <para>
34
+ /// When user exceeds the throttle, new requests accumulate in <see cref="_queuedRequests"/> (i.e. remain
35
+ /// queued).
36
+ /// </para>
37
+ /// </summary>
38
+ private readonly ConcurrentDictionary < string , DateTime > _lastSend = new ( ) ;
39
+
40
+ /// <summary>
41
+ /// Number of milliseconds after sending a request that we allow
42
+ /// another request to go out.
43
+ /// </summary>
44
+ private readonly TimeSpan _throttle = TimeSpan . FromMilliseconds ( 100 ) ;
45
+
46
+ private readonly TimeSpan _runScheduleThrottle = TimeSpan . FromMilliseconds ( 100 ) ;
47
+
48
+ /// <summary>
49
+ /// If console has not responded to a request after this period of time, we assume that it is never going
50
+ /// to respond. Only affects when we try to send a new request with the same terminator - at which point
51
+ /// we tell the parser to stop expecting the old request and start expecting the new request.
52
+ /// </summary>
53
+ private readonly TimeSpan _staleTimeout = TimeSpan . FromSeconds ( 1 ) ;
54
+
55
+ private readonly DateTime _lastRun ;
56
+
57
+ /// <summary>
58
+ /// Creates a new instance.
59
+ /// </summary>
60
+ /// <param name="parser"></param>
61
+ /// <param name="now"></param>
62
+ public AnsiRequestScheduler ( IAnsiResponseParser parser , Func < DateTime > ? now = null )
63
+ {
64
+ _parser = parser ;
65
+ Now = now ?? ( ( ) => DateTime . Now ) ;
66
+ _lastRun = Now ( ) ;
67
+ }
68
+
69
+ /// <summary>
70
+ /// Sends the <paramref name="request"/> immediately or queues it if there is already
71
+ /// an outstanding request for the given <see cref="AnsiEscapeSequenceRequest.Terminator"/>.
72
+ /// </summary>
73
+ /// <param name="request"></param>
74
+ /// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns>
75
+ public bool SendOrSchedule ( AnsiEscapeSequenceRequest request ) { return SendOrSchedule ( request , true ) ; }
76
+
77
+ private bool SendOrSchedule ( AnsiEscapeSequenceRequest request , bool addToQueue )
78
+ {
79
+ if ( CanSend ( request , out ReasonCannotSend reason ) )
80
+ {
81
+ Send ( request ) ;
82
+
83
+ return true ;
84
+ }
85
+
86
+ if ( reason == ReasonCannotSend . OutstandingRequest )
87
+ {
88
+ // If we can evict an old request (no response from terminal after ages)
89
+ if ( EvictStaleRequests ( request . Terminator ) )
90
+ {
91
+ // Try again after evicting
92
+ if ( CanSend ( request , out _ ) )
93
+ {
94
+ Send ( request ) ;
95
+
96
+ return true ;
97
+ }
98
+ }
99
+ }
100
+
101
+ if ( addToQueue )
102
+ {
103
+ _queuedRequests . Add ( Tuple . Create ( request , Now ( ) ) ) ;
104
+ }
105
+
106
+ return false ;
107
+ }
108
+
109
+ private void EvictStaleRequests ( )
110
+ {
111
+ foreach ( string stale in _lastSend . Where ( v => IsStale ( v . Value ) ) . Select ( k => k . Key ) )
112
+ {
113
+ EvictStaleRequests ( stale ) ;
114
+ }
115
+ }
116
+
117
+ private bool IsStale ( DateTime dt ) { return Now ( ) - dt > _staleTimeout ; }
118
+
119
+ /// <summary>
120
+ /// Looks to see if the last time we sent <paramref name="withTerminator"/>
121
+ /// is a long time ago. If so we assume that we will never get a response and
122
+ /// can proceed with a new request for this terminator (returning <see langword="true"/>).
123
+ /// </summary>
124
+ /// <param name="withTerminator"></param>
125
+ /// <returns></returns>
126
+ private bool EvictStaleRequests ( string withTerminator )
127
+ {
128
+ if ( _lastSend . TryGetValue ( withTerminator , out DateTime dt ) )
129
+ {
130
+ if ( IsStale ( dt ) )
131
+ {
132
+ _parser . StopExpecting ( withTerminator , false ) ;
133
+
134
+ return true ;
135
+ }
136
+ }
137
+
138
+ return false ;
139
+ }
140
+
141
+ /// <summary>
142
+ /// Identifies and runs any <see cref="_queuedRequests"/> that can be sent based on the
143
+ /// current outstanding requests of the parser.
144
+ /// </summary>
145
+ /// <param name="force">
146
+ /// Repeated requests to run the schedule over short period of time will be ignored.
147
+ /// Pass <see langword="true"/> to override this behaviour and force evaluation of outstanding requests.
148
+ /// </param>
149
+ /// <returns>
150
+ /// <see langword="true"/> if a request was found and run. <see langword="false"/>
151
+ /// if no outstanding requests or all have existing outstanding requests underway in parser.
152
+ /// </returns>
153
+ public bool RunSchedule ( bool force = false )
154
+ {
155
+ if ( ! force && Now ( ) - _lastRun < _runScheduleThrottle )
156
+ {
157
+ return false ;
158
+ }
159
+
160
+ // Get oldest request
161
+ Tuple < AnsiEscapeSequenceRequest , DateTime > ? opportunity = _queuedRequests . MinBy ( r => r . Item2 ) ;
162
+
163
+ if ( opportunity != null )
164
+ {
165
+ // Give it another go
166
+ if ( SendOrSchedule ( opportunity . Item1 , false ) )
167
+ {
168
+ _queuedRequests . Remove ( opportunity ) ;
169
+
170
+ return true ;
171
+ }
172
+ }
173
+
174
+ EvictStaleRequests ( ) ;
175
+
176
+ return false ;
177
+ }
178
+
179
+ private void Send ( AnsiEscapeSequenceRequest r )
180
+ {
181
+ _lastSend . AddOrUpdate ( r . Terminator , _ => Now ( ) , ( _ , _ ) => Now ( ) ) ;
182
+ _parser . ExpectResponse ( r . Terminator , r . ResponseReceived , r . Abandoned , false ) ;
183
+ r . Send ( ) ;
184
+ }
185
+
186
+ private bool CanSend ( AnsiEscapeSequenceRequest r , out ReasonCannotSend reason )
187
+ {
188
+ if ( ShouldThrottle ( r ) )
189
+ {
190
+ reason = ReasonCannotSend . TooManyRequests ;
191
+
192
+ return false ;
193
+ }
194
+
195
+ if ( _parser . IsExpecting ( r . Terminator ) )
196
+ {
197
+ reason = ReasonCannotSend . OutstandingRequest ;
198
+
199
+ return false ;
200
+ }
201
+
202
+ reason = default ( ReasonCannotSend ) ;
203
+
204
+ return true ;
205
+ }
206
+
207
+ private bool ShouldThrottle ( AnsiEscapeSequenceRequest r )
208
+ {
209
+ if ( _lastSend . TryGetValue ( r . Terminator , out DateTime value ) )
210
+ {
211
+ return Now ( ) - value < _throttle ;
212
+ }
213
+
214
+ return false ;
215
+ }
216
+ }
0 commit comments