Skip to content

Commit 18d24a5

Browse files
committed
Fix the slow exit problem
Moved the call to Console.ReadKey to it's own thread so I can control exiting ReadLine on the pipeline execution thread and unblock PowerShell so that it will execute any Exiting event handlers then let the process shut down before the OS forcibly terminates the process.
1 parent 52e9083 commit 18d24a5

File tree

1 file changed

+84
-14
lines changed

1 file changed

+84
-14
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);
@@ -434,6 +495,15 @@ static PSConsoleReadLine()
434495
};
435496

436497
_singleton = new PSConsoleReadLine();
498+
499+
_breakHandlerGcHandle = GCHandle.Alloc(new BreakHandler(_singleton.BreakHandler));
500+
NativeMethods.SetConsoleCtrlHandler((BreakHandler) _breakHandlerGcHandle.Target, true);
501+
_singleton._readKeyThread = new Thread(_singleton.ReadKeyThreadProc);
502+
_singleton._readKeyThread.Start();
503+
_singleton._readKeyWaitHandle = new AutoResetEvent(false);
504+
_singleton._keyReadWaitHandle = new AutoResetEvent(false);
505+
_singleton._closingWaitHandle = new AutoResetEvent(false);
506+
_singleton._waitHandles = new WaitHandle[] { _singleton._keyReadWaitHandle, _singleton._closingWaitHandle };
437507
}
438508

439509
private PSConsoleReadLine()

0 commit comments

Comments
 (0)