From 277e83126968f0b55846a4850794b05ecee08d05 Mon Sep 17 00:00:00 2001 From: "Patrick M. Meinecke" Date: Wed, 24 Jan 2018 21:17:13 -0500 Subject: [PATCH 1/8] Add a cancellation token to ReadLine Allow custom hosts to cancel ReadLine by supplying a cancellation token. --- PSReadLine/ReadLine.cs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index c993ca8f..2bfad495 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -30,6 +30,8 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods { private static readonly PSConsoleReadLine _singleton = new PSConsoleReadLine(); + private static readonly CancellationToken _defaultCancellationToken = new CancellationTokenSource().Token; + private bool _delayedOneTimeInitCompleted; private IPSConsoleReadLineMockableMethods _mockableMethods; @@ -41,6 +43,7 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods private Thread _readKeyThread; private AutoResetEvent _readKeyWaitHandle; private AutoResetEvent _keyReadWaitHandle; + private CancellationToken _cancelReadCancellationToken; internal ManualResetEvent _closingWaitHandle; private WaitHandle[] _threadProcWaitHandles; private WaitHandle[] _requestKeyWaitHandles; @@ -139,7 +142,12 @@ private void ReadKeyThreadProc() if (handleId == 1) // It was the _closingWaitHandle that was signaled. break; + var localCancellationToken = _singleton._cancelReadCancellationToken; ReadOneOrMoreKeys(); + if (localCancellationToken.IsCancellationRequested) + { + continue; + } // One or more keys were read - let ReadKey know we're done. _keyReadWaitHandle.Set(); @@ -249,6 +257,13 @@ internal static ConsoleKeyInfo ReadKey() throw new OperationCanceledException(); } + if (handleId == 2) + { + // ReadLine was cancelled by the host, throw an exception so we can return + // an empty string. + throw new OperationCanceledException(); + } + var key = _singleton._queuedKeys.Dequeue(); return key; } @@ -275,6 +290,18 @@ private void PrependQueuedKeys(ConsoleKeyInfo key) /// /// The complete command line. public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsics) + { + // Use a default cancellation token instead of CancellationToken.None because the + // WaitHandle is shared and could be triggered accidently. + return ReadLine(runspace, engineIntrinsics, _defaultCancellationToken); + } + + /// + /// Entry point - called from the PowerShell function PSConsoleHostReadLine + /// after the prompt has been displayed. + /// + /// The complete command line. + public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsics, CancellationToken cancellationToken) { var console = _singleton._console; @@ -304,6 +331,8 @@ public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsi _singleton.Initialize(runspace, engineIntrinsics); } + _singleton._cancelReadCancellationToken = cancellationToken; + _singleton._requestKeyWaitHandles[2] = _singleton._cancelReadCancellationToken.WaitHandle; return _singleton.InputLoop(); } catch (OperationCanceledException) @@ -711,7 +740,7 @@ private void DelayedOneTimeInitialize() _singleton._readKeyWaitHandle = new AutoResetEvent(false); _singleton._keyReadWaitHandle = new AutoResetEvent(false); _singleton._closingWaitHandle = new ManualResetEvent(false); - _singleton._requestKeyWaitHandles = new WaitHandle[] {_singleton._keyReadWaitHandle, _singleton._closingWaitHandle}; + _singleton._requestKeyWaitHandles = new WaitHandle[] {_singleton._keyReadWaitHandle, _singleton._closingWaitHandle, _defaultCancellationToken.WaitHandle}; _singleton._threadProcWaitHandles = new WaitHandle[] {_singleton._readKeyWaitHandle, _singleton._closingWaitHandle}; // This is for a "being hosted in an alternate appdomain scenario" (the From 537f5f28faf6377d89a9a67262240208ab4ee71b Mon Sep 17 00:00:00 2001 From: "Patrick M. Meinecke" Date: Wed, 24 Jan 2018 21:19:27 -0500 Subject: [PATCH 2/8] Fix stdin locking on Unix platforms Wait for Console.KeyAvailable before calling Console.ReadKey so getting cursor position doesn't block the thread. --- PSReadLine.build.ps1 | 3 +++ PSReadLine/ConsoleLib.cs | 27 ++++++++++++++++++++++++++- PSReadLine/PSReadLine.csproj | 10 ++++++++++ PSReadLine/packages.config | 1 + 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/PSReadLine.build.ps1 b/PSReadLine.build.ps1 index 62c91961..b0ac393d 100644 --- a/PSReadLine.build.ps1 +++ b/PSReadLine.build.ps1 @@ -202,6 +202,9 @@ task LayoutModule BuildMainModule, BuildMamlHelp, { Copy-Item PSReadLine/bin/$Configuration/Microsoft.PowerShell.PSReadLine2.dll $targetDir Copy-Item PSReadLine/bin/$Configuration/System.Runtime.InteropServices.RuntimeInformation.dll $targetDir + Copy-Item PSReadLine/bin/$Configuration/UnixConsoleEcho.dll $targetDir + Copy-Item PSReadLine/bin/$Configuration/libdisablekeyecho.so $targetDir + Copy-Item PSReadLine/bin/$Configuration/libdisablekeyecho.dylib $targetDir # Copy module manifest, but fix the version to match what we've specified in the binary module. $version = (Get-ChildItem -Path $targetDir/Microsoft.PowerShell.PSReadLine2.dll).VersionInfo.FileVersion diff --git a/PSReadLine/ConsoleLib.cs b/PSReadLine/ConsoleLib.cs index 870a1092..b7933b7f 100644 --- a/PSReadLine/ConsoleLib.cs +++ b/PSReadLine/ConsoleLib.cs @@ -97,7 +97,32 @@ public Encoding OutputEncoding set { try { Console.OutputEncoding = value; } catch { } } } - public ConsoleKeyInfo ReadKey() => Console.ReadKey(true); + private void WaitForKeyAvailable() + { + // On Unix platforms input echo is on by default. If we wait for KeyAvailable without + // disabling it, all input will be echoed. + UnixConsoleEcho.InputEcho.Disable(); + try + { + while (!Console.KeyAvailable) + { + System.Threading.Thread.Sleep(50); + } + } + finally + { + UnixConsoleEcho.InputEcho.Enable(); + } + } + + public ConsoleKeyInfo ReadKey() + { + // Wait for a key to be pressed before calling ReadKey to avoid locking stdin on + // non-Windows platforms. + WaitForKeyAvailable(); + return Console.ReadKey(true); + } + public bool KeyAvailable => Console.KeyAvailable; public void SetWindowPosition(int left, int top) => Console.SetWindowPosition(left, top); public void SetCursorPosition(int left, int top) => Console.SetCursorPosition(left, top); diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 0844de33..dc924449 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -51,6 +51,9 @@ + + packages\UnixConsoleEcho.0.1.0\lib\net461\UnixConsoleEcho.dll + @@ -121,6 +124,13 @@ ..\..\CopyDLL.cmd $(TargetPath) ) + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + +