diff --git a/.vsts-ci/templates/release-general.yml b/.vsts-ci/templates/release-general.yml index 4b531a78f..ea7f3a908 100644 --- a/.vsts-ci/templates/release-general.yml +++ b/.vsts-ci/templates/release-general.yml @@ -46,7 +46,6 @@ steps: **/Newtonsoft.Json.dll **/OmniSharp*.dll **/Serilog*.dll - **/UnixConsoleEcho.dll # The SBOM generation requires our original sources with the `dotnet restore` # produced `project.assets.json` files. diff --git a/Third Party Notices.txt b/Third Party Notices.txt index 3396adc4b..7a16849ba 100644 --- a/Third Party Notices.txt +++ b/Third Party Notices.txt @@ -4,21 +4,6 @@ This file is based on or incorporates material from the projects listed below (T --- -UnixConsoleEcho - -Copyright (c) 2017 Patrick Meinecke -Provided for Informational Purposes Only - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---- - Serilog Copyright 2013-2015 Serilog Contributors diff --git a/src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs index 3c06e9898..c05d30b7a 100644 --- a/src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs @@ -12,12 +12,6 @@ using System.Management.Automation.Runspaces; using Microsoft.PowerShell.EditorServices.Hosting; using System.Globalization; -using System.Collections; - -// TODO: Remove this when we drop support for PS6. -#if CoreCLR -using System.Runtime.InteropServices; -#endif #if DEBUG using System.Diagnostics; @@ -35,14 +29,6 @@ namespace Microsoft.PowerShell.EditorServices.Commands [Cmdlet(VerbsLifecycle.Start, "EditorServices", DefaultParameterSetName = "NamedPipe")] public sealed class StartEditorServicesCommand : PSCmdlet { - // TODO: Remove this when we drop support for PS6. - private static readonly bool s_isWindows = -#if CoreCLR - RuntimeInformation.IsOSPlatform(OSPlatform.Windows); -#else - true; -#endif - private readonly List _disposableResources; private readonly List _loggerUnsubscribers; @@ -420,12 +406,7 @@ private ConsoleReplKind GetReplKind() return ConsoleReplKind.None; } - // TODO: Remove this when we drop support for PS6. - Hashtable psVersionTable = (Hashtable)SessionState.PSVariable.GetValue("PSVersionTable"); - dynamic version = psVersionTable["PSVersion"]; - int majorVersion = (int)version.Major; - - if (UseLegacyReadLine || (!s_isWindows && majorVersion == 6)) + if (UseLegacyReadLine) { _logger.Log(PsesLogLevel.Diagnostic, "REPL configured as Legacy"); return ConsoleReplKind.LegacyReadLine; diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 227919d0f..22f38ccb2 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -38,7 +38,6 @@ - diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleProxy.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleProxy.cs deleted file mode 100644 index 296716ff9..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleProxy.cs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -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. - /// - internal static class ConsoleProxy - { - private static readonly IConsoleOperations s_consoleProxy; - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "Platform specific initialization")] - static ConsoleProxy() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - s_consoleProxy = new WindowsConsoleOperations(); - return; - } - - s_consoleProxy = new UnixConsoleOperations(); - } - - /// - /// Obtains the next character or function key pressed by the user asynchronously. - /// Does not block when other console API's are called. - /// - /// - /// Determines whether to display the pressed key in the console window. - /// to not display the pressed key; otherwise, . - /// - /// The CancellationToken to observe. - /// - /// An object that describes the constant and Unicode character, if any, - /// that correspond to the pressed console key. The object also - /// describes, in a bitwise combination of values, whether - /// one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously with the console key. - /// - public static ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) => - s_consoleProxy.ReadKey(intercept, cancellationToken); - - /// - /// Obtains the next character or function key pressed by the user asynchronously. - /// Does not block when other console API's are called. - /// - /// - /// Determines whether to display the pressed key in the console window. - /// to not display the pressed key; otherwise, . - /// - /// The CancellationToken to observe. - /// - /// A task that will complete with a result of the key pressed by the user. - /// - public static Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) => - s_consoleProxy.ReadKeyAsync(intercept, cancellationToken); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The horizontal position of the console cursor. - public static int GetCursorLeft() => - s_consoleProxy.GetCursorLeft(); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// The horizontal position of the console cursor. - public static int GetCursorLeft(CancellationToken cancellationToken) => - s_consoleProxy.GetCursorLeft(cancellationToken); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// - /// A representing the asynchronous operation. The - /// property will return the horizontal position - /// of the console cursor. - /// - public static Task GetCursorLeftAsync() => - s_consoleProxy.GetCursorLeftAsync(); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// - /// A representing the asynchronous operation. The - /// property will return the horizontal position - /// of the console cursor. - /// - public static Task GetCursorLeftAsync(CancellationToken cancellationToken) => - s_consoleProxy.GetCursorLeftAsync(cancellationToken); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The vertical position of the console cursor. - public static int GetCursorTop() => - s_consoleProxy.GetCursorTop(); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// The vertical position of the console cursor. - public static int GetCursorTop(CancellationToken cancellationToken) => - s_consoleProxy.GetCursorTop(cancellationToken); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// - /// A representing the asynchronous operation. The - /// property will return the vertical position - /// of the console cursor. - /// - public static Task GetCursorTopAsync() => - s_consoleProxy.GetCursorTopAsync(); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// - /// A representing the asynchronous operation. The - /// property will return the vertical position - /// of the console cursor. - /// - public static Task GetCursorTopAsync(CancellationToken cancellationToken) => - s_consoleProxy.GetCursorTopAsync(cancellationToken); - - /// - /// 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) - { - try - { - return s_consoleProxy.ReadKey(intercept, cancellationToken); - } - catch (OperationCanceledException) - { - return new ConsoleKeyInfo( - keyChar: ' ', - ConsoleKey.DownArrow, - shift: false, - alt: false, - control: false); - } - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/IConsoleOperations.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/IConsoleOperations.cs deleted file mode 100644 index 0b310eeb1..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/IConsoleOperations.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console -{ - /// - /// Provides platform specific console utilities. - /// - internal interface IConsoleOperations - { - /// - /// Obtains the next character or function key pressed by the user asynchronously. - /// Does not block when other console API's are called. - /// - /// - /// Determines whether to display the pressed key in the console window. - /// to not display the pressed key; otherwise, . - /// - /// The CancellationToken to observe. - /// - /// An object that describes the constant and Unicode character, if any, - /// that correspond to the pressed console key. The object also - /// describes, in a bitwise combination of values, whether - /// one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously with the console key. - /// - ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken); - - /// - /// Obtains the next character or function key pressed by the user asynchronously. - /// Does not block when other console API's are called. - /// - /// - /// Determines whether to display the pressed key in the console window. - /// to not display the pressed key; otherwise, . - /// - /// The CancellationToken to observe. - /// - /// A task that will complete with a result of the key pressed by the user. - /// - Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The horizontal position of the console cursor. - int GetCursorLeft(); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// The horizontal position of the console cursor. - int GetCursorLeft(CancellationToken cancellationToken); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// - /// A representing the asynchronous operation. The - /// property will return the horizontal position - /// of the console cursor. - /// - Task GetCursorLeftAsync(); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// - /// A representing the asynchronous operation. The - /// property will return the horizontal position - /// of the console cursor. - /// - Task GetCursorLeftAsync(CancellationToken cancellationToken); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The vertical position of the console cursor. - int GetCursorTop(); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// The vertical position of the console cursor. - int GetCursorTop(CancellationToken cancellationToken); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// - /// A representing the asynchronous operation. The - /// property will return the vertical position - /// of the console cursor. - /// - Task GetCursorTopAsync(); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// - /// A representing the asynchronous operation. The - /// property will return the vertical position - /// of the console cursor. - /// - Task GetCursorTopAsync(CancellationToken cancellationToken); - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs index 1233df7b0..7daa19628 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs @@ -1,16 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -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/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs index 601f3f750..39e12974b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -1,15 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; using System.Collections.Generic; using System.Linq; using System.Management.Automation; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { @@ -47,8 +46,8 @@ public override string ReadLine(CancellationToken cancellationToken) StringBuilder inputLine = new(); - int initialCursorCol = ConsoleProxy.GetCursorLeft(cancellationToken); - int initialCursorRow = ConsoleProxy.GetCursorTop(cancellationToken); + int initialCursorCol = Console.CursorLeft; + int initialCursorRow = Console.CursorTop; int currentCursorIndex = 0; @@ -69,7 +68,7 @@ public override string ReadLine(CancellationToken cancellationToken) switch (keyInfo.Key) { case ConsoleKey.Tab: - if (currentCompletion == null) + if (currentCompletion is null) { inputBeforeCompletion = inputLine.ToString(); inputAfterCompletion = null; @@ -116,14 +115,14 @@ public override string ReadLine(CancellationToken cancellationToken) currentCompletion?.GetNextResult( !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift)); - if (completion != null) + if (completion is not null) { currentCursorIndex = InsertInput( inputLine, promptStartCol, promptStartRow, - $"{completion.CompletionText}{inputAfterCompletion}", + completion.CompletionText + inputAfterCompletion, currentCursorIndex, insertIndex: currentCompletion.ReplacementIndex, replaceLength: inputLine.Length - currentCompletion.ReplacementIndex, @@ -190,8 +189,7 @@ public override string ReadLine(CancellationToken cancellationToken) currentCompletion = null; // TODO: Ctrl+Up should allow navigation in multi-line input - - if (currentHistory == null) + if (currentHistory is null) { historyIndex = -1; @@ -200,13 +198,13 @@ public override string ReadLine(CancellationToken cancellationToken) currentHistory = _psesHost.InvokePSCommand(command, executionOptions: null, cancellationToken); - if (currentHistory != null) + if (currentHistory is not null) { historyIndex = currentHistory.Count; } } - if (currentHistory != null && currentHistory.Count > 0 && historyIndex > 0) + if (currentHistory?.Count > 0 && historyIndex > 0) { historyIndex--; @@ -228,9 +226,8 @@ public override string ReadLine(CancellationToken cancellationToken) // The down arrow shouldn't cause history to be loaded, // it's only for navigating an active history array - if (historyIndex > -1 && historyIndex < currentHistory.Count && - currentHistory != null && currentHistory.Count > 0) + currentHistory?.Count > 0) { historyIndex++; @@ -420,9 +417,7 @@ private ConsoleKeyInfo ReadKeyWithIdleSupport(CancellationToken cancellationToke } } - private ConsoleKeyInfo InvokeReadKeyFunc() => - // intercept = false means we display the key in the console - _readKeyFunc(/* intercept */ false); + private ConsoleKeyInfo InvokeReadKeyFunc() => _readKeyFunc(/* intercept */ false); private static int InsertInput( StringBuilder inputLine, @@ -497,10 +492,8 @@ private static int InsertInput( consoleWidth, finalCursorIndex); } - else - { - return inputLine.Length; - } + + return inputLine.Length; } private static int MoveCursorToIndex( @@ -521,6 +514,7 @@ private static int MoveCursorToIndex( return newCursorIndex; } + private static void CalculateCursorFromIndex( int promptStartCol, int promptStartRow, @@ -530,7 +524,7 @@ private static void CalculateCursorFromIndex( out int cursorRow) { cursorCol = promptStartCol + inputIndex; - cursorRow = promptStartRow + cursorCol / consoleWidth; + cursorRow = promptStartRow + (cursorCol / consoleWidth); cursorCol %= consoleWidth; } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs index 10a540c0b..64e3627fa 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs @@ -18,8 +18,6 @@ internal class PsrlReadLine : TerminalReadLine private readonly EngineIntrinsics _engineIntrinsics; - #region Constructors - public PsrlReadLine( PSReadLineProxy psrlProxy, PsesInternalHost psesHost, @@ -34,24 +32,14 @@ public PsrlReadLine( _psrlProxy.OverrideIdleHandler(onIdleAction); } - #endregion - - #region Public Methods - public override string ReadLine(CancellationToken cancellationToken) => _psesHost.InvokeDelegate(representation: "ReadLine", new ExecutionOptions { MustRunInForeground = true }, InvokePSReadLine, cancellationToken); - protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) => ConsoleProxy.ReadKey(intercept: true, cancellationToken); - - #endregion - - #region Private Methods + protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) => _psesHost.ReadKey(intercept: true, cancellationToken); private string InvokePSReadLine(CancellationToken cancellationToken) { EngineIntrinsics engineIntrinsics = _psesHost.IsRunspacePushed ? null : _engineIntrinsics; return _psrlProxy.ReadLine(_psesHost.Runspace, engineIntrinsics, cancellationToken, /* lastExecutionStatus */ null); } - - #endregion } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs index 572e97088..8d05edc18 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 = ConsoleProxy.GetCursorTop(cancellationToken); - int col = ConsoleProxy.GetCursorLeft(cancellationToken); - - // 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/Console/UnixConsoleOperations.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/UnixConsoleOperations.cs deleted file mode 100644 index 63eff5fc3..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/UnixConsoleOperations.cs +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; -using UnixConsoleEcho; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console -{ - internal class UnixConsoleOperations : IConsoleOperations - { - private const int LongWaitForKeySleepTime = 300; - - private const int ShortWaitForKeyTimeout = 5000; - - private const int ShortWaitForKeySpinUntilSleepTime = 30; - - private static readonly ManualResetEventSlim s_waitHandle = new(); - - private static readonly SemaphoreSlim s_readKeyHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - - private static readonly SemaphoreSlim s_stdInHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - - private Func WaitForKeyAvailable; - - private Func> WaitForKeyAvailableAsync; - - internal UnixConsoleOperations() - { - // Switch between long and short wait periods depending on if the - // user has recently (last 5 seconds) pressed a key to avoid preventing - // the CPU from entering low power mode. - WaitForKeyAvailable = LongWaitForKey; - WaitForKeyAvailableAsync = LongWaitForKeyAsync; - } - - public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) - { - s_readKeyHandle.Wait(cancellationToken); - - // On Unix platforms System.Console.ReadKey has an internal lock on stdin. Because - // of this, if a ReadKey call is pending in one thread and in another thread - // Console.CursorLeft is called, both threads block until a key is pressed. - - // To work around this we wait for a key to be pressed before actually calling Console.ReadKey. - // However, any pressed keys during this time will be echoed to the console. To get around - // this we use the UnixConsoleEcho package to disable echo prior to waiting. - if (VersionUtils.IsPS6) - { - InputEcho.Disable(); - } - - try - { - // The WaitForKeyAvailable delegate switches between a long delay between waits and - // a short timeout depending on how recently a key has been pressed. This allows us - // to let the CPU enter low power mode without compromising responsiveness. - while (!WaitForKeyAvailable(cancellationToken)) - { - ; - } - } - finally - { - if (VersionUtils.IsPS6) - { - InputEcho.Disable(); - } - s_readKeyHandle.Release(); - } - - // A key has been pressed, so aquire a lock on our internal stdin handle. This is done - // so any of our calls to cursor position API's do not release ReadKey. - s_stdInHandle.Wait(cancellationToken); - try - { - return System.Console.ReadKey(intercept); - } - finally - { - s_stdInHandle.Release(); - } - } - - public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) - { - await s_readKeyHandle.WaitAsync(cancellationToken).ConfigureAwait(false); - - // I tried to replace this library with a call to `stty -echo`, but unfortunately - // the library also sets up allowing backspace to trigger `Console.KeyAvailable`. - if (VersionUtils.IsPS6) - { - InputEcho.Disable(); - } - - try - { - while (!await WaitForKeyAvailableAsync(cancellationToken).ConfigureAwait(false)) - { - ; - } - } - finally - { - if (VersionUtils.IsPS6) - { - InputEcho.Enable(); - } - s_readKeyHandle.Release(); - } - - await s_stdInHandle.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - return System.Console.ReadKey(intercept); - } - finally - { - s_stdInHandle.Release(); - } - } - - public int GetCursorLeft() => GetCursorLeft(CancellationToken.None); - - public int GetCursorLeft(CancellationToken cancellationToken) - { - s_stdInHandle.Wait(cancellationToken); - try - { - return System.Console.CursorLeft; - } - finally - { - s_stdInHandle.Release(); - } - } - - public Task GetCursorLeftAsync() => GetCursorLeftAsync(CancellationToken.None); - - public async Task GetCursorLeftAsync(CancellationToken cancellationToken) - { - await s_stdInHandle.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - return System.Console.CursorLeft; - } - finally - { - s_stdInHandle.Release(); - } - } - - public int GetCursorTop() => GetCursorTop(CancellationToken.None); - - public int GetCursorTop(CancellationToken cancellationToken) - { - s_stdInHandle.Wait(cancellationToken); - try - { - return System.Console.CursorTop; - } - finally - { - s_stdInHandle.Release(); - } - } - - public Task GetCursorTopAsync() => GetCursorTopAsync(CancellationToken.None); - - public async Task GetCursorTopAsync(CancellationToken cancellationToken) - { - await s_stdInHandle.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - return System.Console.CursorTop; - } - finally - { - s_stdInHandle.Release(); - } - } - - private bool LongWaitForKey(CancellationToken cancellationToken) - { - // Wait for a key to be buffered (in other words, wait for Console.KeyAvailable to become - // true) with a long delay between checks. - while (!IsKeyAvailable(cancellationToken)) - { - s_waitHandle.Wait(LongWaitForKeySleepTime, cancellationToken); - } - - // As soon as a key is buffered, return true and switch the wait logic to be more - // responsive, but also more expensive. - WaitForKeyAvailable = ShortWaitForKey; - return true; - } - - private async Task LongWaitForKeyAsync(CancellationToken cancellationToken) - { - while (!await IsKeyAvailableAsync(cancellationToken).ConfigureAwait(false)) - { - await Task.Delay(LongWaitForKeySleepTime, cancellationToken).ConfigureAwait(false); - } - - WaitForKeyAvailableAsync = ShortWaitForKeyAsync; - return true; - } - - private bool ShortWaitForKey(CancellationToken cancellationToken) - { - // Check frequently for a new key to be buffered. - if (SpinUntilKeyAvailable(ShortWaitForKeyTimeout, cancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - return true; - } - - // If the user has not pressed a key before the end of the SpinUntil timeout then - // the user is idle and we can switch back to long delays between KeyAvailable checks. - cancellationToken.ThrowIfCancellationRequested(); - WaitForKeyAvailable = LongWaitForKey; - return false; - } - - private async Task ShortWaitForKeyAsync(CancellationToken cancellationToken) - { - if (await SpinUntilKeyAvailableAsync(ShortWaitForKeyTimeout, cancellationToken).ConfigureAwait(false)) - { - cancellationToken.ThrowIfCancellationRequested(); - return true; - } - - cancellationToken.ThrowIfCancellationRequested(); - WaitForKeyAvailableAsync = LongWaitForKeyAsync; - return false; - } - - private static bool SpinUntilKeyAvailable(int millisecondsTimeout, CancellationToken cancellationToken) - { - return SpinWait.SpinUntil( - () => - { - s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); - return IsKeyAvailable(cancellationToken); - }, - millisecondsTimeout); - } - - private static Task SpinUntilKeyAvailableAsync(int millisecondsTimeout, CancellationToken cancellationToken) - { - return Task.Factory.StartNew( - () => SpinWait.SpinUntil( - () => - { - // The wait handle is never set, it's just used to enable cancelling the wait. - s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); - return IsKeyAvailable(cancellationToken); - }, - millisecondsTimeout), - cancellationToken); - } - - private static bool IsKeyAvailable(CancellationToken cancellationToken) - { - s_stdInHandle.Wait(cancellationToken); - try - { - return System.Console.KeyAvailable; - } - finally - { - s_stdInHandle.Release(); - } - } - - private static async Task IsKeyAvailableAsync(CancellationToken cancellationToken) - { - await s_stdInHandle.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - return System.Console.KeyAvailable; - } - finally - { - s_stdInHandle.Release(); - } - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/WindowsConsoleOperations.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/WindowsConsoleOperations.cs deleted file mode 100644 index 7823b6bb6..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/WindowsConsoleOperations.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console -{ - internal class WindowsConsoleOperations : IConsoleOperations - { - private ConsoleKeyInfo? _bufferedKey; - - private readonly SemaphoreSlim _readKeyHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - - public int GetCursorLeft() => System.Console.CursorLeft; - - public int GetCursorLeft(CancellationToken cancellationToken) => System.Console.CursorLeft; - - public Task GetCursorLeftAsync() => Task.FromResult(System.Console.CursorLeft); - - public Task GetCursorLeftAsync(CancellationToken cancellationToken) => Task.FromResult(System.Console.CursorLeft); - - public int GetCursorTop() => System.Console.CursorTop; - - public int GetCursorTop(CancellationToken cancellationToken) => System.Console.CursorTop; - - public Task GetCursorTopAsync() => Task.FromResult(System.Console.CursorTop); - - public Task GetCursorTopAsync(CancellationToken cancellationToken) => Task.FromResult(System.Console.CursorTop); - - public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) - { - await _readKeyHandle.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - if (_bufferedKey == null) - { - _bufferedKey = await Task.Run(() => System.Console.ReadKey(intercept)).ConfigureAwait(false); - } - - return _bufferedKey.Value; - } - finally - { - _readKeyHandle.Release(); - - // Throw if we're cancelled so the buffered key isn't cleared. - cancellationToken.ThrowIfCancellationRequested(); - _bufferedKey = null; - } - } - - public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) - { - _readKeyHandle.Wait(cancellationToken); - try - { - return - _bufferedKey ?? (_bufferedKey = System.Console.ReadKey(intercept)).Value; - } - finally - { - _readKeyHandle.Release(); - - // Throw if we're cancelled so the buffered key isn't cleared. - cancellationToken.ThrowIfCancellationRequested(); - _bufferedKey = null; - } - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostRawUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostRawUserInterface.cs index b435ac321..8a90f0503 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostRawUserInterface.cs @@ -1,12 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console; using System; -using System.Management.Automation; using System.Management.Automation.Host; -using System.Threading; +using Microsoft.Extensions.Logging; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host { @@ -16,7 +13,6 @@ internal class EditorServicesConsolePSHostRawUserInterface : PSHostRawUserInterf private readonly PSHostRawUserInterface _internalRawUI; private readonly ILogger _logger; - private KeyInfo? _lastKeyDown; #endregion @@ -26,8 +22,6 @@ internal class EditorServicesConsolePSHostRawUserInterface : PSHostRawUserInterf /// Creates a new instance of the TerminalPSHostRawUserInterface /// class with the given IConsoleHost implementation. /// - /// The ILogger implementation to use for this instance. - /// The InternalHost instance from the origin runspace. public EditorServicesConsolePSHostRawUserInterface( ILoggerFactory loggerFactory, PSHostRawUserInterface internalRawUI) @@ -72,10 +66,7 @@ public override Size BufferSize /// public override Coordinates CursorPosition { - get => new( - ConsoleProxy.GetCursorLeft(), - ConsoleProxy.GetCursorTop()); - + get => _internalRawUI.CursorPosition; set => _internalRawUI.CursorPosition = value; } @@ -135,70 +126,12 @@ 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.ReadKey(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. /// - public override void FlushInputBuffer() - { - _logger.LogWarning( - "PSHostRawUserInterface.FlushInputBuffer was called"); - } + public override void FlushInputBuffer() => _logger.LogWarning("PSHostRawUserInterface.FlushInputBuffer was called"); /// /// Gets the contents of the console buffer in a rectangular area. @@ -262,6 +195,7 @@ public override void SetBufferContents( /// implementation for the process. /// public override int LengthInBufferCells(char source) => _internalRawUI.LengthInBufferCells(source); + /// /// Determines the number of BufferCells a string occupies. /// @@ -290,62 +224,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/EditorServicesConsolePSHostUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs index 04dbf46da..06fa4a74b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs @@ -8,16 +8,12 @@ using System.Management.Automation; using System.Management.Automation.Host; using System.Security; -using System.Threading; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host { internal class EditorServicesConsolePSHostUserInterface : PSHostUserInterface { - private readonly IReadLineProvider _readLineProvider; - private readonly PSHostUserInterface _underlyingHostUI; /// @@ -28,10 +24,8 @@ internal class EditorServicesConsolePSHostUserInterface : PSHostUserInterface public EditorServicesConsolePSHostUserInterface( ILoggerFactory loggerFactory, - IReadLineProvider readLineProvider, PSHostUserInterface underlyingHostUI) { - _readLineProvider = readLineProvider; _underlyingHostUI = underlyingHostUI; RawUI = new EditorServicesConsolePSHostRawUserInterface(loggerFactory, underlyingHostUI.RawUI); } @@ -48,9 +42,9 @@ public EditorServicesConsolePSHostUserInterface( public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) => _underlyingHostUI.PromptForCredential(caption, message, userName, targetName); - public override string ReadLine() => _readLineProvider.ReadLine.ReadLine(CancellationToken.None); + public override string ReadLine() => _underlyingHostUI.ReadLine(); - public override SecureString ReadLineAsSecureString() => _readLineProvider.ReadLine.ReadSecureLine(CancellationToken.None); + public override SecureString ReadLineAsSecureString() => _underlyingHostUI.ReadLineAsSecureString(); public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) => _underlyingHostUI.Write(foregroundColor, backgroundColor, value); diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 3ffc2761d..3bb797054 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -73,6 +73,8 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns private bool _skipNextPrompt; + private CancellationToken _readKeyCancellationToken; + private bool _resettingRunspace; public PsesInternalHost( @@ -114,7 +116,7 @@ public PsesInternalHost( DebugContext = new PowerShellDebugContext(loggerFactory, this); UI = hostInfo.ConsoleReplEnabled - ? new EditorServicesConsolePSHostUserInterface(loggerFactory, _readLineProvider, hostInfo.PSHost.UI) + ? new EditorServicesConsolePSHostUserInterface(loggerFactory, hostInfo.PSHost.UI) : new NullPSHostUI(); } @@ -690,7 +692,18 @@ public void WriteWithPrompt(PSCommand command, CancellationToken cancellationTok UI.WriteLine(command.GetInvocationText()); } - private string InvokeReadLine(CancellationToken cancellationToken) => _readLineProvider.ReadLine.ReadLine(cancellationToken); + private string InvokeReadLine(CancellationToken cancellationToken) + { + try + { + _readKeyCancellationToken = cancellationToken; + return _readLineProvider.ReadLine.ReadLine(cancellationToken); + } + finally + { + _readKeyCancellationToken = CancellationToken.None; + } + } private void InvokeInput(string input, CancellationToken cancellationToken) { @@ -862,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. @@ -869,16 +889,39 @@ private ConsoleKeyInfo ReadKey(bool intercept) // This isn't functionally required, // but helps us determine when the prompt needs a newline added - _lastKey = ConsoleProxy.SafeReadKey(intercept, CancellationToken.None); - return _lastKey.Value; + // NOTE: This requests that the client (the Code extension) send a non-printing key back + // to the terminal on stdin, emulating a user pressing a button. This allows + // 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, thus allowing ReadKey to return, and us to ignore it. + using CancellationTokenRegistration registration = _readKeyCancellationToken.Register( + () => _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. diff --git a/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs b/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs index 9681b7dc6..1e87127b1 100644 --- a/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs @@ -109,9 +109,9 @@ public async Task CompletesVariableInFile() public async Task CompletesAttributeValue() { IEnumerable results = await GetCompletionResultsAsync(CompleteAttributeValue.SourceDetails).ConfigureAwait(true); - Assert.Collection(results, + Assert.Collection(results.OrderBy(c => c.SortText), actual => Assert.Equal(actual, CompleteAttributeValue.ExpectedCompletion1), - acutal => Assert.Equal(acutal, CompleteAttributeValue.ExpectedCompletion2), + actual => Assert.Equal(actual, CompleteAttributeValue.ExpectedCompletion2), actual => Assert.Equal(actual, CompleteAttributeValue.ExpectedCompletion3)); }