Skip to content

Commit ad4f871

Browse files
committed
Merge branch 'master' of https://github.com/lzybkr/PSReadLine
2 parents ba29d92 + 34f080e commit ad4f871

File tree

2 files changed

+85
-15
lines changed

2 files changed

+85
-15
lines changed

PSReadLine/ReadLine.cs

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
using System.Linq;
66
using System.Management.Automation;
77
using System.Management.Automation.Language;
8+
using System.Runtime.InteropServices;
89
using System.Text;
910
using System.Text.RegularExpressions;
11+
using System.Threading;
1012

1113
namespace PSConsoleUtilities
1214
{
@@ -65,6 +67,12 @@ public class PSConsoleReadLine
6567
{
6668
private static readonly PSConsoleReadLine _singleton;
6769

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;
6876
private bool _captureKeys;
6977
private readonly Queue<ConsoleKeyInfo> _savedKeys;
7078
private readonly HistoryQueue<string> _demoStrings;
@@ -183,29 +191,77 @@ public PSConsoleReadlineOptions Options
183191

184192
#endregion Unit test only properties
185193

186-
[ExcludeFromCodeCoverage]
187-
private static ConsoleKeyInfo ReadKey()
194+
private void ReadKeyThreadProc()
188195
{
189-
var start = DateTime.Now;
190-
while (Console.KeyAvailable)
196+
while (true)
191197
{
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)
194203
{
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+
}
197210
}
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();
198224
}
225+
}
199226

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)
204248
{
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+
}
206255

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();
207262
}
208-
return key;
263+
264+
return false;
209265
}
210266

211267
/// <summary>
@@ -225,6 +281,11 @@ public static string ReadLine()
225281
_singleton.Initialize();
226282
return _singleton.InputLoop();
227283
}
284+
catch (OperationCanceledException)
285+
{
286+
// Console is exiting - return value isn't too critical - null or 'exit' could work equally well.
287+
return "";
288+
}
228289
finally
229290
{
230291
NativeMethods.SetConsoleMode(handle, dwConsoleMode);
@@ -436,6 +497,15 @@ static PSConsoleReadLine()
436497
};
437498

438499
_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 };
439509
}
440510

441511
private PSConsoleReadLine()

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Installation
2424
First, you need to download the module. Using PsGet (http://psget.net, very easy to install), you can run:
2525

2626
```
27-
psget PSReadline
27+
install-module PSReadline
2828
```
2929

3030
Alternatively, download the file https://github.com/lzybkr/PSReadLine/raw/master/PSReadline.zip and extract the contents into your `C:\Users\[User]\Documents\WindowsPowerShell\modules\PSReadline` folder. (You may have to create these directories if they don't exist)

0 commit comments

Comments
 (0)