From e3278a86c0d29036a93a4a2276a86fc2f9723666 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Tue, 12 Apr 2022 12:18:53 -0700 Subject: [PATCH] Delete `ConsoleProxy.cs` and further simplify `ReadKey` logic --- .../PowerShell/Console/ConsoleProxy.cs | 47 ------- .../Services/PowerShell/Console/IReadLine.cs | 4 +- .../PowerShell/Console/PsrlReadLine.cs | 2 +- .../PowerShell/Console/TerminalReadLine.cs | 83 ------------- ...orServicesConsolePSHostRawUserInterface.cs | 115 +----------------- .../PowerShell/Host/PsesInternalHost.cs | 37 ++++-- 6 files changed, 32 insertions(+), 256 deletions(-) delete mode 100644 src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleProxy.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleProxy.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleProxy.cs deleted file mode 100644 index 7c9ea2bbc5..0000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleProxy.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Threading; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console -{ - /// - /// Provides asynchronous implementations of the API's as well as - /// synchronous implementations that work around platform specific issues. - /// NOTE: We're missing GetCursorPosition. - /// - internal static class ConsoleProxy - { - internal static readonly ConsoleKeyInfo s_nullKeyInfo = new( - keyChar: ' ', - ConsoleKey.DownArrow, - shift: false, - alt: false, - control: false); - - /// - /// This method is sent to PSReadLine as a workaround for issues with the System.Console - /// implementation. Functionally it is the same as System.Console.ReadKey, - /// with the exception that it will not lock the standard input stream. - /// - /// - /// Determines whether to display the pressed key in the console window. - /// true to not display the pressed key; otherwise, false. - /// - /// - /// The that can be used to cancel the request. - /// - /// - /// An object that describes the ConsoleKey constant and Unicode character, if any, - /// that correspond to the pressed console key. The ConsoleKeyInfo object also describes, - /// in a bitwise combination of ConsoleModifiers values, whether one or more Shift, Alt, - /// or Ctrl modifier keys was pressed simultaneously with the console key. - /// - internal static ConsoleKeyInfo SafeReadKey(bool intercept, CancellationToken cancellationToken) - { - ConsoleKeyInfo key = System.Console.ReadKey(intercept); - return cancellationToken.IsCancellationRequested ? s_nullKeyInfo : key; - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs index c80afc41b0..7daa19628a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs @@ -1,15 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Security; using System.Threading; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { + // TODO: Do we really need a whole interface for this? internal interface IReadLine { string ReadLine(CancellationToken cancellationToken); - - SecureString ReadSecureLine(CancellationToken cancellationToken); } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs index 3a9437d3eb..a54a0dde65 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs @@ -40,7 +40,7 @@ public PsrlReadLine( public override string ReadLine(CancellationToken cancellationToken) => _psesHost.InvokeDelegate(representation: "ReadLine", new ExecutionOptions { MustRunInForeground = true }, InvokePSReadLine, cancellationToken); - protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) => ConsoleProxy.SafeReadKey(intercept: true, cancellationToken); + protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) => _psesHost.ReadKey(intercept: true, cancellationToken); #endregion diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs index ff06bdceb5..8d05edc182 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; -using System.Management.Automation; -using System.Security; using System.Threading; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console @@ -15,85 +12,5 @@ internal abstract class TerminalReadLine : IReadLine public abstract string ReadLine(CancellationToken cancellationToken); protected abstract ConsoleKeyInfo ReadKey(CancellationToken cancellationToken); - - public SecureString ReadSecureLine(CancellationToken cancellationToken) - { - Console.TreatControlCAsInput = true; - int previousInputLength = 0; - SecureString secureString = new(); - try - { - bool enterPressed = false; - while (!enterPressed && !cancellationToken.IsCancellationRequested) - { - ConsoleKeyInfo keyInfo = ReadKey(cancellationToken); - - if (keyInfo.IsCtrlC()) - { - throw new PipelineStoppedException(); - } - - switch (keyInfo.Key) - { - case ConsoleKey.Enter: - // Stop the while loop so we can realign the cursor - // and then return the entered string - enterPressed = true; - continue; - - case ConsoleKey.Tab: - break; - - case ConsoleKey.Backspace: - if (secureString.Length > 0) - { - secureString.RemoveAt(secureString.Length - 1); - } - break; - - default: - if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) - { - secureString.AppendChar(keyInfo.KeyChar); - } - break; - } - - // Re-render the secure string characters - int currentInputLength = secureString.Length; - int consoleWidth = Console.WindowWidth; - - if (currentInputLength > previousInputLength) - { - Console.Write('*'); - } - else if (previousInputLength > 0 && currentInputLength < previousInputLength) - { - int row = Console.CursorTop; - int col = Console.CursorLeft; - - // Back up the cursor before clearing the character - col--; - if (col < 0) - { - col = consoleWidth - 1; - row--; - } - - Console.SetCursorPosition(col, row); - Console.Write(' '); - Console.SetCursorPosition(col, row); - } - - previousInputLength = currentInputLength; - } - } - finally - { - Console.TreatControlCAsInput = false; - } - - return secureString; - } } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostRawUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostRawUserInterface.cs index d2329fc792..576fffd5a6 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostRawUserInterface.cs @@ -2,10 +2,8 @@ // Licensed under the MIT License. using System; -using System.Management.Automation; using System.Management.Automation.Host; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host { @@ -15,7 +13,6 @@ internal class EditorServicesConsolePSHostRawUserInterface : PSHostRawUserInterf private readonly PSHostRawUserInterface _internalRawUI; private readonly ILogger _logger; - private KeyInfo? _lastKeyDown; #endregion @@ -129,60 +126,7 @@ public override string WindowTitle /// /// Options for reading the current keypress. /// A KeyInfo struct with details about the current keypress. - public override KeyInfo ReadKey(ReadKeyOptions options) - { - bool includeUp = (options & ReadKeyOptions.IncludeKeyUp) != 0; - - // Key Up was requested and we have a cached key down we can return. - if (includeUp && _lastKeyDown != null) - { - KeyInfo info = _lastKeyDown.Value; - _lastKeyDown = null; - return new KeyInfo( - info.VirtualKeyCode, - info.Character, - info.ControlKeyState, - keyDown: false); - } - - bool intercept = (options & ReadKeyOptions.NoEcho) != 0; - bool includeDown = (options & ReadKeyOptions.IncludeKeyDown) != 0; - if (!(includeDown || includeUp)) - { - throw new PSArgumentException( - "Cannot read key options. To read options, set one or both of the following: IncludeKeyDown, IncludeKeyUp.", - nameof(options)); - } - - // Allow ControlC as input so we can emulate pipeline stop requests. We can't actually - // determine if a stop is requested without using non-public API's. - bool oldValue = System.Console.TreatControlCAsInput; - try - { - System.Console.TreatControlCAsInput = true; - ConsoleKeyInfo key = ConsoleProxy.SafeReadKey(intercept, default); - - if (IsCtrlC(key)) - { - // Caller wants CtrlC as input so return it. - if ((options & ReadKeyOptions.AllowCtrlC) != 0) - { - return ProcessKey(key, includeDown); - } - - // Caller doesn't want CtrlC so throw a PipelineStoppedException to emulate - // a real stop. This will not show an exception to a script based caller and it - // will avoid having to return something like default(KeyInfo). - throw new PipelineStoppedException(); - } - - return ProcessKey(key, includeDown); - } - finally - { - System.Console.TreatControlCAsInput = oldValue; - } - } + public override KeyInfo ReadKey(ReadKeyOptions options) => _internalRawUI.ReadKey(options); /// /// Flushes the current input buffer. @@ -279,62 +223,5 @@ public override void SetBufferContents( public override int LengthInBufferCells(string source, int offset) => _internalRawUI.LengthInBufferCells(source, offset); #endregion - - /// - /// Determines if a key press represents the input Ctrl + C. - /// - /// The key to test. - /// - /// if the key represents the input Ctrl + C, - /// otherwise . - /// - private static bool IsCtrlC(ConsoleKeyInfo keyInfo) - { - // In the VSCode terminal Ctrl C is processed as virtual key code "3", which - // is not a named value in the ConsoleKey enum. - if ((int)keyInfo.Key == 3) - { - return true; - } - - return keyInfo.Key == ConsoleKey.C && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0; - } - - /// - /// Converts objects to objects and caches - /// key down events for the next key up request. - /// - /// The key to convert. - /// - /// A value indicating whether the result should be a key down event. - /// - /// The converted value. - private KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) - { - // Translate ConsoleModifiers to ControlKeyStates - ControlKeyStates states = default; - if ((key.Modifiers & ConsoleModifiers.Alt) != 0) - { - states |= ControlKeyStates.LeftAltPressed; - } - - if ((key.Modifiers & ConsoleModifiers.Control) != 0) - { - states |= ControlKeyStates.LeftCtrlPressed; - } - - if ((key.Modifiers & ConsoleModifiers.Shift) != 0) - { - states |= ControlKeyStates.ShiftPressed; - } - - KeyInfo result = new((int)key.Key, key.KeyChar, states, isDown); - if (isDown) - { - _lastKeyDown = result; - } - - return result; - } } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 36ecd319c9..3bb797054f 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -875,6 +875,13 @@ private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs args) } } + private static readonly ConsoleKeyInfo s_nullKeyInfo = new( + keyChar: ' ', + ConsoleKey.DownArrow, + shift: false, + alt: false, + control: false); + private ConsoleKeyInfo ReadKey(bool intercept) { // PSRL doesn't tell us when CtrlC was sent. @@ -887,20 +894,34 @@ private ConsoleKeyInfo ReadKey(bool intercept) // PSReadLine's thread waiting on Console.ReadKey to return. Normally we'd just cancel // this call, but the .NET API ReadKey is not cancellable, and is stuck until we send // input. This leads to a myriad of problems, but we circumvent them by pretending to - // press a key. + // press a key, thus allowing ReadKey to return, and us to ignore it. using CancellationTokenRegistration registration = _readKeyCancellationToken.Register( - () => _languageServer?.SendNotification("powerShell/sendKeyPress") - ); - _lastKey = ConsoleProxy.SafeReadKey(intercept, _readKeyCancellationToken); - return _lastKey.Value; + () => _languageServer?.SendNotification("powerShell/sendKeyPress")); + + // TODO: We may want to allow users of PSES to override this method call. + _lastKey = System.Console.ReadKey(intercept); + + // TODO: After fixing PSReadLine so that when canceled it doesn't read a key, we can + // stop using s_nullKeyInfo (which is a down arrow so we don't change the buffer + // content). Without this, the sent key press is translated to an @ symbol. + return _readKeyCancellationToken.IsCancellationRequested ? s_nullKeyInfo : _lastKey.Value; } - private bool LastKeyWasCtrlC() + internal ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) { - return _lastKey.HasValue - && _lastKey.Value.IsCtrlC(); + try + { + _readKeyCancellationToken = cancellationToken; + return ReadKey(intercept); + } + finally + { + _readKeyCancellationToken = CancellationToken.None; + } } + private bool LastKeyWasCtrlC() => _lastKey.HasValue && _lastKey.Value.IsCtrlC(); + private void StopDebugContext() { // We are officially stopping the debugger.