5
5
using System . Linq ;
6
6
using System . Management . Automation ;
7
7
using System . Management . Automation . Language ;
8
+ using System . Runtime . InteropServices ;
8
9
using System . Text ;
9
10
using System . Text . RegularExpressions ;
11
+ using System . Threading ;
10
12
11
13
namespace PSConsoleUtilities
12
14
{
@@ -65,6 +67,12 @@ public class PSConsoleReadLine
65
67
{
66
68
private static readonly PSConsoleReadLine _singleton ;
67
69
70
+ private static readonly GCHandle _breakHandlerGcHandle ;
71
+ private Thread _readKeyThread ;
72
+ private AutoResetEvent _readKeyWaitHandle ;
73
+ private AutoResetEvent _keyReadWaitHandle ;
74
+ private AutoResetEvent _closingWaitHandle ;
75
+ private WaitHandle [ ] _waitHandles ;
68
76
private bool _captureKeys ;
69
77
private readonly Queue < ConsoleKeyInfo > _savedKeys ;
70
78
private readonly HistoryQueue < string > _demoStrings ;
@@ -183,29 +191,77 @@ public PSConsoleReadlineOptions Options
183
191
184
192
#endregion Unit test only properties
185
193
186
- [ ExcludeFromCodeCoverage ]
187
- private static ConsoleKeyInfo ReadKey ( )
194
+ private void ReadKeyThreadProc ( )
188
195
{
189
- var start = DateTime . Now ;
190
- while ( Console . KeyAvailable )
196
+ while ( true )
191
197
{
192
- _singleton . _queuedKeys . Enqueue ( Console . ReadKey ( true ) ) ;
193
- if ( ( DateTime . Now - start ) . Milliseconds > 2 )
198
+ // Wait until ReadKey tells us to read a key.
199
+ _readKeyWaitHandle . WaitOne ( ) ;
200
+
201
+ var start = DateTime . Now ;
202
+ while ( Console . KeyAvailable )
194
203
{
195
- // Don't spend too long in this loop if there are lots of queued keys
196
- break ;
204
+ _queuedKeys . Enqueue ( Console . ReadKey ( true ) ) ;
205
+ if ( ( DateTime . Now - start ) . Milliseconds > 2 )
206
+ {
207
+ // Don't spend too long in this loop if there are lots of queued keys
208
+ break ;
209
+ }
197
210
}
211
+
212
+ var key = _queuedKeys . Count > 0
213
+ ? _queuedKeys . Dequeue ( )
214
+ : Console . ReadKey ( true ) ;
215
+ if ( _captureKeys )
216
+ {
217
+ _savedKeys . Enqueue ( key ) ;
218
+ }
219
+
220
+ _queuedKeys . Enqueue ( key ) ;
221
+
222
+ // One or more keys were read - let ReadKey know we're done.
223
+ _keyReadWaitHandle . Set ( ) ;
198
224
}
225
+ }
199
226
200
- var key = _singleton . _queuedKeys . Count > 0
201
- ? _singleton . _queuedKeys . Dequeue ( )
202
- : Console . ReadKey ( true ) ;
203
- if ( _singleton . _captureKeys )
227
+ [ ExcludeFromCodeCoverage ]
228
+ private static ConsoleKeyInfo ReadKey ( )
229
+ {
230
+ // Reading a key is handled on a different thread. During process shutdown,
231
+ // PowerShell will wait in it's ConsoleCtrlHandler until the pipeline has completed.
232
+ // If we're running, we're most likely blocked waiting for user input.
233
+ // This is a problem for two reasons. First, exiting takes a long time (5 seconds
234
+ // on Win8) because PowerShell is waiting forever, but the OS will forcibly terminate
235
+ // the console. Also - if there are any event handlers for the engine event
236
+ // PowerShell.Exiting, those handlers won't get a chance to run.
237
+ //
238
+ // By waiting for a key on a different thread, our pipeline execution thread
239
+ // (the thread Readline is called from) avoid being blocked in code that can't
240
+ // be unblocked and instead blocks on events we control.
241
+
242
+ // First, set an event so the thread to read a key actually attempts to read a key.
243
+ _singleton . _readKeyWaitHandle . Set ( ) ;
244
+
245
+ // Next, wait for one of two things - either a key is pressed on the console is exiting.
246
+ int handleId = WaitHandle . WaitAny ( _singleton . _waitHandles ) ;
247
+ if ( handleId == 1 )
204
248
{
205
- _singleton . _savedKeys . Enqueue ( key ) ;
249
+ // The console is exiting - throw an exception to unwind the stack to the point
250
+ // where we can return from ReadLine.
251
+ throw new OperationCanceledException ( ) ;
252
+ }
253
+ return _singleton . _queuedKeys . Dequeue ( ) ;
254
+ }
206
255
256
+ private bool BreakHandler ( ConsoleBreakSignal signal )
257
+ {
258
+ if ( signal == ConsoleBreakSignal . Close || signal == ConsoleBreakSignal . Shutdown )
259
+ {
260
+ // Set the event so ReadKey throws an exception to unwind.
261
+ _singleton . _closingWaitHandle . Set ( ) ;
207
262
}
208
- return key ;
263
+
264
+ return false ;
209
265
}
210
266
211
267
/// <summary>
@@ -225,6 +281,11 @@ public static string ReadLine()
225
281
_singleton . Initialize ( ) ;
226
282
return _singleton . InputLoop ( ) ;
227
283
}
284
+ catch ( OperationCanceledException )
285
+ {
286
+ // Console is exiting - return value isn't too critical - null or 'exit' could work equally well.
287
+ return "" ;
288
+ }
228
289
finally
229
290
{
230
291
NativeMethods . SetConsoleMode ( handle , dwConsoleMode ) ;
@@ -436,6 +497,15 @@ static PSConsoleReadLine()
436
497
} ;
437
498
438
499
_singleton = new PSConsoleReadLine ( ) ;
500
+
501
+ _breakHandlerGcHandle = GCHandle . Alloc ( new BreakHandler ( _singleton . BreakHandler ) ) ;
502
+ NativeMethods . SetConsoleCtrlHandler ( ( BreakHandler ) _breakHandlerGcHandle . Target , true ) ;
503
+ _singleton . _readKeyThread = new Thread ( _singleton . ReadKeyThreadProc ) ;
504
+ _singleton . _readKeyThread . Start ( ) ;
505
+ _singleton . _readKeyWaitHandle = new AutoResetEvent ( false ) ;
506
+ _singleton . _keyReadWaitHandle = new AutoResetEvent ( false ) ;
507
+ _singleton . _closingWaitHandle = new AutoResetEvent ( false ) ;
508
+ _singleton . _waitHandles = new WaitHandle [ ] { _singleton . _keyReadWaitHandle , _singleton . _closingWaitHandle } ;
439
509
}
440
510
441
511
private PSConsoleReadLine ( )
0 commit comments