diff --git a/.gitignore b/.gitignore index b19f5bd8d..7275ea2d4 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ registered_data.ini .dotnet/ module/Plaster module/PSScriptAnalyzer +module/PSReadLine docs/_site/ docs/_repo/ docs/metadata/ diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index a9ab32f43..4cbfc6f17 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -309,6 +309,25 @@ task RestorePsesModules -After Build { Save-Module @splatParameters } + + # TODO: Replace this with adding a new module to Save when a new PSReadLine release comes out to the Gallery + if (-not (Test-Path $PSScriptRoot/module/PSReadLine)) + { + Write-Host "`tInstalling module: PSReadLine" + + # Download AppVeyor zip + $jobId = (Invoke-RestMethod https://ci.appveyor.com/api/projects/lzybkr/PSReadLine).build.jobs[0].jobId + Invoke-RestMethod https://ci.appveyor.com/api/buildjobs/$jobId/artifacts/bin%2FRelease%2FPSReadLine.zip -OutFile $PSScriptRoot/module/PSRL.zip + + # Position PSReadLine + Expand-Archive $PSScriptRoot/module/PSRL.zip $PSScriptRoot/module/PSRL + Move-Item $PSScriptRoot/module/PSRL/PSReadLine $PSScriptRoot/module + + # Clean up + Remove-Item -Force -Recurse $PSScriptRoot/module/PSRL.zip + Remove-Item -Force -Recurse $PSScriptRoot/module/PSRL + } + Write-Host "`n" } diff --git a/appveyor.yml b/appveyor.yml index 6d22ddaf9..24b302c32 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,6 +6,7 @@ skip_tags: true branches: only: - master + - 2.0.0 environment: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true # Don't download unneeded packages diff --git a/module/PowerShellEditorServices/Start-EditorServices.ps1 b/module/PowerShellEditorServices/Start-EditorServices.ps1 index c84ad537a..dffdd466d 100644 --- a/module/PowerShellEditorServices/Start-EditorServices.ps1 +++ b/module/PowerShellEditorServices/Start-EditorServices.ps1 @@ -314,7 +314,8 @@ try { -BundledModulesPath $BundledModulesPath ` -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent + -WaitForDebugger:$WaitForDebugger.IsPresent ` + -FeatureFlags $FeatureFlags # TODO: Verify that the service is started Log "Start-EditorServicesHost returned $editorServicesHost" diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 324590877..12b287e20 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -365,7 +365,7 @@ private EditorSession CreateSession( bool enableConsoleRepl) { EditorSession editorSession = new EditorSession(this.logger); - PowerShellContext powerShellContext = new PowerShellContext(this.logger); + PowerShellContext powerShellContext = new PowerShellContext(this.logger, this.featureFlags.Contains("PSReadLine")); EditorServicesPSHostUserInterface hostUserInterface = enableConsoleRepl @@ -405,7 +405,9 @@ private EditorSession CreateDebugSession( bool enableConsoleRepl) { EditorSession editorSession = new EditorSession(this.logger); - PowerShellContext powerShellContext = new PowerShellContext(this.logger); + PowerShellContext powerShellContext = new PowerShellContext( + this.logger, + this.featureFlags.Contains("PSReadLine")); EditorServicesPSHostUserInterface hostUserInterface = enableConsoleRepl diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index e867e79e8..72662fbf7 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -118,6 +118,17 @@ protected Task LaunchScript(RequestContext requestContext) private async Task OnExecutionCompleted(Task executeTask) { + try + { + await executeTask; + } + catch (Exception e) + { + Logger.Write( + LogLevel.Error, + "Exception occurred while awaiting debug launch task.\n\n" + e.ToString()); + } + Logger.Write(LogLevel.Verbose, "Execution completed, terminating..."); this.executionCompleted = true; @@ -470,7 +481,7 @@ protected async Task HandleDisconnectRequest( if (this.executionCompleted == false) { this.disconnectRequestContext = requestContext; - this.editorSession.PowerShellContext.AbortExecution(); + this.editorSession.PowerShellContext.AbortExecution(shouldAbortDebugSession: true); if (this.isInteractiveDebugSession) { @@ -755,6 +766,20 @@ protected async Task HandleStackTraceRequest( StackFrameDetails[] stackFrames = editorSession.DebugService.GetStackFrames(); + // Handle a rare race condition where the adapter requests stack frames before they've + // begun building. + if (stackFrames == null) + { + await requestContext.SendResult( + new StackTraceResponseBody + { + StackFrames = new StackFrame[0], + TotalFrames = 0 + }); + + return; + } + List newStackFrames = new List(); int startFrameIndex = stackTraceParams.StartFrame ?? 0; diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 684d86d7c..1c974bba8 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -1502,6 +1502,15 @@ private static async Task DelayThenInvokeDiagnostics( catch (TaskCanceledException) { // If the task is cancelled, exit directly + foreach (var script in filesToAnalyze) + { + await PublishScriptDiagnostics( + script, + script.SyntaxMarkers, + correctionIndex, + eventSender); + } + return; } diff --git a/src/PowerShellEditorServices/Console/ConsoleProxy.cs b/src/PowerShellEditorServices/Console/ConsoleProxy.cs new file mode 100644 index 000000000..bd7488169 --- /dev/null +++ b/src/PowerShellEditorServices/Console/ConsoleProxy.cs @@ -0,0 +1,95 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.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 IConsoleOperations s_consoleProxy; + + static ConsoleProxy() + { + // Maybe we should just include the RuntimeInformation package for FullCLR? +#if CoreCLR + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + s_consoleProxy = new WindowsConsoleOperations(); + return; + } + + s_consoleProxy = new UnixConsoleOperations(); +#else + s_consoleProxy = new WindowsConsoleOperations(); +#endif + } + + public static Task ReadKeyAsync(CancellationToken cancellationToken) => + s_consoleProxy.ReadKeyAsync(cancellationToken); + + public static int GetCursorLeft() => + s_consoleProxy.GetCursorLeft(); + + public static int GetCursorLeft(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorLeft(cancellationToken); + + public static Task GetCursorLeftAsync() => + s_consoleProxy.GetCursorLeftAsync(); + + public static Task GetCursorLeftAsync(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorLeftAsync(cancellationToken); + + public static int GetCursorTop() => + s_consoleProxy.GetCursorTop(); + + public static int GetCursorTop(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorTop(cancellationToken); + + public static Task GetCursorTopAsync() => + s_consoleProxy.GetCursorTopAsync(); + + public static Task GetCursorTopAsync(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorTopAsync(cancellationToken); + + /// + /// On Unix platforms this method is sent to PSReadLine as a work around for issues + /// with the System.Console implementation for that platform. 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 UnixReadKey(bool intercept, CancellationToken cancellationToken) + { + try + { + return ((UnixConsoleOperations)s_consoleProxy).ReadKey(intercept, cancellationToken); + } + catch (OperationCanceledException) + { + return default(ConsoleKeyInfo); + } + } + } +} diff --git a/src/PowerShellEditorServices/Console/ConsoleReadLine.cs b/src/PowerShellEditorServices/Console/ConsoleReadLine.cs index a3da640c7..9d8af2b8b 100644 --- a/src/PowerShellEditorServices/Console/ConsoleReadLine.cs +++ b/src/PowerShellEditorServices/Console/ConsoleReadLine.cs @@ -6,7 +6,6 @@ using System.Collections.ObjectModel; using System.Linq; using System.Text; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -20,8 +19,6 @@ namespace Microsoft.PowerShell.EditorServices.Console internal class ConsoleReadLine { #region Private Field - private static IConsoleOperations s_consoleProxy; - private PowerShellContext powerShellContext; #endregion @@ -29,18 +26,6 @@ internal class ConsoleReadLine #region Constructors static ConsoleReadLine() { - // Maybe we should just include the RuntimeInformation package for FullCLR? - #if CoreCLR - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - s_consoleProxy = new WindowsConsoleOperations(); - return; - } - - s_consoleProxy = new UnixConsoleOperations(); - #else - s_consoleProxy = new WindowsConsoleOperations(); - #endif } public ConsoleReadLine(PowerShellContext powerShellContext) @@ -54,20 +39,20 @@ public ConsoleReadLine(PowerShellContext powerShellContext) public Task ReadCommandLine(CancellationToken cancellationToken) { - return this.ReadLine(true, cancellationToken); + return this.ReadLineAsync(true, cancellationToken); } public Task ReadSimpleLine(CancellationToken cancellationToken) { - return this.ReadLine(false, cancellationToken); + return this.ReadLineAsync(false, cancellationToken); } public async Task ReadSecureLine(CancellationToken cancellationToken) { SecureString secureString = new SecureString(); - int initialPromptRow = Console.CursorTop; - int initialPromptCol = Console.CursorLeft; + int initialPromptRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + int initialPromptCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); int previousInputLength = 0; Console.TreatControlCAsInput = true; @@ -114,7 +99,8 @@ public async Task ReadSecureLine(CancellationToken cancellationTok } else if (previousInputLength > 0 && currentInputLength < previousInputLength) { - int row = Console.CursorTop, col = Console.CursorLeft; + int row = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + int col = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); // Back up the cursor before clearing the character col--; @@ -146,10 +132,30 @@ public async Task ReadSecureLine(CancellationToken cancellationTok private static async Task ReadKeyAsync(CancellationToken cancellationToken) { - return await s_consoleProxy.ReadKeyAsync(cancellationToken); + return await ConsoleProxy.ReadKeyAsync(cancellationToken); + } + + private async Task ReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await this.powerShellContext.InvokeReadLineAsync(isCommandLine, cancellationToken); } - private async Task ReadLine(bool isCommandLine, CancellationToken cancellationToken) + /// + /// Invokes a custom ReadLine method that is similar to but more basic than PSReadLine. + /// This method should be used when PSReadLine is disabled, either by user settings or + /// unsupported PowerShell versions. + /// + /// + /// Indicates whether ReadLine should act like a command line. + /// + /// + /// The cancellation token that will be checked prior to completing the returned task. + /// + /// + /// A task object representing the asynchronus operation. The Result property on + /// the task object returns the user input string. + /// + internal async Task InvokeLegacyReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) { string inputBeforeCompletion = null; string inputAfterCompletion = null; @@ -160,8 +166,8 @@ private async Task ReadLine(bool isCommandLine, CancellationToken cancel StringBuilder inputLine = new StringBuilder(); - int initialCursorCol = Console.CursorLeft; - int initialCursorRow = Console.CursorTop; + int initialCursorCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); + int initialCursorRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken); int initialWindowLeft = Console.WindowLeft; int initialWindowTop = Console.WindowTop; @@ -492,8 +498,8 @@ private int CalculateIndexFromCursor( int consoleWidth) { return - ((Console.CursorTop - promptStartRow) * consoleWidth) + - Console.CursorLeft - promptStartCol; + ((ConsoleProxy.GetCursorTop() - promptStartRow) * consoleWidth) + + ConsoleProxy.GetCursorLeft() - promptStartCol; } private void CalculateCursorFromIndex( diff --git a/src/PowerShellEditorServices/Console/IConsoleOperations.cs b/src/PowerShellEditorServices/Console/IConsoleOperations.cs index 721ae8ff7..a5556eda5 100644 --- a/src/PowerShellEditorServices/Console/IConsoleOperations.cs +++ b/src/PowerShellEditorServices/Console/IConsoleOperations.cs @@ -1,3 +1,8 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Threading; using System.Threading.Tasks; @@ -18,5 +23,97 @@ public interface IConsoleOperations /// A task that will complete with a result of the key pressed by the user. /// Task ReadKeyAsync(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/Console/UnixConsoleOperations.cs b/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs index ab5cccfd6..df5ec2460 100644 --- a/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs +++ b/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs @@ -1,19 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; using UnixConsoleEcho; namespace Microsoft.PowerShell.EditorServices.Console { internal class UnixConsoleOperations : IConsoleOperations { - private const int LONG_READ_DELAY = 300; + private const int LongWaitForKeySleepTime = 300; + + private const int ShortWaitForKeyTimeout = 5000; + + private const int ShortWaitForKeySpinUntilSleepTime = 30; + + private static readonly ManualResetEventSlim s_waitHandle = new ManualResetEventSlim(); - private const int SHORT_READ_TIMEOUT = 5000; + private static readonly SemaphoreSlim s_readKeyHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - private static readonly ManualResetEventSlim _waitHandle = new ManualResetEventSlim(); + private static readonly SemaphoreSlim s_stdInHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - private SemaphoreSlim _readKeyHandle = new SemaphoreSlim(1, 1); + private Func WaitForKeyAvailable; + + private Func> WaitForKeyAvailableAsync; internal UnixConsoleOperations() { @@ -21,65 +35,250 @@ internal UnixConsoleOperations() // user has recently (last 5 seconds) pressed a key to avoid preventing // the CPU from entering low power mode. WaitForKeyAvailable = LongWaitForKey; + WaitForKeyAvailableAsync = LongWaitForKeyAsync; + } + + internal 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. + 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 + { + 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(CancellationToken cancellationToken) { - await _readKeyHandle.WaitAsync(cancellationToken); + await s_readKeyHandle.WaitAsync(cancellationToken); // 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`. InputEcho.Disable(); try { - while (!await WaitForKeyAvailable(cancellationToken)); + while (!await WaitForKeyAvailableAsync(cancellationToken)); } finally { InputEcho.Enable(); - _readKeyHandle.Release(); + s_readKeyHandle.Release(); + } + + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.ReadKey(intercept: true); + } + finally + { + s_stdInHandle.Release(); } + } - return System.Console.ReadKey(intercept: true); + public int GetCursorLeft() + { + return GetCursorLeft(CancellationToken.None); } - private Func> WaitForKeyAvailable; + public int GetCursorLeft(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.CursorLeft; + } + finally + { + s_stdInHandle.Release(); + } + } + + public async Task GetCursorLeftAsync() + { + return await GetCursorLeftAsync(CancellationToken.None); + } - private async Task LongWaitForKey(CancellationToken cancellationToken) + public async Task GetCursorLeftAsync(CancellationToken cancellationToken) { - while (!System.Console.KeyAvailable) + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.CursorLeft; + } + finally { - await Task.Delay(LONG_READ_DELAY, cancellationToken); + s_stdInHandle.Release(); } + } + + public int GetCursorTop() + { + return GetCursorTop(CancellationToken.None); + } + public int GetCursorTop(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.CursorTop; + } + finally + { + s_stdInHandle.Release(); + } + } + + public async Task GetCursorTopAsync() + { + return await GetCursorTopAsync(CancellationToken.None); + } + + public async Task GetCursorTopAsync(CancellationToken cancellationToken) + { + await s_stdInHandle.WaitAsync(cancellationToken); + 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 ShortWaitForKey(CancellationToken cancellationToken) + private async Task LongWaitForKeyAsync(CancellationToken cancellationToken) + { + while (!await IsKeyAvailableAsync(cancellationToken)) + { + await Task.Delay(LongWaitForKeySleepTime, cancellationToken); + } + + WaitForKeyAvailableAsync = ShortWaitForKeyAsync; + return true; + } + + private bool ShortWaitForKey(CancellationToken cancellationToken) { - if (await SpinUntilKeyAvailable(SHORT_READ_TIMEOUT, 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 SpinUntilKeyAvailable(int millisecondsTimeout, CancellationToken cancellationToken) + private async Task ShortWaitForKeyAsync(CancellationToken cancellationToken) + { + if (await SpinUntilKeyAvailableAsync(ShortWaitForKeyTimeout, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + return true; + } + + cancellationToken.ThrowIfCancellationRequested(); + WaitForKeyAvailableAsync = LongWaitForKeyAsync; + return false; + } + + private bool SpinUntilKeyAvailable(int millisecondsTimeout, CancellationToken cancellationToken) + { + return SpinWait.SpinUntil( + () => + { + s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); + return IsKeyAvailable(cancellationToken); + }, + millisecondsTimeout); + } + + private async Task SpinUntilKeyAvailableAsync(int millisecondsTimeout, CancellationToken cancellationToken) { return await Task.Factory.StartNew( () => SpinWait.SpinUntil( () => { // The wait handle is never set, it's just used to enable cancelling the wait. - _waitHandle.Wait(30, cancellationToken); - return System.Console.KeyAvailable || cancellationToken.IsCancellationRequested; + s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); + return IsKeyAvailable(cancellationToken); }, millisecondsTimeout)); } + + private bool IsKeyAvailable(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.KeyAvailable; + } + finally + { + s_stdInHandle.Release(); + } + } + + private async Task IsKeyAvailableAsync(CancellationToken cancellationToken) + { + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.KeyAvailable; + } + finally + { + s_stdInHandle.Release(); + } + } } } diff --git a/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs b/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs index 3158c87c4..86c543123 100644 --- a/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs +++ b/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs @@ -1,6 +1,12 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Console { @@ -8,7 +14,23 @@ internal class WindowsConsoleOperations : IConsoleOperations { private ConsoleKeyInfo? _bufferedKey; - private SemaphoreSlim _readKeyHandle = new SemaphoreSlim(1, 1); + private 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(CancellationToken cancellationToken) { diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index 1fdae53db..1cb201f07 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -15,6 +15,7 @@ using Microsoft.PowerShell.EditorServices.Utility; using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Session.Capabilities; +using System.Threading; namespace Microsoft.PowerShell.EditorServices { @@ -47,6 +48,7 @@ public class DebugService private static int breakpointHitCounter = 0; + private SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore(); #endregion #region Properties @@ -350,7 +352,7 @@ public void Break() /// public void Abort() { - this.powerShellContext.AbortExecution(); + this.powerShellContext.AbortExecution(shouldAbortDebugSession: true); } /// @@ -362,33 +364,40 @@ public void Abort() public VariableDetailsBase[] GetVariables(int variableReferenceId) { VariableDetailsBase[] childVariables; - - if ((variableReferenceId < 0) || (variableReferenceId >= this.variables.Count)) + this.debugInfoHandle.Wait(); + try { - logger.Write(LogLevel.Warning, $"Received request for variableReferenceId {variableReferenceId} that is out of range of valid indices."); - return new VariableDetailsBase[0]; - } + if ((variableReferenceId < 0) || (variableReferenceId >= this.variables.Count)) + { + logger.Write(LogLevel.Warning, $"Received request for variableReferenceId {variableReferenceId} that is out of range of valid indices."); + return new VariableDetailsBase[0]; + } - VariableDetailsBase parentVariable = this.variables[variableReferenceId]; - if (parentVariable.IsExpandable) - { - childVariables = parentVariable.GetChildren(this.logger); - foreach (var child in childVariables) + VariableDetailsBase parentVariable = this.variables[variableReferenceId]; + if (parentVariable.IsExpandable) { - // Only add child if it hasn't already been added. - if (child.Id < 0) + childVariables = parentVariable.GetChildren(this.logger); + foreach (var child in childVariables) { - child.Id = this.nextVariableId++; - this.variables.Add(child); + // Only add child if it hasn't already been added. + if (child.Id < 0) + { + child.Id = this.nextVariableId++; + this.variables.Add(child); + } } } + else + { + childVariables = new VariableDetailsBase[0]; + } + + return childVariables; } - else + finally { - childVariables = new VariableDetailsBase[0]; + this.debugInfoHandle.Release(); } - - return childVariables; } /// @@ -410,7 +419,18 @@ public VariableDetailsBase GetVariableFromExpression(string variableExpression, string[] variablePathParts = variableExpression.Split('.'); VariableDetailsBase resolvedVariable = null; - IEnumerable variableList = this.variables; + IEnumerable variableList; + + // Ensure debug info isn't currently being built. + this.debugInfoHandle.Wait(); + try + { + variableList = this.variables; + } + finally + { + this.debugInfoHandle.Release(); + } foreach (var variableName in variablePathParts) { @@ -491,9 +511,18 @@ await this.powerShellContext.ExecuteCommand( // OK, now we have a PS object from the supplied value string (expression) to assign to a variable. // Get the variable referenced by variableContainerReferenceId and variable name. - VariableContainerDetails variableContainer = (VariableContainerDetails)this.variables[variableContainerReferenceId]; - VariableDetailsBase variable = variableContainer.Children[name]; + VariableContainerDetails variableContainer = null; + await this.debugInfoHandle.WaitAsync(); + try + { + variableContainer = (VariableContainerDetails)this.variables[variableContainerReferenceId]; + } + finally + { + this.debugInfoHandle.Release(); + } + VariableDetailsBase variable = variableContainer.Children[name]; // Determine scope in which the variable lives. This is required later for the call to Get-Variable -Scope. string scope = null; if (variableContainerReferenceId == this.scriptScopeVariables.Id) @@ -507,9 +536,10 @@ await this.powerShellContext.ExecuteCommand( else { // Determine which stackframe's local scope the variable is in. - for (int i = 0; i < this.stackFrameDetails.Length; i++) + StackFrameDetails[] stackFrames = await this.GetStackFramesAsync(); + for (int i = 0; i < stackFrames.Length; i++) { - var stackFrame = this.stackFrameDetails[i]; + var stackFrame = stackFrames[i]; if (stackFrame.LocalVariables.ContainsVariable(variable.Id)) { scope = i.ToString(); @@ -637,7 +667,54 @@ await this.powerShellContext.ExecuteScriptString( /// public StackFrameDetails[] GetStackFrames() { - return this.stackFrameDetails; + this.debugInfoHandle.Wait(); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } + } + + internal StackFrameDetails[] GetStackFrames(CancellationToken cancellationToken) + { + this.debugInfoHandle.Wait(cancellationToken); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } + } + + internal async Task GetStackFramesAsync() + { + await this.debugInfoHandle.WaitAsync(); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } + } + + internal async Task GetStackFramesAsync(CancellationToken cancellationToken) + { + await this.debugInfoHandle.WaitAsync(cancellationToken); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } } /// @@ -648,8 +725,9 @@ public StackFrameDetails[] GetStackFrames() /// The list of VariableScope instances which describe the available variable scopes. public VariableScope[] GetVariableScopes(int stackFrameId) { - int localStackFrameVariableId = this.stackFrameDetails[stackFrameId].LocalVariables.Id; - int autoVariablesId = this.stackFrameDetails[stackFrameId].AutoVariables.Id; + var stackFrames = this.GetStackFrames(); + int localStackFrameVariableId = stackFrames[stackFrameId].LocalVariables.Id; + int autoVariablesId = stackFrames[stackFrameId].AutoVariables.Id; return new VariableScope[] { @@ -709,16 +787,24 @@ private async Task ClearCommandBreakpoints() private async Task FetchStackFramesAndVariables(string scriptNameOverride) { - this.nextVariableId = VariableDetailsBase.FirstVariableId; - this.variables = new List(); + await this.debugInfoHandle.WaitAsync(); + try + { + this.nextVariableId = VariableDetailsBase.FirstVariableId; + this.variables = new List(); - // Create a dummy variable for index 0, should never see this. - this.variables.Add(new VariableDetails("Dummy", null)); + // Create a dummy variable for index 0, should never see this. + this.variables.Add(new VariableDetails("Dummy", null)); - // Must retrieve global/script variales before stack frame variables - // as we check stack frame variables against globals. - await FetchGlobalAndScriptVariables(); - await FetchStackFrames(scriptNameOverride); + // Must retrieve global/script variales before stack frame variables + // as we check stack frame variables against globals. + await FetchGlobalAndScriptVariables(); + await FetchStackFrames(scriptNameOverride); + } + finally + { + this.debugInfoHandle.Release(); + } } private async Task FetchGlobalAndScriptVariables() @@ -851,6 +937,7 @@ private async Task FetchStackFrames(string scriptNameOverride) var results = await this.powerShellContext.ExecuteCommand(psCommand); var callStackFrames = results.ToArray(); + this.stackFrameDetails = new StackFrameDetails[callStackFrames.Length]; for (int i = 0; i < callStackFrames.Length; i++) diff --git a/src/PowerShellEditorServices/Language/AstOperations.cs b/src/PowerShellEditorServices/Language/AstOperations.cs index c28416280..e90be6bb4 100644 --- a/src/PowerShellEditorServices/Language/AstOperations.cs +++ b/src/PowerShellEditorServices/Language/AstOperations.cs @@ -16,13 +16,14 @@ namespace Microsoft.PowerShell.EditorServices using System.Diagnostics; using System.Management.Automation; using System.Management.Automation.Language; - using System.Management.Automation.Runspaces; /// /// Provides common operations for the syntax tree of a parsed script. /// internal static class AstOperations { + private static readonly SemaphoreSlim s_completionHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + /// /// Gets completions for the symbol found in the Ast at /// the given file offset. @@ -55,88 +56,95 @@ static public async Task GetCompletions( ILogger logger, CancellationToken cancellationToken) { - var type = scriptAst.Extent.StartScriptPosition.GetType(); - var method = + if (!s_completionHandle.Wait(0)) + { + return null; + } + + try + { + var type = scriptAst.Extent.StartScriptPosition.GetType(); + var method = #if CoreCLR - type.GetMethod( - "CloneWithNewOffset", - BindingFlags.Instance | BindingFlags.NonPublic); + type.GetMethod( + "CloneWithNewOffset", + BindingFlags.Instance | BindingFlags.NonPublic); #else - type.GetMethod( - "CloneWithNewOffset", - BindingFlags.Instance | BindingFlags.NonPublic, - null, - new[] { typeof(int) }, null); + type.GetMethod( + "CloneWithNewOffset", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + new[] { typeof(int) }, null); #endif - IScriptPosition cursorPosition = - (IScriptPosition)method.Invoke( - scriptAst.Extent.StartScriptPosition, - new object[] { fileOffset }); - - logger.Write( - LogLevel.Verbose, - string.Format( - "Getting completions at offset {0} (line: {1}, column: {2})", - fileOffset, - cursorPosition.LineNumber, - cursorPosition.ColumnNumber)); - - CommandCompletion commandCompletion = null; - if (powerShellContext.IsDebuggerStopped) - { - PSCommand command = new PSCommand(); - command.AddCommand("TabExpansion2"); - command.AddParameter("Ast", scriptAst); - command.AddParameter("Tokens", currentTokens); - command.AddParameter("PositionOfCursor", cursorPosition); - command.AddParameter("Options", null); - - PSObject outputObject = - (await powerShellContext.ExecuteCommand(command, false, false)) - .FirstOrDefault(); - - if (outputObject != null) + IScriptPosition cursorPosition = + (IScriptPosition)method.Invoke( + scriptAst.Extent.StartScriptPosition, + new object[] { fileOffset }); + + logger.Write( + LogLevel.Verbose, + string.Format( + "Getting completions at offset {0} (line: {1}, column: {2})", + fileOffset, + cursorPosition.LineNumber, + cursorPosition.ColumnNumber)); + + if (!powerShellContext.IsAvailable) { - ErrorRecord errorRecord = outputObject.BaseObject as ErrorRecord; - if (errorRecord != null) - { - logger.WriteException( - "Encountered an error while invoking TabExpansion2 in the debugger", - errorRecord.Exception); - } - else + return null; + } + + var stopwatch = new Stopwatch(); + + // If the current runspace is out of process we can use + // CommandCompletion.CompleteInput because PSReadLine won't be taking up the + // main runspace. + if (powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandle(cancellationToken)) + using (PowerShell powerShell = PowerShell.Create()) { - commandCompletion = outputObject.BaseObject as CommandCompletion; + powerShell.Runspace = runspaceHandle.Runspace; + stopwatch.Start(); + try + { + return CommandCompletion.CompleteInput( + scriptAst, + currentTokens, + cursorPosition, + options: null, + powershell: powerShell); + } + finally + { + stopwatch.Stop(); + logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + } } } - } - else if (powerShellContext.CurrentRunspace.Runspace.RunspaceAvailability == - RunspaceAvailability.Available) - { - using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandle(cancellationToken)) - using (PowerShell powerShell = PowerShell.Create()) - { - powerShell.Runspace = runspaceHandle.Runspace; - - Stopwatch stopwatch = new Stopwatch(); - stopwatch.Start(); - commandCompletion = - CommandCompletion.CompleteInput( + CommandCompletion commandCompletion = null; + await powerShellContext.InvokeOnPipelineThread( + pwsh => + { + stopwatch.Start(); + commandCompletion = CommandCompletion.CompleteInput( scriptAst, currentTokens, cursorPosition, - null, - powerShell); - - stopwatch.Stop(); + options: null, + powershell: pwsh); + }); + stopwatch.Stop(); + logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - } + return commandCompletion; + } + finally + { + s_completionHandle.Release(); } - - return commandCompletion; } /// diff --git a/src/PowerShellEditorServices/Language/LanguageService.cs b/src/PowerShellEditorServices/Language/LanguageService.cs index 5dfad5aa4..70747a523 100644 --- a/src/PowerShellEditorServices/Language/LanguageService.cs +++ b/src/PowerShellEditorServices/Language/LanguageService.cs @@ -34,6 +34,7 @@ public class LanguageService private Dictionary> CmdletToAliasDictionary; private Dictionary AliasToCmdletDictionary; private IDocumentSymbolProvider[] documentSymbolProviders; + private SemaphoreSlim aliasHandle = AsyncUtils.CreateSimpleLockingSemaphore(); const int DefaultWaitTimeoutMilliseconds = 5000; @@ -323,30 +324,39 @@ public async Task FindReferencesOfSymbol( foreach (var fileName in fileMap.Keys) { var file = (ScriptFile)fileMap[fileName]; - IEnumerable symbolReferencesinFile = - AstOperations - .FindReferencesOfSymbol( - file.ScriptAst, - foundSymbol, - CmdletToAliasDictionary, - AliasToCmdletDictionary) - .Select( - reference => - { - try - { - reference.SourceLine = - file.GetLine(reference.ScriptRegion.StartLineNumber); - } - catch (ArgumentOutOfRangeException e) - { - reference.SourceLine = string.Empty; - this.logger.WriteException("Found reference is out of range in script file", e); - } - - reference.FilePath = file.FilePath; - return reference; - }); + IEnumerable symbolReferencesinFile; + await this.aliasHandle.WaitAsync(); + try + { + symbolReferencesinFile = + AstOperations + .FindReferencesOfSymbol( + file.ScriptAst, + foundSymbol, + CmdletToAliasDictionary, + AliasToCmdletDictionary) + .Select( + reference => + { + try + { + reference.SourceLine = + file.GetLine(reference.ScriptRegion.StartLineNumber); + } + catch (ArgumentOutOfRangeException e) + { + reference.SourceLine = string.Empty; + this.logger.WriteException("Found reference is out of range in script file", e); + } + + reference.FilePath = file.FilePath; + return reference; + }); + } + finally + { + this.aliasHandle.Release(); + } symbolReferences.AddRange(symbolReferencesinFile); } @@ -669,21 +679,33 @@ public FunctionDefinitionAst GetFunctionDefinitionForHelpComment( /// private async Task GetAliases() { - if (!this.areAliasesLoaded) + await this.aliasHandle.WaitAsync(); + try { - try + if (!this.areAliasesLoaded) { - RunspaceHandle runspaceHandle = - await this.powerShellContext.GetRunspaceHandle( - new CancellationTokenSource(DefaultWaitTimeoutMilliseconds).Token); - - CommandInvocationIntrinsics invokeCommand = runspaceHandle.Runspace.SessionStateProxy.InvokeCommand; - IEnumerable aliases = invokeCommand.GetCommands("*", CommandTypes.Alias, true); + if (this.powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + this.areAliasesLoaded = true; + return; + } - runspaceHandle.Dispose(); + var aliases = await this.powerShellContext.ExecuteCommand( + new PSCommand() + .AddCommand("Microsoft.PowerShell.Core\\Get-Command") + .AddParameter("CommandType", CommandTypes.Alias), + sendOutputToHost: false, + sendErrorToHost: false); foreach (AliasInfo aliasInfo in aliases) { + // Using Get-Command will obtain aliases from modules not yet loaded, + // these aliases will not have a definition. + if (string.IsNullOrEmpty(aliasInfo.Definition)) + { + continue; + } + if (!CmdletToAliasDictionary.ContainsKey(aliasInfo.Definition)) { CmdletToAliasDictionary.Add(aliasInfo.Definition, new List() { aliasInfo.Name }); @@ -698,19 +720,10 @@ await this.powerShellContext.GetRunspaceHandle( this.areAliasesLoaded = true; } - catch (PSNotSupportedException e) - { - this.logger.Write( - LogLevel.Warning, - $"Caught PSNotSupportedException while attempting to get aliases from remote session:\n\n{e.ToString()}"); - - // Prevent the aliases from being fetched again - no point if the remote doesn't support InvokeCommand. - this.areAliasesLoaded = true; - } - catch (TaskCanceledException) - { - // The wait for a RunspaceHandle has timed out, skip aliases for now - } + } + finally + { + this.aliasHandle.Release(); } } diff --git a/src/PowerShellEditorServices/Session/ExecutionOptions.cs b/src/PowerShellEditorServices/Session/ExecutionOptions.cs index 3372c7556..a1071606f 100644 --- a/src/PowerShellEditorServices/Session/ExecutionOptions.cs +++ b/src/PowerShellEditorServices/Session/ExecutionOptions.cs @@ -10,6 +10,8 @@ namespace Microsoft.PowerShell.EditorServices /// public class ExecutionOptions { + private bool? _shouldExecuteInOriginalRunspace; + #region Properties /// @@ -38,6 +40,36 @@ public class ExecutionOptions /// public bool InterruptCommandPrompt { get; set; } + /// + /// Gets or sets a value indicating whether the text of the command + /// should be written to the host as if it was ran interactively. + /// + public bool WriteInputToHost { get; set; } + + /// + /// Gets or sets a value indicating whether the command to + /// be executed is a console input prompt, such as the + /// PSConsoleHostReadLine function. + /// + internal bool IsReadLine { get; set; } + + /// + /// Gets or sets a value indicating whether the command should + /// be invoked in the original runspace. In the majority of cases + /// this should remain unset. + /// + internal bool ShouldExecuteInOriginalRunspace + { + get + { + return _shouldExecuteInOriginalRunspace ?? IsReadLine; + } + set + { + _shouldExecuteInOriginalRunspace = value; + } + } + #endregion #region Constructors @@ -50,6 +82,7 @@ public ExecutionOptions() { this.WriteOutputToHost = true; this.WriteErrorsToHost = true; + this.WriteInputToHost = false; this.AddToHistory = false; this.InterruptCommandPrompt = false; } diff --git a/src/PowerShellEditorServices/Session/ExecutionTarget.cs b/src/PowerShellEditorServices/Session/ExecutionTarget.cs new file mode 100644 index 000000000..70ec3cb6f --- /dev/null +++ b/src/PowerShellEditorServices/Session/ExecutionTarget.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Represents the different API's available for executing commands. + /// + internal enum ExecutionTarget + { + /// + /// Indicates that the command should be invoked through the PowerShell debugger. + /// + Debugger, + + /// + /// Indicates that the command should be invoked via an instance of the PowerShell class. + /// + PowerShell, + + /// + /// Indicates that the command should be invoked through the PowerShell engine's event manager. + /// + InvocationEvent + } +} diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs index 33925f044..7c32acdb3 100644 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs +++ b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs @@ -26,6 +26,7 @@ public class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession private Guid instanceId = Guid.NewGuid(); private EditorServicesPSHostUserInterface hostUserInterface; private IHostSupportsInteractiveSession hostSupportsInteractiveSession; + private PowerShellContext powerShellContext; #endregion @@ -55,6 +56,7 @@ public EditorServicesPSHost( this.hostDetails = hostDetails; this.hostUserInterface = hostUserInterface; this.hostSupportsInteractiveSession = powerShellContext; + this.powerShellContext = powerShellContext; } #endregion @@ -251,7 +253,7 @@ public override PSHostUserInterface UI /// public override void EnterNestedPrompt() { - Logger.Write(LogLevel.Verbose, "EnterNestedPrompt() called."); + this.powerShellContext.EnterNestedPrompt(); } /// @@ -259,7 +261,7 @@ public override void EnterNestedPrompt() /// public override void ExitNestedPrompt() { - Logger.Write(LogLevel.Verbose, "ExitNestedPrompt() called."); + this.powerShellContext.ExitNestedPrompt(); } /// diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs index 2aceaaf10..2cbf29365 100644 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs @@ -112,7 +112,10 @@ public EditorServicesPSHostUserInterface( #region Public Methods - void IHostInput.StartCommandLoop() + /// + /// Starts the host's interactive command loop. + /// + public void StartCommandLoop() { if (!this.IsCommandLoopRunning) { @@ -121,7 +124,10 @@ void IHostInput.StartCommandLoop() } } - void IHostInput.StopCommandLoop() + /// + /// Stops the host's interactive command loop. + /// + public void StopCommandLoop() { if (this.IsCommandLoopRunning) { @@ -636,10 +642,28 @@ public Collection PromptForChoice( #region Private Methods - private async Task WritePromptStringToHost() + private Coordinates lastPromptLocation; + + private async Task WritePromptStringToHost(CancellationToken cancellationToken) { + try + { + if (this.lastPromptLocation != null && + this.lastPromptLocation.X == await ConsoleProxy.GetCursorLeftAsync(cancellationToken) && + this.lastPromptLocation.Y == await ConsoleProxy.GetCursorTopAsync(cancellationToken)) + { + return; + } + } + // When output is redirected (like when running tests) attempting to get + // the cursor position will throw. + catch (System.IO.IOException) + { + } + PSCommand promptCommand = new PSCommand().AddScript("prompt"); + cancellationToken.ThrowIfCancellationRequested(); string promptString = (await this.powerShellContext.ExecuteCommand(promptCommand, false, false)) .Select(pso => pso.BaseObject) @@ -669,8 +693,13 @@ private async Task WritePromptStringToHost() promptString); } + cancellationToken.ThrowIfCancellationRequested(); + // Write the prompt string this.WriteOutput(promptString, false); + this.lastPromptLocation = new Coordinates( + await ConsoleProxy.GetCursorLeftAsync(cancellationToken), + await ConsoleProxy.GetCursorTopAsync(cancellationToken)); } private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs) @@ -707,14 +736,23 @@ private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs) private async Task StartReplLoop(CancellationToken cancellationToken) { - do + while (!cancellationToken.IsCancellationRequested) { string commandString = null; + int originalCursorTop = 0; - await this.WritePromptStringToHost(); + try + { + await this.WritePromptStringToHost(cancellationToken); + } + catch (OperationCanceledException) + { + break; + } try { + originalCursorTop = await ConsoleProxy.GetCursorTopAsync(cancellationToken); commandString = await this.ReadCommandLine(cancellationToken); } catch (PipelineStoppedException) @@ -739,29 +777,29 @@ private async Task StartReplLoop(CancellationToken cancellationToken) Logger.WriteException("Caught exception while reading command line", e); } - - if (commandString != null) + finally { - if (!string.IsNullOrWhiteSpace(commandString)) - { - var unusedTask = - this.powerShellContext - .ExecuteScriptString( - commandString, - false, - true, - true) - .ConfigureAwait(false); - - break; - } - else + if (!cancellationToken.IsCancellationRequested && + originalCursorTop == await ConsoleProxy.GetCursorTopAsync(cancellationToken)) { - this.WriteOutput(string.Empty); + this.WriteLine(); } } + + if (!string.IsNullOrWhiteSpace(commandString)) + { + var unusedTask = + this.powerShellContext + .ExecuteScriptString( + commandString, + writeInputToHost: false, + writeOutputToHost: true, + addToHistory: true) + .ConfigureAwait(continueOnCapturedContext: false); + + break; + } } - while (!cancellationToken.IsCancellationRequested); } private InputPromptHandler CreateInputPromptHandler() @@ -856,6 +894,12 @@ private void WaitForPromptCompletion( private void PowerShellContext_DebuggerStop(object sender, System.Management.Automation.DebuggerStopEventArgs e) { + if (!this.IsCommandLoopRunning) + { + StartCommandLoop(); + return; + } + // Cancel any existing prompt first this.CancelCommandPrompt(); @@ -871,45 +915,42 @@ private void PowerShellContext_DebuggerResumed(object sender, System.Management. private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionStatusChangedEventArgs eventArgs) { // The command loop should only be manipulated if it's already started - if (this.IsCommandLoopRunning) + if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted) { - if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted) + // When aborted, cancel any lingering prompts + if (this.activePromptHandler != null) { - // When aborted, cancel any lingering prompts - if (this.activePromptHandler != null) - { - this.activePromptHandler.CancelPrompt(); - this.WriteOutput(string.Empty); - } + this.activePromptHandler.CancelPrompt(); + this.WriteOutput(string.Empty); } - else if ( - eventArgs.ExecutionOptions.WriteOutputToHost || - eventArgs.ExecutionOptions.InterruptCommandPrompt) + } + else if ( + eventArgs.ExecutionOptions.WriteOutputToHost || + eventArgs.ExecutionOptions.InterruptCommandPrompt) + { + // Any command which writes output to the host will affect + // the display of the prompt + if (eventArgs.ExecutionStatus != ExecutionStatus.Running) { - // Any command which writes output to the host will affect - // the display of the prompt - if (eventArgs.ExecutionStatus != ExecutionStatus.Running) - { - // Execution has completed, start the input prompt - this.ShowCommandPrompt(); - } - else - { - // A new command was started, cancel the input prompt - this.CancelCommandPrompt(); - this.WriteOutput(string.Empty); - } + // Execution has completed, start the input prompt + this.ShowCommandPrompt(); + StartCommandLoop(); } - else if ( - eventArgs.ExecutionOptions.WriteErrorsToHost && - (eventArgs.ExecutionStatus == ExecutionStatus.Failed || - eventArgs.HadErrors)) + else { + // A new command was started, cancel the input prompt + StopCommandLoop(); this.CancelCommandPrompt(); - this.WriteOutput(string.Empty); - this.ShowCommandPrompt(); } } + else if ( + eventArgs.ExecutionOptions.WriteErrorsToHost && + (eventArgs.ExecutionStatus == ExecutionStatus.Failed || + eventArgs.HadErrors)) + { + this.WriteOutput(string.Empty, true); + var unusedTask = this.WritePromptStringToHost(CancellationToken.None); + } } #endregion diff --git a/src/PowerShellEditorServices/Session/IPromptContext.cs b/src/PowerShellEditorServices/Session/IPromptContext.cs new file mode 100644 index 000000000..157715e7d --- /dev/null +++ b/src/PowerShellEditorServices/Session/IPromptContext.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides methods for interacting with implementations of ReadLine. + /// + public interface IPromptContext + { + /// + /// Read a string that has been input by the user. + /// + /// Indicates if ReadLine should act like a command REPL. + /// + /// The cancellation token can be used to cancel reading user input. + /// + /// + /// A task object that represents the completion of reading input. The Result property will + /// return the input string. + /// + Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken); + + /// + /// Performs any additional actions required to cancel the current ReadLine invocation. + /// + void AbortReadLine(); + + /// + /// Creates a task that completes when the current ReadLine invocation has been aborted. + /// + /// + /// A task object that represents the abortion of the current ReadLine invocation. + /// + Task AbortReadLineAsync(); + + /// + /// Blocks until the current ReadLine invocation has exited. + /// + void WaitForReadLineExit(); + + /// + /// Creates a task that completes when the current ReadLine invocation has exited. + /// + /// + /// A task object that represents the exit of the current ReadLine invocation. + /// + Task WaitForReadLineExitAsync(); + + /// + /// Adds the specified command to the history managed by the ReadLine implementation. + /// + /// The command to record. + void AddToHistory(string command); + + /// + /// Forces the prompt handler to trigger PowerShell event handling, reliquishing control + /// of the pipeline thread during event processing. + /// + void ForcePSEventHandling(); + } +} diff --git a/src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs b/src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs index d47264478..55540ba9d 100644 --- a/src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs +++ b/src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Management.Automation; +using System.Management.Automation.Host; using System.Management.Automation.Runspaces; namespace Microsoft.PowerShell.EditorServices.Session @@ -21,6 +22,12 @@ IEnumerable ExecuteCommandInDebugger( PSCommand psCommand, bool sendOutputToHost, out DebuggerResumeAction? debuggerResumeAction); + + void StopCommandInDebugger(PowerShellContext powerShellContext); + + bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace); + + void ExitNestedPrompt(PSHost host); } } diff --git a/src/PowerShellEditorServices/Session/InvocationEventQueue.cs b/src/PowerShellEditorServices/Session/InvocationEventQueue.cs new file mode 100644 index 000000000..77d85bf23 --- /dev/null +++ b/src/PowerShellEditorServices/Session/InvocationEventQueue.cs @@ -0,0 +1,263 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Management.Automation.Runspaces; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System.Management.Automation; + + /// + /// Provides the ability to take over the current pipeline in a runspace. + /// + internal class InvocationEventQueue + { + private const string ShouldProcessInExecutionThreadPropertyName = "ShouldProcessInExecutionThread"; + + private static readonly PropertyInfo s_shouldProcessInExecutionThreadProperty = + typeof(PSEventSubscriber) + .GetProperty( + ShouldProcessInExecutionThreadPropertyName, + BindingFlags.Instance | BindingFlags.NonPublic); + + private readonly PromptNest _promptNest; + + private readonly Runspace _runspace; + + private readonly PowerShellContext _powerShellContext; + + private InvocationRequest _invocationRequest; + + private SemaphoreSlim _lock = AsyncUtils.CreateSimpleLockingSemaphore(); + + private InvocationEventQueue(PowerShellContext powerShellContext, PromptNest promptNest) + { + _promptNest = promptNest; + _powerShellContext = powerShellContext; + _runspace = powerShellContext.CurrentRunspace.Runspace; + } + + internal static InvocationEventQueue Create(PowerShellContext powerShellContext, PromptNest promptNest) + { + var eventQueue = new InvocationEventQueue(powerShellContext, promptNest); + eventQueue.CreateInvocationSubscriber(); + return eventQueue; + } + + /// + /// Executes a command on the main pipeline thread through + /// eventing. A event subscriber will + /// be created that creates a nested PowerShell instance for + /// to utilize. + /// + /// + /// Avoid using this method directly if possible. + /// will route commands + /// through this method if required. + /// + /// The expected result type. + /// The to be executed. + /// + /// Error messages from PowerShell will be written to the . + /// + /// Specifies options to be used when executing this command. + /// + /// An awaitable which will provide results once the command + /// execution completes. + /// + internal async Task> ExecuteCommandOnIdle( + PSCommand psCommand, + StringBuilder errorMessages, + ExecutionOptions executionOptions) + { + var request = new PipelineExecutionRequest( + _powerShellContext, + psCommand, + errorMessages, + executionOptions); + + await SetInvocationRequestAsync( + new InvocationRequest( + pwsh => request.Execute().GetAwaiter().GetResult())); + + try + { + return await request.Results; + } + finally + { + await SetInvocationRequestAsync(request: null); + } + } + + /// + /// Marshals a to run on the pipeline thread. A new + /// will be created for the invocation. + /// + /// + /// The to invoke on the pipeline thread. The nested + /// instance for the created + /// will be passed as an argument. + /// + /// + /// An awaitable that the caller can use to know when execution completes. + /// + internal async Task InvokeOnPipelineThread(Action invocationAction) + { + var request = new InvocationRequest(pwsh => + { + using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: false)) + { + pwsh.Runspace = _runspace; + invocationAction(pwsh); + } + }); + + await SetInvocationRequestAsync(request); + try + { + await request.Task; + } + finally + { + await SetInvocationRequestAsync(null); + } + } + + private async Task WaitForExistingRequestAsync() + { + InvocationRequest existingRequest; + await _lock.WaitAsync(); + try + { + existingRequest = _invocationRequest; + if (existingRequest == null || existingRequest.Task.IsCompleted) + { + return; + } + } + finally + { + _lock.Release(); + } + + await existingRequest.Task; + } + + private async Task SetInvocationRequestAsync(InvocationRequest request) + { + await WaitForExistingRequestAsync(); + await _lock.WaitAsync(); + try + { + _invocationRequest = request; + } + finally + { + _lock.Release(); + } + + _powerShellContext.ForcePSEventHandling(); + } + + private void OnPowerShellIdle(object sender, EventArgs e) + { + if (!_lock.Wait(0)) + { + return; + } + + InvocationRequest currentRequest = null; + try + { + if (_invocationRequest == null) + { + return; + } + + currentRequest = _invocationRequest; + } + finally + { + _lock.Release(); + } + + _promptNest.PushPromptContext(); + try + { + currentRequest.Invoke(_promptNest.GetPowerShell()); + } + finally + { + _promptNest.PopPromptContext(); + } + } + + private PSEventSubscriber CreateInvocationSubscriber() + { + PSEventSubscriber subscriber = _runspace.Events.SubscribeEvent( + source: null, + eventName: PSEngineEvent.OnIdle, + sourceIdentifier: PSEngineEvent.OnIdle, + data: null, + handlerDelegate: OnPowerShellIdle, + supportEvent: true, + forwardEvent: false); + + SetSubscriberExecutionThreadWithReflection(subscriber); + + subscriber.Unsubscribed += OnInvokerUnsubscribed; + + return subscriber; + } + + private void OnInvokerUnsubscribed(object sender, PSEventUnsubscribedEventArgs e) + { + CreateInvocationSubscriber(); + } + + private void SetSubscriberExecutionThreadWithReflection(PSEventSubscriber subscriber) + { + // We need to create the PowerShell object in the same thread so we can get a nested + // PowerShell. This is the only way to consistently take control of the pipeline. The + // alternative is to make the subscriber a script block and have that create and process + // the PowerShell object, but that puts us in a different SessionState and is a lot slower. + s_shouldProcessInExecutionThreadProperty.SetValue(subscriber, true); + } + + private class InvocationRequest : TaskCompletionSource + { + private readonly Action _invocationAction; + + internal InvocationRequest(Action invocationAction) + { + _invocationAction = invocationAction; + } + + internal void Invoke(PowerShell pwsh) + { + try + { + _invocationAction(pwsh); + + // Ensure the result is set in another thread otherwise the caller + // may take over the pipeline thread. + System.Threading.Tasks.Task.Run(() => SetResult(true)); + } + catch (Exception e) + { + System.Threading.Tasks.Task.Run(() => SetException(e)); + } + } + } + } +} diff --git a/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs b/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs new file mode 100644 index 000000000..ad68d0512 --- /dev/null +++ b/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Console; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal class LegacyReadLineContext : IPromptContext + { + private readonly ConsoleReadLine _legacyReadLine; + + internal LegacyReadLineContext(PowerShellContext powerShellContext) + { + _legacyReadLine = new ConsoleReadLine(powerShellContext); + } + + public Task AbortReadLineAsync() + { + return Task.FromResult(true); + } + + public async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await _legacyReadLine.InvokeLegacyReadLineAsync(isCommandLine, cancellationToken); + } + + public Task WaitForReadLineExitAsync() + { + return Task.FromResult(true); + } + + public void AddToHistory(string command) + { + // Do nothing, history is managed completely by the PowerShell engine in legacy ReadLine. + } + + public void AbortReadLine() + { + // Do nothing, no additional actions are needed to cancel ReadLine. + } + + public void WaitForReadLineExit() + { + // Do nothing, ReadLine cancellation is instant or not appliciable. + } + + public void ForcePSEventHandling() + { + // Do nothing, the pipeline thread is not occupied by legacy ReadLine. + } + } +} diff --git a/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs new file mode 100644 index 000000000..53aa59121 --- /dev/null +++ b/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs @@ -0,0 +1,201 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System; +using System.Management.Automation.Runspaces; +using Microsoft.PowerShell.EditorServices.Console; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session { + using System.Management.Automation; + + internal class PSReadLinePromptContext : IPromptContext { + private const string ReadLineScript = @" + [System.Diagnostics.DebuggerHidden()] + [System.Diagnostics.DebuggerStepThrough()] + param() + return [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine( + $Host.Runspace, + $ExecutionContext, + $args[0])"; + + private const string ReadLineInitScript = @" + [System.Diagnostics.DebuggerHidden()] + [System.Diagnostics.DebuggerStepThrough()] + param() + end { + $module = Get-Module -ListAvailable PSReadLine | Where-Object Version -ge '2.0.0' | Sort-Object -Descending Version | Select-Object -First 1 + if (-not $module) { + return + } + + Import-Module -ModuleInfo $module + return 'Microsoft.PowerShell.PSConsoleReadLine' -as [type] + }"; + + private readonly PowerShellContext _powerShellContext; + + private PromptNest _promptNest; + + private InvocationEventQueue _invocationEventQueue; + + private ConsoleReadLine _consoleReadLine; + + private CancellationTokenSource _readLineCancellationSource; + + private PSReadLineProxy _readLineProxy; + + internal PSReadLinePromptContext( + PowerShellContext powerShellContext, + PromptNest promptNest, + InvocationEventQueue invocationEventQueue, + PSReadLineProxy readLineProxy) + { + _promptNest = promptNest; + _powerShellContext = powerShellContext; + _invocationEventQueue = invocationEventQueue; + _consoleReadLine = new ConsoleReadLine(powerShellContext); + _readLineProxy = readLineProxy; + +#if CoreCLR + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + _readLineProxy.OverrideReadKey( + intercept => ConsoleProxy.UnixReadKey( + intercept, + _readLineCancellationSource.Token)); +#endif + } + + internal static bool TryGetPSReadLineProxy( + ILogger logger, + Runspace runspace, + out PSReadLineProxy readLineProxy) + { + readLineProxy = null; + using (var pwsh = PowerShell.Create()) + { + pwsh.Runspace = runspace; + var psReadLineType = pwsh + .AddScript(ReadLineInitScript) + .Invoke() + .FirstOrDefault(); + + if (psReadLineType == null) + { + return false; + } + + try + { + readLineProxy = new PSReadLineProxy(psReadLineType, logger); + } + catch (InvalidOperationException) + { + // The Type we got back from PowerShell doesn't have the members we expected. + // Could be an older version, a custom build, or something a newer version with + // breaking changes. + return false; + } + } + + return true; + } + + public async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + _readLineCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var localTokenSource = _readLineCancellationSource; + if (localTokenSource.Token.IsCancellationRequested) + { + throw new TaskCanceledException(); + } + + try + { + if (!isCommandLine) + { + return await _consoleReadLine.InvokeLegacyReadLineAsync( + false, + _readLineCancellationSource.Token); + } + + var result = (await _powerShellContext.ExecuteCommand( + new PSCommand() + .AddScript(ReadLineScript) + .AddArgument(_readLineCancellationSource.Token), + null, + new ExecutionOptions() + { + WriteErrorsToHost = false, + WriteOutputToHost = false, + InterruptCommandPrompt = false, + AddToHistory = false, + IsReadLine = isCommandLine + })) + .FirstOrDefault(); + + return cancellationToken.IsCancellationRequested + ? string.Empty + : result; + } + finally + { + _readLineCancellationSource = null; + } + } + + public void AbortReadLine() + { + if (_readLineCancellationSource == null) + { + return; + } + + _readLineCancellationSource.Cancel(); + + WaitForReadLineExit(); + } + + public async Task AbortReadLineAsync() { + if (_readLineCancellationSource == null) + { + return; + } + + _readLineCancellationSource.Cancel(); + + await WaitForReadLineExitAsync(); + } + + public void WaitForReadLineExit() + { + using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: true)) + { } + } + + public async Task WaitForReadLineExitAsync () { + using (await _promptNest.GetRunspaceHandleAsync(CancellationToken.None, isReadLine: true)) + { } + } + + public void AddToHistory(string command) + { + _readLineProxy.AddToHistory(command); + } + + public void ForcePSEventHandling() + { + _readLineProxy.ForcePSEventHandling(); + } + } +} diff --git a/src/PowerShellEditorServices/Session/PSReadLineProxy.cs b/src/PowerShellEditorServices/Session/PSReadLineProxy.cs new file mode 100644 index 000000000..50aaf4af3 --- /dev/null +++ b/src/PowerShellEditorServices/Session/PSReadLineProxy.cs @@ -0,0 +1,119 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Reflection; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal class PSReadLineProxy + { + private const string FieldMemberType = "field"; + + private const string MethodMemberType = "method"; + + private const string AddToHistoryMethodName = "AddToHistory"; + + private const string SetKeyHandlerMethodName = "SetKeyHandler"; + + private const string ReadKeyOverrideFieldName = "_readKeyOverride"; + + private const string VirtualTerminalTypeName = "Microsoft.PowerShell.Internal.VirtualTerminal"; + + private const string ForcePSEventHandlingMethodName = "ForcePSEventHandling"; + + private static readonly Type[] s_setKeyHandlerTypes = + { + typeof(string[]), + typeof(Action), + typeof(string), + typeof(string) + }; + + private static readonly Type[] s_addToHistoryTypes = { typeof(string) }; + + private readonly FieldInfo _readKeyOverrideField; + + internal PSReadLineProxy(Type psConsoleReadLine, ILogger logger) + { + ForcePSEventHandling = + (Action)psConsoleReadLine.GetMethod( + ForcePSEventHandlingMethodName, + BindingFlags.Static | BindingFlags.NonPublic) + ?.CreateDelegate(typeof(Action)); + + AddToHistory = (Action)psConsoleReadLine.GetMethod( + AddToHistoryMethodName, + s_addToHistoryTypes) + ?.CreateDelegate(typeof(Action)); + + SetKeyHandler = + (Action, string, string>)psConsoleReadLine.GetMethod( + SetKeyHandlerMethodName, + s_setKeyHandlerTypes) + ?.CreateDelegate(typeof(Action, string, string>)); + + _readKeyOverrideField = psConsoleReadLine.GetTypeInfo().Assembly + .GetType(VirtualTerminalTypeName) + ?.GetField(ReadKeyOverrideFieldName, BindingFlags.Static | BindingFlags.NonPublic); + + if (_readKeyOverrideField == null) + { + throw NewInvalidPSReadLineVersionException( + FieldMemberType, + ReadKeyOverrideFieldName, + logger); + } + + if (SetKeyHandler == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + SetKeyHandlerMethodName, + logger); + } + + if (AddToHistory == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + AddToHistoryMethodName, + logger); + } + + if (ForcePSEventHandling == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + ForcePSEventHandlingMethodName, + logger); + } + } + + internal Action AddToHistory { get; } + + internal Action, object>, string, string> SetKeyHandler { get; } + + internal Action ForcePSEventHandling { get; } + + internal void OverrideReadKey(Func readKeyFunc) + { + _readKeyOverrideField.SetValue(null, readKeyFunc); + } + + private static InvalidOperationException NewInvalidPSReadLineVersionException( + string memberType, + string memberName, + ILogger logger) + { + logger.Write( + LogLevel.Error, + $"The loaded version of PSReadLine is not supported. The {memberType} \"{memberName}\" was not found."); + + return new InvalidOperationException(); + } + } +} diff --git a/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs b/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs new file mode 100644 index 000000000..cb1d66073 --- /dev/null +++ b/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal interface IPipelineExecutionRequest + { + Task Execute(); + + Task WaitTask { get; } + } + + /// + /// Contains details relating to a request to execute a + /// command on the PowerShell pipeline thread. + /// + /// The expected result type of the execution. + internal class PipelineExecutionRequest : IPipelineExecutionRequest + { + private PowerShellContext _powerShellContext; + private PSCommand _psCommand; + private StringBuilder _errorMessages; + private ExecutionOptions _executionOptions; + private TaskCompletionSource> _resultsTask; + + public Task> Results + { + get { return this._resultsTask.Task; } + } + + public Task WaitTask { get { return Results; } } + + public PipelineExecutionRequest( + PowerShellContext powerShellContext, + PSCommand psCommand, + StringBuilder errorMessages, + bool sendOutputToHost) + : this( + powerShellContext, + psCommand, + errorMessages, + new ExecutionOptions() + { + WriteOutputToHost = sendOutputToHost + }) + { } + + + public PipelineExecutionRequest( + PowerShellContext powerShellContext, + PSCommand psCommand, + StringBuilder errorMessages, + ExecutionOptions executionOptions) + { + _powerShellContext = powerShellContext; + _psCommand = psCommand; + _errorMessages = errorMessages; + _executionOptions = executionOptions; + _resultsTask = new TaskCompletionSource>(); + } + + public async Task Execute() + { + var results = + await _powerShellContext.ExecuteCommand( + _psCommand, + _errorMessages, + _executionOptions); + + var unusedTask = Task.Run(() => _resultsTask.SetResult(results)); + } + } +} diff --git a/src/PowerShellEditorServices/Session/PowerShell3Operations.cs b/src/PowerShellEditorServices/Session/PowerShell3Operations.cs index 2199e1839..eb6cf0252 100644 --- a/src/PowerShellEditorServices/Session/PowerShell3Operations.cs +++ b/src/PowerShellEditorServices/Session/PowerShell3Operations.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation; +using System.Management.Automation.Host; using System.Management.Automation.Runspaces; namespace Microsoft.PowerShell.EditorServices.Session @@ -69,6 +70,27 @@ public IEnumerable ExecuteCommandInDebugger( return executionResult; } + + public void StopCommandInDebugger(PowerShellContext powerShellContext) + { + } + + public bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace) + { + return promptNest.IsInDebugger; + } + + public void ExitNestedPrompt(PSHost host) + { + try + { + host.ExitNestedPrompt(); + } + // FlowControlException is not accessible in PSv3 + catch (Exception) + { + } + } } } diff --git a/src/PowerShellEditorServices/Session/PowerShell4Operations.cs b/src/PowerShellEditorServices/Session/PowerShell4Operations.cs index ea4070225..d9060ed2f 100644 --- a/src/PowerShellEditorServices/Session/PowerShell4Operations.cs +++ b/src/PowerShellEditorServices/Session/PowerShell4Operations.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation; +using System.Management.Automation.Host; using System.Management.Automation.Runspaces; namespace Microsoft.PowerShell.EditorServices.Session @@ -79,6 +80,33 @@ public IEnumerable ExecuteCommandInDebugger( return results; } + + public void StopCommandInDebugger(PowerShellContext powerShellContext) + { +#if !PowerShellv3 + powerShellContext.CurrentRunspace.Runspace.Debugger.StopProcessCommand(); +#endif + } + + public virtual bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace) + { + return promptNest.IsInDebugger; + } + + public void ExitNestedPrompt(PSHost host) + { +#if !PowerShellv3 + try + { + host.ExitNestedPrompt(); + } + catch (FlowControlException) + { + } +#else + throw new NotSupportedException(); +#endif + } } } diff --git a/src/PowerShellEditorServices/Session/PowerShell5Operations.cs b/src/PowerShellEditorServices/Session/PowerShell5Operations.cs index 54f434cb8..e27c3b14e 100644 --- a/src/PowerShellEditorServices/Session/PowerShell5Operations.cs +++ b/src/PowerShellEditorServices/Session/PowerShell5Operations.cs @@ -16,6 +16,16 @@ public override void PauseDebugger(Runspace runspace) { runspace.Debugger.SetDebuggerStepMode(true); } +#endif + } + + public override bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace) + { +#if !PowerShellv3 && !PowerShellv4 + return runspace.Debugger.InBreakpoint || + (promptNest.IsRemote && promptNest.IsInDebugger); +#else + throw new System.NotSupportedException(); #endif } } diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index f8d6e69a3..3d7c557f2 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -3,26 +3,26 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Console; -using Microsoft.PowerShell.EditorServices.Utility; using System; -using System.Globalization; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; using System.Linq; +using System.Management.Automation.Host; +using System.Management.Automation.Remoting; +using System.Management.Automation.Runspaces; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Session.Capabilities; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices { - using Session; using System.Management.Automation; - using System.Management.Automation.Host; - using System.Management.Automation.Runspaces; - using Microsoft.PowerShell.EditorServices.Session.Capabilities; - using System.IO; /// /// Manages the lifetime and usage of a PowerShell session. @@ -33,6 +33,9 @@ public class PowerShellContext : IDisposable, IHostSupportsInteractiveSession { #region Fields + private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + private bool isPSReadLineEnabled; private ILogger logger; private PowerShell powerShell; private bool ownsInitialRunspace; @@ -43,32 +46,32 @@ public class PowerShellContext : IDisposable, IHostSupportsInteractiveSession private IVersionSpecificOperations versionSpecificOperations; - private int pipelineThreadId; - private TaskCompletionSource debuggerStoppedTask; - private TaskCompletionSource pipelineExecutionTask; - - private object runspaceMutex = new object(); - private AsyncQueue runspaceWaitQueue = new AsyncQueue(); - private Stack runspaceStack = new Stack(); + private int isCommandLoopRestarterSet; + #endregion #region Properties + private IPromptContext PromptContext { get; set; } + + private PromptNest PromptNest { get; set; } + + private InvocationEventQueue InvocationEventQueue { get; set; } + + private EngineIntrinsics EngineIntrinsics { get; set; } + + private PSHost ExternalHost { get; set; } + /// /// Gets a boolean that indicates whether the debugger is currently stopped, /// either at a breakpoint or because the user broke execution. /// - public bool IsDebuggerStopped - { - get - { - return - this.debuggerStoppedTask != null && - this.CurrentRunspace.Runspace.RunspaceAvailability != RunspaceAvailability.Available; - } - } + public bool IsDebuggerStopped => + this.versionSpecificOperations.IsDebuggerStopped( + PromptNest, + CurrentRunspace.Runspace); /// /// Gets the current state of the session. @@ -94,6 +97,8 @@ public PowerShellVersionDetails LocalPowerShellVersion /// private IHostOutput ConsoleWriter { get; set; } + private IHostInput ConsoleReader { get; set; } + /// /// Gets details pertaining to the current runspace. /// @@ -103,6 +108,12 @@ public RunspaceDetails CurrentRunspace private set; } + /// + /// Gets a value indicating whether the current runspace + /// is ready for a command + /// + public bool IsAvailable => this.SessionState == PowerShellContextState.Ready; + /// /// Gets the working directory path the PowerShell context was inititially set when the debugger launches. /// This path is used to determine whether a script in the call stack is an "external" script. @@ -117,9 +128,13 @@ public RunspaceDetails CurrentRunspace /// /// /// An ILogger implementation used for writing log messages. - public PowerShellContext(ILogger logger) + /// + /// Indicates whether PSReadLine should be used if possible + /// + public PowerShellContext(ILogger logger, bool isPSReadLineEnabled) { this.logger = logger; + this.isPSReadLineEnabled = isPSReadLineEnabled; } /// @@ -140,6 +155,7 @@ public static Runspace CreateRunspace( { var psHost = new EditorServicesPSHost(powerShellContext, hostDetails, hostUserInterface, logger); powerShellContext.ConsoleWriter = hostUserInterface; + powerShellContext.ConsoleReader = hostUserInterface; return CreateRunspace(psHost); } @@ -196,6 +212,7 @@ public void Initialize( this.ownsInitialRunspace = ownsInitialRunspace; this.SessionState = PowerShellContextState.NotStarted; this.ConsoleWriter = consoleHost; + this.ConsoleReader = consoleHost as IHostInput; // Get the PowerShell runtime version this.LocalPowerShellVersion = @@ -264,13 +281,48 @@ public void Initialize( } // Now that initialization is complete we can watch for InvocationStateChanged - this.powerShell.InvocationStateChanged += powerShell_InvocationStateChanged; - this.SessionState = PowerShellContextState.Ready; + // EngineIntrinsics is used in some instances to interact with the initial + // runspace without having to wait for PSReadLine to check for events. + this.EngineIntrinsics = + initialRunspace + .SessionStateProxy + .PSVariable + .GetValue("ExecutionContext") + as EngineIntrinsics; + + // The external host is used to properly exit from a nested prompt that + // was entered by the user. + this.ExternalHost = + initialRunspace + .SessionStateProxy + .PSVariable + .GetValue("Host") + as PSHost; + // Now that the runspace is ready, enqueue it for first use - RunspaceHandle runspaceHandle = new RunspaceHandle(this); - this.runspaceWaitQueue.EnqueueAsync(runspaceHandle).Wait(); + this.PromptNest = new PromptNest( + this, + this.powerShell, + this.ConsoleReader, + this.versionSpecificOperations); + this.InvocationEventQueue = InvocationEventQueue.Create(this, this.PromptNest); + + if (powerShellVersion.Major >= 5 && + this.isPSReadLineEnabled && + PSReadLinePromptContext.TryGetPSReadLineProxy(logger, initialRunspace, out PSReadLineProxy proxy)) + { + this.PromptContext = new PSReadLinePromptContext( + this, + this.PromptNest, + this.InvocationEventQueue, + proxy); + } + else + { + this.PromptContext = new LegacyReadLineContext(this); + } } /// @@ -339,7 +391,7 @@ private void CleanupRunspace(RunspaceDetails runspaceDetails) /// A RunspaceHandle instance that gives access to the session's runspace. public Task GetRunspaceHandle() { - return this.GetRunspaceHandle(CancellationToken.None); + return this.GetRunspaceHandleImpl(CancellationToken.None, isReadLine: false); } /// @@ -351,7 +403,7 @@ public Task GetRunspaceHandle() /// A RunspaceHandle instance that gives access to the session's runspace. public Task GetRunspaceHandle(CancellationToken cancellationToken) { - return this.runspaceWaitQueue.DequeueAsync(cancellationToken); + return this.GetRunspaceHandleImpl(cancellationToken, isReadLine: false); } /// @@ -434,28 +486,56 @@ public async Task> ExecuteCommand( StringBuilder errorMessages, ExecutionOptions executionOptions) { + // Add history to PSReadLine before cancelling, otherwise it will be restored as the + // cancelled prompt when it's called again. + if (executionOptions.AddToHistory) + { + this.PromptContext.AddToHistory(psCommand.Commands[0].CommandText); + } + bool hadErrors = false; RunspaceHandle runspaceHandle = null; + ExecutionTarget executionTarget = ExecutionTarget.PowerShell; IEnumerable executionResult = Enumerable.Empty(); + var shouldCancelReadLine = + executionOptions.InterruptCommandPrompt || + executionOptions.WriteOutputToHost; // If the debugger is active and the caller isn't on the pipeline // thread, send the command over to that thread to be executed. - if (Thread.CurrentThread.ManagedThreadId != this.pipelineThreadId && - this.pipelineExecutionTask != null) + // Determine if execution should take place in a different thread + // using the following criteria: + // 1. The current frame in the prompt nest has a thread controller + // (meaning it is a nested prompt or is in the debugger) + // 2. We aren't already on the thread in question + // 3. The command is not a candidate for background invocation + // via PowerShell eventing + // 4. The command cannot be for a PSReadLine pipeline while we + // are currently in a out of process runspace + var threadController = PromptNest.GetThreadController(); + if (!(threadController == null || + !threadController.IsPipelineThread || + threadController.IsCurrentThread() || + this.ShouldExecuteWithEventing(executionOptions) || + (PromptNest.IsRemote && executionOptions.IsReadLine))) { this.logger.Write(LogLevel.Verbose, "Passing command execution to pipeline thread."); - PipelineExecutionRequest executionRequest = + if (shouldCancelReadLine && PromptNest.IsReadLineBusy()) + { + // If a ReadLine pipeline is running in the debugger then we'll hang here + // if we don't cancel it. Typically we can rely on OnExecutionStatusChanged but + // the pipeline request won't even start without clearing the current task. + this.ConsoleReader?.StopCommandLoop(); + } + + // Send the pipeline execution request to the pipeline thread + return await threadController.RequestPipelineExecution( new PipelineExecutionRequest( this, psCommand, errorMessages, - executionOptions.WriteOutputToHost); - - // Send the pipeline execution request to the pipeline thread - this.pipelineExecutionTask.SetResult(executionRequest); - - return await executionRequest.Results; + executionOptions)); } else { @@ -473,73 +553,127 @@ public async Task> ExecuteCommand( endOfStatement: false)); } - this.OnExecutionStatusChanged( - ExecutionStatus.Running, - executionOptions, - false); + executionTarget = GetExecutionTarget(executionOptions); + + // If a ReadLine pipeline is running we can still execute commands that + // don't write output (e.g. command completion) + if (executionTarget == ExecutionTarget.InvocationEvent) + { + return (await this.InvocationEventQueue.ExecuteCommandOnIdle( + psCommand, + errorMessages, + executionOptions)); + } + + // Prompt is stopped and started based on the execution status, so naturally + // we don't want PSReadLine pipelines to factor in. + if (!executionOptions.IsReadLine) + { + this.OnExecutionStatusChanged( + ExecutionStatus.Running, + executionOptions, + false); + } - if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.AvailableForNestedCommand || - this.debuggerStoppedTask != null) + runspaceHandle = await this.GetRunspaceHandle(executionOptions.IsReadLine); + if (executionOptions.WriteInputToHost) { - executionResult = - this.ExecuteCommandInDebugger( + this.WriteOutput(psCommand.Commands[0].CommandText, true); + } + + if (executionTarget == ExecutionTarget.Debugger) + { + // Manually change the session state for debugger commands because + // we don't have an invocation state event to attach to. + if (!executionOptions.IsReadLine) + { + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Running, + PowerShellExecutionResult.NotFinished, + null)); + } + try + { + return this.ExecuteCommandInDebugger( psCommand, executionOptions.WriteOutputToHost); + } + catch (Exception e) + { + logger.Write( + LogLevel.Error, + "Exception occurred while executing debugger command:\r\n\r\n" + e.ToString()); + } + finally + { + if (!executionOptions.IsReadLine) + { + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Ready, + PowerShellExecutionResult.Stopped, + null)); + } + } } - else + + var invocationSettings = new PSInvocationSettings() + { + AddToHistory = executionOptions.AddToHistory + }; + + this.logger.Write( + LogLevel.Verbose, + string.Format( + "Attempting to execute command(s):\r\n\r\n{0}", + GetStringForPSCommand(psCommand))); + + + PowerShell shell = this.PromptNest.GetPowerShell(executionOptions.IsReadLine); + shell.Commands = psCommand; + + // Don't change our SessionState for ReadLine. + if (!executionOptions.IsReadLine) + { + shell.InvocationStateChanged += powerShell_InvocationStateChanged; + } + + shell.Runspace = executionOptions.ShouldExecuteInOriginalRunspace + ? this.initialRunspace.Runspace + : this.CurrentRunspace.Runspace; + try { - this.logger.Write( - LogLevel.Verbose, - string.Format( - "Attempting to execute command(s):\r\n\r\n{0}", - GetStringForPSCommand(psCommand))); - - // Set the runspace - runspaceHandle = await this.GetRunspaceHandle(); - if (runspaceHandle.Runspace.RunspaceAvailability != RunspaceAvailability.AvailableForNestedCommand) + // Nested PowerShell instances can't be invoked asynchronously. This occurs + // in nested prompts and pipeline requests from eventing. + if (shell.IsNested) { - this.powerShell.Runspace = runspaceHandle.Runspace; + return shell.Invoke(null, invocationSettings); } - // Invoke the pipeline on a background thread - // TODO: Use built-in async invocation! - executionResult = - await Task.Factory.StartNew>( - () => - { - Collection result = null; - try - { - this.powerShell.Commands = psCommand; - - PSInvocationSettings invocationSettings = new PSInvocationSettings(); - invocationSettings.AddToHistory = executionOptions.AddToHistory; - result = this.powerShell.Invoke(null, invocationSettings); - } - catch (RemoteException e) - { - if (!e.SerializedRemoteException.TypeNames[0].EndsWith("PipelineStoppedException")) - { - // Rethrow anything that isn't a PipelineStoppedException - throw e; - } - } - - return result; - }, - CancellationToken.None, // Might need a cancellation token - TaskCreationOptions.None, - TaskScheduler.Default - ); - - if (this.powerShell.HadErrors) + return await Task.Factory.StartNew>( + () => shell.Invoke(null, invocationSettings), + CancellationToken.None, // Might need a cancellation token + TaskCreationOptions.None, + TaskScheduler.Default); + } + finally + { + if (!executionOptions.IsReadLine) + { + shell.InvocationStateChanged -= powerShell_InvocationStateChanged; + } + + if (shell.HadErrors) { var strBld = new StringBuilder(1024); strBld.AppendFormat("Execution of the following command(s) completed with errors:\r\n\r\n{0}\r\n", GetStringForPSCommand(psCommand)); int i = 1; - foreach (var error in this.powerShell.Streams.Error) + foreach (var error in shell.Streams.Error) { if (i > 1) strBld.Append("\r\n\r\n"); strBld.Append($"Error #{i++}:\r\n"); @@ -556,7 +690,7 @@ await Task.Factory.StartNew>( } // We've reported these errors, clear them so they don't keep showing up. - this.powerShell.Streams.Error.Clear(); + shell.Streams.Error.Clear(); var errorMessage = strBld.ToString(); @@ -573,6 +707,14 @@ await Task.Factory.StartNew>( } } } + catch (PSRemotingDataStructureException e) + { + this.logger.Write( + LogLevel.Error, + "Pipeline stopped while executing command:\r\n\r\n" + e.ToString()); + + errorMessages?.Append(e.Message); + } catch (PipelineStoppedException e) { this.logger.Write( @@ -613,7 +755,11 @@ await Task.Factory.StartNew>( SessionDetails sessionDetails = null; // Get the SessionDetails and then write the prompt - if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.Available) + if (executionTarget == ExecutionTarget.Debugger) + { + sessionDetails = this.GetSessionDetailsInDebugger(); + } + else if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.Available) { // This state can happen if the user types a command that causes the // debugger to exit before we reach this point. No RunspaceHandle @@ -625,10 +771,6 @@ await Task.Factory.StartNew>( sessionDetails = this.GetSessionDetailsInRunspace(runspaceHandle.Runspace); } - else if (this.IsDebuggerStopped) - { - sessionDetails = this.GetSessionDetailsInDebugger(); - } else { sessionDetails = this.GetSessionDetailsInNestedPipeline(); @@ -643,14 +785,14 @@ await Task.Factory.StartNew>( { runspaceHandle.Dispose(); } + + this.OnExecutionStatusChanged( + ExecutionStatus.Completed, + executionOptions, + hadErrors); } } - this.OnExecutionStatusChanged( - ExecutionStatus.Completed, - executionOptions, - hadErrors); - return executionResult; } @@ -740,23 +882,15 @@ public async Task> ExecuteScriptString( bool writeOutputToHost, bool addToHistory) { - // Get rid of leading and trailing whitespace and newlines - scriptString = scriptString.Trim(); - - if (writeInputToHost) - { - this.WriteOutput(scriptString, false); - } - - PSCommand psCommand = new PSCommand(); - psCommand.AddScript(scriptString); - - return - await this.ExecuteCommand( - psCommand, - errorMessages, - writeOutputToHost, - addToHistory: addToHistory); + return await this.ExecuteCommand( + new PSCommand().AddScript(scriptString.Trim()), + errorMessages, + new ExecutionOptions() + { + WriteOutputToHost = writeOutputToHost, + AddToHistory = addToHistory, + WriteInputToHost = writeInputToHost + }); } /// @@ -778,8 +912,15 @@ public async Task ExecuteScriptWithArgs(string script, string arguments = null, try { // Assume we can only debug scripts from the FileSystem provider - string workingDir = - this.CurrentRunspace.Runspace.SessionStateProxy.Path.CurrentFileSystemLocation.ProviderPath; + string workingDir = (await ExecuteCommand( + new PSCommand() + .AddCommand("Microsoft.PowerShell.Management\\Get-Location") + .AddParameter("PSProvider", "FileSystem"), + false, + false)) + .FirstOrDefault() + .ProviderPath; + workingDir = workingDir.TrimEnd(Path.DirectorySeparatorChar); scriptAbsPath = workingDir + Path.DirectorySeparatorChar + script; } @@ -821,6 +962,57 @@ await this.ExecuteCommand( addToHistory: true); } + /// + /// Forces the to trigger PowerShell event handling, + /// reliquishing control of the pipeline thread during event processing. + /// + /// + /// This method is called automatically by and + /// . Consider using them instead of this method directly when + /// possible. + /// + internal void ForcePSEventHandling() + { + PromptContext.ForcePSEventHandling(); + } + + /// + /// Marshals a to run on the pipeline thread. A new + /// will be created for the invocation. + /// + /// + /// The to invoke on the pipeline thread. The nested + /// instance for the created + /// will be passed as an argument. + /// + /// + /// An awaitable that the caller can use to know when execution completes. + /// + /// + /// This method is called automatically by . Consider using + /// that method instead of calling this directly when possible. + /// + internal async Task InvokeOnPipelineThread(Action invocationAction) + { + if (this.PromptNest.IsReadLineBusy()) + { + await this.InvocationEventQueue.InvokeOnPipelineThread(invocationAction); + return; + } + + // If this is invoked when ReadLine isn't busy then there shouldn't be any running + // pipelines. Right now this method is only used by command completion which doesn't + // actually require running on the pipeline thread, as long as nothing else is running. + invocationAction.Invoke(this.PromptNest.GetPowerShell()); + } + + internal async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await PromptContext.InvokeReadLineAsync( + isCommandLine, + cancellationToken); + } + internal static TResult ExecuteScriptAndGetItem(string scriptToExecute, Runspace runspace, TResult defaultValue = default(TResult)) { Pipeline pipeline = null; @@ -890,26 +1082,50 @@ public async Task LoadHostProfiles() } /// - /// Causes the current execution to be aborted no matter what state + /// Causes the most recent execution to be aborted no matter what state /// it is currently in. /// public void AbortExecution() + { + this.AbortExecution(shouldAbortDebugSession: false); + } + + /// + /// Causes the most recent execution to be aborted no matter what state + /// it is currently in. + /// + /// + /// A value indicating whether a debug session should be aborted if one + /// is currently active. + /// + public void AbortExecution(bool shouldAbortDebugSession) { if (this.SessionState != PowerShellContextState.Aborting && this.SessionState != PowerShellContextState.Disposed) { this.logger.Write(LogLevel.Verbose, "Execution abort requested..."); - // Clean up the debugger - if (this.IsDebuggerStopped) + if (shouldAbortDebugSession) { - this.ResumeDebugger(DebuggerResumeAction.Stop); - this.debuggerStoppedTask = null; - this.pipelineExecutionTask = null; + this.ExitAllNestedPrompts(); } - // Stop the running pipeline - this.powerShell.BeginStop(null, null); + if (this.PromptNest.IsInDebugger) + { + if (shouldAbortDebugSession) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + this.ResumeDebugger(DebuggerResumeAction.Stop); + } + else + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + } + else + { + this.PromptNest.GetPowerShell(isReadLine: false).BeginStop(null, null); + } this.SessionState = PowerShellContextState.Aborting; @@ -927,6 +1143,33 @@ public void AbortExecution() } } + /// + /// Exit all consecutive nested prompts that the user has entered. + /// + internal void ExitAllNestedPrompts() + { + while (this.PromptNest.IsNestedPrompt) + { + this.PromptNest.WaitForCurrentFrameExit(frame => this.ExitNestedPrompt()); + this.versionSpecificOperations.ExitNestedPrompt(ExternalHost); + } + } + + /// + /// Exit all consecutive nested prompts that the user has entered. + /// + /// + /// A task object that represents all nested prompts being exited + /// + internal async Task ExitAllNestedPromptsAsync() + { + while (this.PromptNest.IsNestedPrompt) + { + await this.PromptNest.WaitForCurrentFrameExitAsync(frame => this.ExitNestedPrompt()); + this.versionSpecificOperations.ExitNestedPrompt(ExternalHost); + } + } + /// /// Causes the debugger to break execution wherever it currently is. /// This method is internal because the real Break API is provided @@ -943,22 +1186,56 @@ internal void BreakExecution() internal void ResumeDebugger(DebuggerResumeAction resumeAction) { - if (this.debuggerStoppedTask != null) + ResumeDebugger(resumeAction, shouldWaitForExit: true); + } + + private void ResumeDebugger(DebuggerResumeAction resumeAction, bool shouldWaitForExit) + { + resumeRequestHandle.Wait(); + try { - // Set the result so that the execution thread resumes. - // The execution thread will clean up the task. - if (!this.debuggerStoppedTask.TrySetResult(resumeAction)) + if (this.PromptNest.IsNestedPrompt) + { + this.ExitAllNestedPrompts(); + } + + if (this.PromptNest.IsInDebugger) + { + // Set the result so that the execution thread resumes. + // The execution thread will clean up the task. + if (shouldWaitForExit) + { + this.PromptNest.WaitForCurrentFrameExit( + frame => + { + frame.ThreadController.StartThreadExit(resumeAction); + this.ConsoleReader?.StopCommandLoop(); + if (this.SessionState != PowerShellContextState.Ready) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + }); + } + else + { + this.PromptNest.GetThreadController().StartThreadExit(resumeAction); + this.ConsoleReader?.StopCommandLoop(); + if (this.SessionState != PowerShellContextState.Ready) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + } + } + else { this.logger.Write( LogLevel.Error, - $"Tried to resume debugger with action {resumeAction} but the task was already completed."); + $"Tried to resume debugger with action {resumeAction} but there was no debuggerStoppedTask."); } } - else + finally { - this.logger.Write( - LogLevel.Error, - $"Tried to resume debugger with action {resumeAction} but there was no debuggerStoppedTask."); + resumeRequestHandle.Release(); } } @@ -968,22 +1245,9 @@ internal void ResumeDebugger(DebuggerResumeAction resumeAction) /// public void Dispose() { - // Do we need to abort a running execution? - if (this.SessionState == PowerShellContextState.Running || - this.IsDebuggerStopped) - { - this.AbortExecution(); - } - + this.PromptNest.Dispose(); this.SessionState = PowerShellContextState.Disposed; - if (this.powerShell != null) - { - this.powerShell.InvocationStateChanged -= this.powerShell_InvocationStateChanged; - this.powerShell.Dispose(); - this.powerShell = null; - } - // Clean up the active runspace this.CleanupRunspace(this.CurrentRunspace); @@ -1012,6 +1276,57 @@ public void Dispose() this.initialRunspace = null; } + private async Task GetRunspaceHandle(bool isReadLine) + { + return await this.GetRunspaceHandleImpl(CancellationToken.None, isReadLine); + } + + private async Task GetRunspaceHandleImpl(CancellationToken cancellationToken, bool isReadLine) + { + return await this.PromptNest.GetRunspaceHandleAsync(cancellationToken, isReadLine); + } + + private ExecutionTarget GetExecutionTarget(ExecutionOptions options = null) + { + if (options == null) + { + options = new ExecutionOptions(); + } + + var noBackgroundInvocation = + options.InterruptCommandPrompt || + options.WriteOutputToHost || + options.IsReadLine || + PromptNest.IsRemote; + + // Take over the pipeline if PSReadLine is running, we aren't trying to run PSReadLine, and + // we aren't in a remote session. + if (!noBackgroundInvocation && PromptNest.IsReadLineBusy() && PromptNest.IsMainThreadBusy()) + { + return ExecutionTarget.InvocationEvent; + } + + // We can't take the pipeline from PSReadLine if it's in a remote session, so we need to + // invoke locally in that case. + if (IsDebuggerStopped && PromptNest.IsInDebugger && !(options.IsReadLine && PromptNest.IsRemote)) + { + return ExecutionTarget.Debugger; + } + + return ExecutionTarget.PowerShell; + } + + private bool ShouldExecuteWithEventing(ExecutionOptions executionOptions) + { + return + this.PromptNest.IsReadLineBusy() && + this.PromptNest.IsMainThreadBusy() && + !(executionOptions.IsReadLine || + executionOptions.InterruptCommandPrompt || + executionOptions.WriteOutputToHost || + IsCurrentRunspaceOutOfProcess()); + } + private void CloseRunspace(RunspaceDetails runspaceDetails) { string exitCommand = null; @@ -1076,18 +1391,99 @@ internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) { Validate.IsNotNull("runspaceHandle", runspaceHandle); - if (this.runspaceWaitQueue.IsEmpty) + if (PromptNest.IsMainThreadBusy() || (runspaceHandle.IsReadLine && PromptNest.IsReadLineBusy())) { - var newRunspaceHandle = new RunspaceHandle(this); - this.runspaceWaitQueue.EnqueueAsync(newRunspaceHandle).Wait(); + var unusedTask = PromptNest + .ReleaseRunspaceHandleAsync(runspaceHandle) + .ConfigureAwait(false); } else { // Write the situation to the log since this shouldn't happen this.logger.Write( LogLevel.Error, - "The PowerShellContext.runspaceWaitQueue has more than one item"); + "ReleaseRunspaceHandle was called when the main thread was not busy."); + } + } + + /// + /// Determines if the current runspace is out of process. + /// + /// + /// A value indicating whether the current runspace is out of process. + /// + internal bool IsCurrentRunspaceOutOfProcess() + { + return + CurrentRunspace.Context == RunspaceContext.EnteredProcess || + CurrentRunspace.Context == RunspaceContext.DebuggedRunspace || + CurrentRunspace.Location == RunspaceLocation.Remote; + } + + /// + /// Called by the external PSHost when $Host.EnterNestedPrompt is called. + /// + internal void EnterNestedPrompt() + { + if (this.IsCurrentRunspaceOutOfProcess()) + { + throw new NotSupportedException(); + } + + this.PromptNest.PushPromptContext(PromptNestFrameType.NestedPrompt); + var localThreadController = this.PromptNest.GetThreadController(); + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Ready, + PowerShellExecutionResult.Stopped, + null)); + + // Reset command loop mainly for PSReadLine + this.ConsoleReader?.StopCommandLoop(); + this.ConsoleReader?.StartCommandLoop(); + + var localPipelineExecutionTask = localThreadController.TakeExecutionRequest(); + var localDebuggerStoppedTask = localThreadController.Exit(); + + // Wait for off-thread pipeline requests and/or ExitNestedPrompt + while (true) + { + int taskIndex = Task.WaitAny( + localPipelineExecutionTask, + localDebuggerStoppedTask); + + if (taskIndex == 0) + { + var localExecutionTask = localPipelineExecutionTask.GetAwaiter().GetResult(); + localPipelineExecutionTask = localThreadController.TakeExecutionRequest(); + localExecutionTask.Execute().GetAwaiter().GetResult(); + continue; + } + + this.ConsoleReader?.StopCommandLoop(); + this.PromptNest.PopPromptContext(); + break; + } + } + + /// + /// Called by the external PSHost when $Host.ExitNestedPrompt is called. + /// + internal void ExitNestedPrompt() + { + if (this.PromptNest.NestedPromptLevel == 1 || !this.PromptNest.IsNestedPrompt) + { + this.logger.Write( + LogLevel.Error, + "ExitNestedPrompt was called outside of a nested prompt."); + return; } + + // Stop the command input loop so PSReadLine isn't invoked between ExitNestedPrompt + // being invoked and EnterNestedPrompt getting the message to exit. + this.ConsoleReader?.StopCommandLoop(); + this.PromptNest.GetThreadController().StartThreadExit(DebuggerResumeAction.Stop); } /// @@ -1109,15 +1505,17 @@ public async Task SetWorkingDirectory(string path, bool isPathAlreadyEscaped) { this.InitialWorkingDirectory = path; - using (RunspaceHandle runspaceHandle = await this.GetRunspaceHandle()) + if (!isPathAlreadyEscaped) { - if (!isPathAlreadyEscaped) - { - path = EscapePath(path, false); - } - - runspaceHandle.Runspace.SessionStateProxy.Path.SetLocation(path); + path = EscapePath(path, false); } + + await ExecuteCommand( + new PSCommand().AddCommand("Set-Location").AddParameter("Path", path), + null, + sendOutputToHost: false, + sendErrorToHost: false, + addToHistory: false); } /// @@ -1238,7 +1636,9 @@ private IEnumerable ExecuteCommandInDebugger(PSCommand psComma if (debuggerResumeAction.HasValue) { // Resume the debugger with the specificed action - this.ResumeDebugger(debuggerResumeAction.Value); + this.ResumeDebugger( + debuggerResumeAction.Value, + shouldWaitForExit: false); } return output; @@ -1401,11 +1801,11 @@ private Command GetOutputCommand(bool endOfStatement) { Command outputCommand = new Command( - command: this.IsDebuggerStopped ? "Out-String" : "Out-Default", + command: this.PromptNest.IsInDebugger ? "Out-String" : "Out-Default", isScript: false, useLocalScope: true); - if (this.IsDebuggerStopped) + if (this.PromptNest.IsInDebugger) { // Out-String needs the -Stream parameter added outputCommand.Parameters.Add("Stream"); @@ -1512,6 +1912,12 @@ private SessionDetails GetSessionDetails(Func invokeAction) LogLevel.Verbose, "Runtime exception occurred while gathering runspace info:\r\n\r\n" + e.ToString()); } + catch (ArgumentNullException) + { + this.logger.Write( + LogLevel.Error, + "Could not retrieve session details but no exception was thrown."); + } // TODO: Return a harmless object if necessary this.mostRecentSessionDetails = null; @@ -1652,21 +2058,46 @@ private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs ar /// public event EventHandler DebuggerResumed; + private void StartCommandLoopOnRunspaceAvailable() + { + if (Interlocked.CompareExchange(ref this.isCommandLoopRestarterSet, 1, 1) == 1) + { + return; + } + + EventHandler handler = null; + handler = (runspace, eventArgs) => + { + if (eventArgs.RunspaceAvailability != RunspaceAvailability.Available || + this.versionSpecificOperations.IsDebuggerStopped(this.PromptNest, (Runspace)runspace)) + { + return; + } + + ((Runspace)runspace).AvailabilityChanged -= handler; + Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 0); + this.ConsoleReader?.StartCommandLoop(); + }; + + this.CurrentRunspace.Runspace.AvailabilityChanged += handler; + Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 1); + } + private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) { - this.logger.Write(LogLevel.Verbose, "Debugger stopped execution."); + if (CurrentRunspace.Context == RunspaceContext.Original) + { + StartCommandLoopOnRunspaceAvailable(); + } - // Set the task so a result can be set - this.debuggerStoppedTask = - new TaskCompletionSource(); + this.logger.Write(LogLevel.Verbose, "Debugger stopped execution."); - // Save the pipeline thread ID and create the pipeline execution task - this.pipelineThreadId = Thread.CurrentThread.ManagedThreadId; - this.pipelineExecutionTask = new TaskCompletionSource(); + PromptNest.PushPromptContext( + IsCurrentRunspaceOutOfProcess() + ? PromptNestFrameType.Debug | PromptNestFrameType.Remote + : PromptNestFrameType.Debug); - // Hold on to local task vars so that the fields can be cleared independently - Task localDebuggerStoppedTask = this.debuggerStoppedTask.Task; - Task localPipelineExecutionTask = this.pipelineExecutionTask.Task; + ThreadController localThreadController = PromptNest.GetThreadController(); // Update the session state this.OnSessionStateChanged( @@ -1676,18 +2107,35 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) PowerShellExecutionResult.Stopped, null)); - // Get the session details and push the current - // runspace if the session has changed - var sessionDetails = this.GetSessionDetailsInDebugger(); + // Get the session details and push the current + // runspace if the session has changed + SessionDetails sessionDetails = null; + try + { + sessionDetails = this.GetSessionDetailsInDebugger(); + } + catch (InvalidOperationException) + { + this.logger.Write( + LogLevel.Verbose, + "Attempting to get session details failed, most likely due to a running pipeline that is attempting to stop."); + } - // Push the current runspace if the session has changed - this.UpdateRunspaceDetailsIfSessionChanged(sessionDetails, isDebuggerStop: true); + if (!localThreadController.FrameExitTask.Task.IsCompleted) + { + // Push the current runspace if the session has changed + this.UpdateRunspaceDetailsIfSessionChanged(sessionDetails, isDebuggerStop: true); - // Raise the event for the debugger service - this.DebuggerStop?.Invoke(sender, e); + // Raise the event for the debugger service + this.DebuggerStop?.Invoke(sender, e); + } this.logger.Write(LogLevel.Verbose, "Starting pipeline thread message loop..."); + Task localPipelineExecutionTask = + localThreadController.TakeExecutionRequest(); + Task localDebuggerStoppedTask = + localThreadController.Exit(); while (true) { int taskIndex = @@ -1700,7 +2148,7 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) // Write a new output line before continuing this.WriteOutput("", true); - e.ResumeAction = localDebuggerStoppedTask.Result; + e.ResumeAction = localDebuggerStoppedTask.GetAwaiter().GetResult(); this.logger.Write(LogLevel.Verbose, "Received debugger resume action " + e.ResumeAction.ToString()); // Notify listeners that the debugger has resumed @@ -1731,15 +2179,14 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) this.logger.Write(LogLevel.Verbose, "Received pipeline thread execution request."); IPipelineExecutionRequest executionRequest = localPipelineExecutionTask.Result; - - this.pipelineExecutionTask = new TaskCompletionSource(); - localPipelineExecutionTask = this.pipelineExecutionTask.Task; - - executionRequest.Execute().Wait(); + localPipelineExecutionTask = localThreadController.TakeExecutionRequest(); + executionRequest.Execute().GetAwaiter().GetResult(); this.logger.Write(LogLevel.Verbose, "Pipeline thread execution completed."); - if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.Available) + if (!this.versionSpecificOperations.IsDebuggerStopped( + this.PromptNest, + this.CurrentRunspace.Runspace)) { if (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace) { @@ -1761,9 +2208,7 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) } } - // Clear the task so that it won't be used again - this.debuggerStoppedTask = null; - this.pipelineExecutionTask = null; + PromptNest.PopPromptContext(); } // NOTE: This event is 'internal' because the DebugService provides @@ -1779,56 +2224,6 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) #region Nested Classes - private interface IPipelineExecutionRequest - { - Task Execute(); - } - - /// - /// Contains details relating to a request to execute a - /// command on the PowerShell pipeline thread. - /// - /// The expected result type of the execution. - private class PipelineExecutionRequest : IPipelineExecutionRequest - { - PowerShellContext powerShellContext; - PSCommand psCommand; - StringBuilder errorMessages; - bool sendOutputToHost; - TaskCompletionSource> resultsTask; - - public Task> Results - { - get { return this.resultsTask.Task; } - } - - public PipelineExecutionRequest( - PowerShellContext powerShellContext, - PSCommand psCommand, - StringBuilder errorMessages, - bool sendOutputToHost) - { - this.powerShellContext = powerShellContext; - this.psCommand = psCommand; - this.errorMessages = errorMessages; - this.sendOutputToHost = sendOutputToHost; - this.resultsTask = new TaskCompletionSource>(); - } - - public async Task Execute() - { - var results = - await this.powerShellContext.ExecuteCommand( - psCommand, - errorMessages, - sendOutputToHost); - - this.resultsTask.SetResult(results); - - // TODO: Deal with errors? - } - } - private void ConfigureRunspaceCapabilities(RunspaceDetails runspaceDetails) { DscBreakpointCapability.CheckForCapability(this.CurrentRunspace, this, this.logger); diff --git a/src/PowerShellEditorServices/Session/PromptNest.cs b/src/PowerShellEditorServices/Session/PromptNest.cs new file mode 100644 index 000000000..9cf4437f2 --- /dev/null +++ b/src/PowerShellEditorServices/Session/PromptNest.cs @@ -0,0 +1,564 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System; + using System.Management.Automation; + + /// + /// Represents the stack of contexts in which PowerShell commands can be invoked. + /// + internal class PromptNest : IDisposable + { + private ConcurrentStack _frameStack; + + private PromptNestFrame _readLineFrame; + + private IHostInput _consoleReader; + + private PowerShellContext _powerShellContext; + + private IVersionSpecificOperations _versionSpecificOperations; + + private bool _isDisposed; + + private object _syncObject = new object(); + + private object _disposeSyncObject = new object(); + + /// + /// Initializes a new instance of the class. + /// + /// + /// The to track prompt status for. + /// + /// + /// The instance for the first frame. + /// + /// + /// The input handler. + /// + /// + /// The for the calling + /// instance. + /// + /// + /// This constructor should only be called when + /// is set to the initial runspace. + /// + internal PromptNest( + PowerShellContext powerShellContext, + PowerShell initialPowerShell, + IHostInput consoleReader, + IVersionSpecificOperations versionSpecificOperations) + { + _versionSpecificOperations = versionSpecificOperations; + _consoleReader = consoleReader; + _powerShellContext = powerShellContext; + _frameStack = new ConcurrentStack(); + _frameStack.Push( + new PromptNestFrame( + initialPowerShell, + NewHandleQueue())); + + var readLineShell = PowerShell.Create(); + readLineShell.Runspace = powerShellContext.CurrentRunspace.Runspace; + _readLineFrame = new PromptNestFrame( + readLineShell, + new AsyncQueue()); + + ReleaseRunspaceHandleImpl(isReadLine: true); + } + + /// + /// Gets a value indicating whether the current frame was created by a debugger stop event. + /// + internal bool IsInDebugger => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Debug); + + /// + /// Gets a value indicating whether the current frame was created for an out of process runspace. + /// + internal bool IsRemote => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Remote); + + /// + /// Gets a value indicating whether the current frame was created by PSHost.EnterNestedPrompt(). + /// + internal bool IsNestedPrompt => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt); + + /// + /// Gets a value indicating the current number of frames managed by this PromptNest. + /// + internal int NestedPromptLevel => _frameStack.Count; + + private PromptNestFrame CurrentFrame + { + get + { + _frameStack.TryPeek(out PromptNestFrame currentFrame); + return _isDisposed ? _readLineFrame : currentFrame; + } + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + lock (_disposeSyncObject) + { + if (_isDisposed || !disposing) + { + return; + } + + while (NestedPromptLevel > 1) + { + _consoleReader?.StopCommandLoop(); + var currentFrame = CurrentFrame; + if (currentFrame.FrameType.HasFlag(PromptNestFrameType.Debug)) + { + _versionSpecificOperations.StopCommandInDebugger(_powerShellContext); + currentFrame.ThreadController.StartThreadExit(DebuggerResumeAction.Stop); + currentFrame.WaitForFrameExit(CancellationToken.None); + continue; + } + + if (currentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt)) + { + _powerShellContext.ExitAllNestedPrompts(); + continue; + } + + currentFrame.PowerShell.BeginStop(null, null); + currentFrame.WaitForFrameExit(CancellationToken.None); + } + + _consoleReader?.StopCommandLoop(); + _readLineFrame.Dispose(); + CurrentFrame.Dispose(); + _frameStack.Clear(); + _powerShellContext = null; + _consoleReader = null; + _isDisposed = true; + } + } + + /// + /// Gets the for the current frame. + /// + /// + /// The for the current frame, or + /// if the current frame does not have one. + /// + internal ThreadController GetThreadController() + { + if (_isDisposed) + { + return null; + } + + return CurrentFrame.IsThreadController ? CurrentFrame.ThreadController : null; + } + + /// + /// Create a new and set it as the current frame. + /// + internal void PushPromptContext() + { + if (_isDisposed) + { + return; + } + + PushPromptContext(PromptNestFrameType.Normal); + } + + /// + /// Create a new and set it as the current frame. + /// + /// The frame type. + internal void PushPromptContext(PromptNestFrameType frameType) + { + if (_isDisposed) + { + return; + } + + _frameStack.Push( + new PromptNestFrame( + frameType.HasFlag(PromptNestFrameType.Remote) + ? PowerShell.Create() + : PowerShell.Create(RunspaceMode.CurrentRunspace), + NewHandleQueue(), + frameType)); + } + + /// + /// Dispose of the current and revert to the previous frame. + /// + internal void PopPromptContext() + { + PromptNestFrame currentFrame; + lock (_syncObject) + { + if (_isDisposed || _frameStack.Count == 1) + { + return; + } + + _frameStack.TryPop(out currentFrame); + } + + currentFrame.Dispose(); + } + + /// + /// Get the instance for the current + /// . + /// + /// Indicates whether this is for a PSReadLine command. + /// The instance for the current frame. + internal PowerShell GetPowerShell(bool isReadLine = false) + { + if (_isDisposed) + { + return null; + } + + // Typically we want to run PSReadLine on the current nest frame. + // The exception is when the current frame is remote, in which + // case we need to run it in it's own frame because we can't take + // over a remote pipeline through event invocation. + if (NestedPromptLevel > 1 && !IsRemote) + { + return CurrentFrame.PowerShell; + } + + return isReadLine ? _readLineFrame.PowerShell : CurrentFrame.PowerShell; + } + + /// + /// Get the for the current . + /// + /// + /// The that can be used to cancel the request. + /// + /// Indicates whether this is for a PSReadLine command. + /// The for the current frame. + internal RunspaceHandle GetRunspaceHandle(CancellationToken cancellationToken, bool isReadLine) + { + if (_isDisposed) + { + return null; + } + + // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace + // is in process. + if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + GetRunspaceHandleImpl(cancellationToken, isReadLine: false); + } + + return GetRunspaceHandleImpl(cancellationToken, isReadLine); + } + + + /// + /// Get the for the current . + /// + /// + /// The that will be checked prior to + /// completing the returned task. + /// + /// Indicates whether this is for a PSReadLine command. + /// + /// A object representing the asynchronous operation. + /// The property will return the + /// for the current frame. + /// + internal async Task GetRunspaceHandleAsync(CancellationToken cancellationToken, bool isReadLine) + { + if (_isDisposed) + { + return null; + } + + // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace + // is in process. + if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + await GetRunspaceHandleImplAsync(cancellationToken, isReadLine: false); + } + + return await GetRunspaceHandleImplAsync(cancellationToken, isReadLine); + } + + /// + /// Releases control of the runspace aquired via the . + /// + /// + /// The representing the control to release. + /// + internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) + { + if (_isDisposed) + { + return; + } + + ReleaseRunspaceHandleImpl(runspaceHandle.IsReadLine); + if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + ReleaseRunspaceHandleImpl(isReadLine: false); + } + } + + /// + /// Releases control of the runspace aquired via the . + /// + /// + /// The representing the control to release. + /// + /// + /// A object representing the release of the + /// . + /// + internal async Task ReleaseRunspaceHandleAsync(RunspaceHandle runspaceHandle) + { + if (_isDisposed) + { + return; + } + + await ReleaseRunspaceHandleImplAsync(runspaceHandle.IsReadLine); + if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + await ReleaseRunspaceHandleImplAsync(isReadLine: false); + } + } + + /// + /// Determines if the current frame is unavailable for commands. + /// + /// + /// A value indicating whether the current frame is unavailable for commands. + /// + internal bool IsMainThreadBusy() + { + return !_isDisposed && CurrentFrame.Queue.IsEmpty; + } + + /// + /// Determines if a PSReadLine command is currently running. + /// + /// + /// A value indicating whether a PSReadLine command is currently running. + /// + internal bool IsReadLineBusy() + { + return !_isDisposed && _readLineFrame.Queue.IsEmpty; + } + + /// + /// Blocks until the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + internal void WaitForCurrentFrameExit(Action initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + initiator.Invoke(currentFrame); + } + finally + { + currentFrame.WaitForFrameExit(CancellationToken.None); + } + } + + /// + /// Blocks until the current frame has been disposed. + /// + internal void WaitForCurrentFrameExit() + { + if (_isDisposed) + { + return; + } + + CurrentFrame.WaitForFrameExit(CancellationToken.None); + } + + /// + /// Blocks until the current frame has been disposed. + /// + /// + /// The used the exit the block prior to + /// the current frame being disposed. + /// + internal void WaitForCurrentFrameExit(CancellationToken cancellationToken) + { + if (_isDisposed) + { + return; + } + + CurrentFrame.WaitForFrameExit(cancellationToken); + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(Func initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + await initiator.Invoke(currentFrame); + } + finally + { + await currentFrame.WaitForFrameExitAsync(CancellationToken.None); + } + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(Action initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + initiator.Invoke(currentFrame); + } + finally + { + await currentFrame.WaitForFrameExitAsync(CancellationToken.None); + } + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync() + { + if (_isDisposed) + { + return; + } + + await WaitForCurrentFrameExitAsync(CancellationToken.None); + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// The used the exit the block prior to the current frame being disposed. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(CancellationToken cancellationToken) + { + if (_isDisposed) + { + return; + } + + await CurrentFrame.WaitForFrameExitAsync(cancellationToken); + } + + private AsyncQueue NewHandleQueue() + { + var queue = new AsyncQueue(); + queue.Enqueue(new RunspaceHandle(_powerShellContext)); + return queue; + } + + private RunspaceHandle GetRunspaceHandleImpl(CancellationToken cancellationToken, bool isReadLine) + { + if (isReadLine) + { + return _readLineFrame.Queue.Dequeue(cancellationToken); + } + + return CurrentFrame.Queue.Dequeue(cancellationToken); + } + + private async Task GetRunspaceHandleImplAsync(CancellationToken cancellationToken, bool isReadLine) + { + if (isReadLine) + { + return await _readLineFrame.Queue.DequeueAsync(cancellationToken); + } + + return await CurrentFrame.Queue.DequeueAsync(cancellationToken); + } + + private void ReleaseRunspaceHandleImpl(bool isReadLine) + { + if (isReadLine) + { + _readLineFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, true)); + return; + } + + CurrentFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, false)); + } + + private async Task ReleaseRunspaceHandleImplAsync(bool isReadLine) + { + if (isReadLine) + { + await _readLineFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, true)); + return; + } + + await CurrentFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, false)); + } + } +} diff --git a/src/PowerShellEditorServices/Session/PromptNestFrame.cs b/src/PowerShellEditorServices/Session/PromptNestFrame.cs new file mode 100644 index 000000000..cae7dfb8a --- /dev/null +++ b/src/PowerShellEditorServices/Session/PromptNestFrame.cs @@ -0,0 +1,137 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System.Management.Automation; + + /// + /// Represents a single frame in the . + /// + internal class PromptNestFrame : IDisposable + { + private const PSInvocationState IndisposableStates = PSInvocationState.Stopping | PSInvocationState.Running; + + private SemaphoreSlim _frameExited = new SemaphoreSlim(initialCount: 0); + + private bool _isDisposed = false; + + /// + /// Gets the instance. + /// + internal PowerShell PowerShell { get; } + + /// + /// Gets the queue that controls command invocation order. + /// + internal AsyncQueue Queue { get; } + + /// + /// Gets the frame type. + /// + internal PromptNestFrameType FrameType { get; } + + /// + /// Gets the . + /// + internal ThreadController ThreadController { get; } + + /// + /// Gets a value indicating whether the frame requires command invocations + /// to be routed to a specific thread. + /// + internal bool IsThreadController { get; } + + internal PromptNestFrame(PowerShell powerShell, AsyncQueue handleQueue) + : this(powerShell, handleQueue, PromptNestFrameType.Normal) + { } + + internal PromptNestFrame( + PowerShell powerShell, + AsyncQueue handleQueue, + PromptNestFrameType frameType) + { + PowerShell = powerShell; + Queue = handleQueue; + FrameType = frameType; + IsThreadController = (frameType & (PromptNestFrameType.Debug | PromptNestFrameType.NestedPrompt)) != 0; + if (!IsThreadController) + { + return; + } + + ThreadController = new ThreadController(this); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + if (disposing) + { + if (IndisposableStates.HasFlag(PowerShell.InvocationStateInfo.State)) + { + PowerShell.BeginStop( + asyncResult => + { + PowerShell.Runspace = null; + PowerShell.Dispose(); + }, + state: null); + } + else + { + PowerShell.Runspace = null; + PowerShell.Dispose(); + } + + _frameExited.Release(); + } + + _isDisposed = true; + } + + /// + /// Blocks until the frame has been disposed. + /// + /// + /// The that will exit the block when cancelled. + /// + internal void WaitForFrameExit(CancellationToken cancellationToken) + { + _frameExited.Wait(cancellationToken); + _frameExited.Release(); + } + + /// + /// Creates a task object that is completed when the frame has been disposed. + /// + /// + /// The that will be checked prior to completing + /// the returned task. + /// + /// + /// A object that represents this frame being disposed. + /// + internal async Task WaitForFrameExitAsync(CancellationToken cancellationToken) + { + await _frameExited.WaitAsync(cancellationToken); + _frameExited.Release(); + } + } +} diff --git a/src/PowerShellEditorServices/Session/PromptNestFrameType.cs b/src/PowerShellEditorServices/Session/PromptNestFrameType.cs new file mode 100644 index 000000000..b42b42098 --- /dev/null +++ b/src/PowerShellEditorServices/Session/PromptNestFrameType.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + [Flags] + internal enum PromptNestFrameType + { + Normal = 0, + + NestedPrompt = 1, + + Debug = 2, + + Remote = 4 + } +} diff --git a/src/PowerShellEditorServices/Session/RunspaceHandle.cs b/src/PowerShellEditorServices/Session/RunspaceHandle.cs index b7fc0e8f1..4947eadbe 100644 --- a/src/PowerShellEditorServices/Session/RunspaceHandle.cs +++ b/src/PowerShellEditorServices/Session/RunspaceHandle.cs @@ -28,14 +28,21 @@ public Runspace Runspace } } + internal bool IsReadLine { get; } + /// /// Initializes a new instance of the RunspaceHandle class using the /// given runspace. /// /// The PowerShellContext instance which manages the runspace. public RunspaceHandle(PowerShellContext powerShellContext) + : this(powerShellContext, false) + { } + + internal RunspaceHandle(PowerShellContext powerShellContext, bool isReadLine) { this.powerShellContext = powerShellContext; + this.IsReadLine = isReadLine; } /// diff --git a/src/PowerShellEditorServices/Session/ThreadController.cs b/src/PowerShellEditorServices/Session/ThreadController.cs new file mode 100644 index 000000000..9a5583f5b --- /dev/null +++ b/src/PowerShellEditorServices/Session/ThreadController.cs @@ -0,0 +1,131 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides the ability to route PowerShell command invocations to a specific thread. + /// + internal class ThreadController + { + private PromptNestFrame _nestFrame; + + internal AsyncQueue PipelineRequestQueue { get; } + + internal TaskCompletionSource FrameExitTask { get; } + + internal int ManagedThreadId { get; } + + internal bool IsPipelineThread { get; } + + /// + /// Initializes an new instance of the ThreadController class. This constructor should only + /// ever been called from the thread it is meant to control. + /// + /// The parent PromptNestFrame object. + internal ThreadController(PromptNestFrame nestFrame) + { + _nestFrame = nestFrame; + PipelineRequestQueue = new AsyncQueue(); + FrameExitTask = new TaskCompletionSource(); + ManagedThreadId = Thread.CurrentThread.ManagedThreadId; + + // If the debugger stop is triggered on a thread with no default runspace we + // shouldn't attempt to route commands to it. + IsPipelineThread = Runspace.DefaultRunspace != null; + } + + /// + /// Determines if the caller is already on the thread that this object maintains. + /// + /// + /// A value indicating if the caller is already on the thread maintained by this object. + /// + internal bool IsCurrentThread() + { + return Thread.CurrentThread.ManagedThreadId == ManagedThreadId; + } + + /// + /// Requests the invocation of a PowerShell command on the thread maintained by this object. + /// + /// The execution request to send. + /// + /// A task object representing the asynchronous operation. The Result property will return + /// the output of the command invocation. + /// + internal async Task> RequestPipelineExecution( + PipelineExecutionRequest executionRequest) + { + await PipelineRequestQueue.EnqueueAsync(executionRequest); + return await executionRequest.Results; + } + + /// + /// Retrieves the first currently queued execution request. If there are no pending + /// execution requests then the task will be completed when one is requested. + /// + /// + /// A task object representing the asynchronous operation. The Result property will return + /// the retrieved pipeline execution request. + /// + internal async Task TakeExecutionRequest() + { + return await PipelineRequestQueue.DequeueAsync(); + } + + /// + /// Marks the thread to be exited. + /// + /// + /// The resume action for the debugger. If the frame is not a debugger frame this parameter + /// is ignored. + /// + internal void StartThreadExit(DebuggerResumeAction action) + { + StartThreadExit(action, waitForExit: false); + } + + /// + /// Marks the thread to be exited. + /// + /// + /// The resume action for the debugger. If the frame is not a debugger frame this parameter + /// is ignored. + /// + /// + /// Indicates whether the method should block until the exit is completed. + /// + internal void StartThreadExit(DebuggerResumeAction action, bool waitForExit) + { + Task.Run(() => FrameExitTask.TrySetResult(action)); + if (!waitForExit) + { + return; + } + + _nestFrame.WaitForFrameExit(CancellationToken.None); + } + + /// + /// Creates a task object that completes when the thread has be marked for exit. + /// + /// + /// A task object representing the frame receiving a request to exit. The Result property + /// will return the DebuggerResumeAction supplied with the request. + /// + internal async Task Exit() + { + return await FrameExitTask.Task.ConfigureAwait(false); + } + } +} diff --git a/src/PowerShellEditorServices/Utility/AsyncLock.cs b/src/PowerShellEditorServices/Utility/AsyncLock.cs index eee894d9c..5eba1b24f 100644 --- a/src/PowerShellEditorServices/Utility/AsyncLock.cs +++ b/src/PowerShellEditorServices/Utility/AsyncLock.cs @@ -74,6 +74,31 @@ public Task LockAsync(CancellationToken cancellationToken) TaskScheduler.Default); } + /// + /// Obtains or waits for a lock which can be used to synchronize + /// access to a resource. + /// + /// + public IDisposable Lock() + { + return Lock(CancellationToken.None); + } + + /// + /// Obtains or waits for a lock which can be used to synchronize + /// access to a resource. The wait may be cancelled with the + /// given CancellationToken. + /// + /// + /// A CancellationToken which can be used to cancel the lock. + /// + /// + public IDisposable Lock(CancellationToken cancellationToken) + { + lockSemaphore.Wait(cancellationToken); + return this.lockReleaseTask.Result; + } + #endregion #region Private Classes diff --git a/src/PowerShellEditorServices/Utility/AsyncQueue.cs b/src/PowerShellEditorServices/Utility/AsyncQueue.cs index 98c00dc8e..85bbc1592 100644 --- a/src/PowerShellEditorServices/Utility/AsyncQueue.cs +++ b/src/PowerShellEditorServices/Utility/AsyncQueue.cs @@ -87,13 +87,38 @@ public async Task EnqueueAsync(T item) return; } } - + // No more requests waiting, queue the item for a later request this.itemQueue.Enqueue(item); this.IsEmpty = false; } } + /// + /// Enqueues an item onto the end of the queue. + /// + /// The item to be added to the queue. + public void Enqueue(T item) + { + using (queueLock.Lock()) + { + while (this.requestQueue.Count > 0) + { + var requestTaskSource = this.requestQueue.Dequeue(); + if (requestTaskSource.Task.IsCanceled) + { + continue; + } + + requestTaskSource.SetResult(item); + return; + } + } + + this.itemQueue.Enqueue(item); + this.IsEmpty = false; + } + /// /// Dequeues an item from the queue or waits asynchronously /// until an item is available. @@ -149,6 +174,50 @@ public async Task DequeueAsync(CancellationToken cancellationToken) return await requestTask; } + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. + /// + /// + public T Dequeue() + { + return Dequeue(CancellationToken.None); + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. The wait can be cancelled + /// using the given CancellationToken. + /// + /// + /// A CancellationToken with which a dequeue wait can be cancelled. + /// + /// + public T Dequeue(CancellationToken cancellationToken) + { + TaskCompletionSource requestTask; + using (queueLock.Lock(cancellationToken)) + { + if (this.itemQueue.Count > 0) + { + T item = this.itemQueue.Dequeue(); + this.IsEmpty = this.itemQueue.Count == 0; + + return item; + } + + requestTask = new TaskCompletionSource(); + this.requestQueue.Enqueue(requestTask); + + if (cancellationToken.CanBeCanceled) + { + cancellationToken.Register(() => requestTask.TrySetCanceled()); + } + } + + return requestTask.Task.GetAwaiter().GetResult(); + } + #endregion } } diff --git a/src/PowerShellEditorServices/Utility/AsyncUtils.cs b/src/PowerShellEditorServices/Utility/AsyncUtils.cs new file mode 100644 index 000000000..8da21b942 --- /dev/null +++ b/src/PowerShellEditorServices/Utility/AsyncUtils.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides utility methods for common asynchronous operations. + /// + internal static class AsyncUtils + { + /// + /// Creates a with an handle initial and + /// max count of one. + /// + /// A simple single handle . + internal static SemaphoreSlim CreateSimpleLockingSemaphore() + { + return new SemaphoreSlim(initialCount: 1, maxCount: 1); + } + } +} diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 65d8308b8..ac88caaf8 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -72,9 +72,12 @@ void debugService_BreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) // TODO: Needed? } - async void debugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) + void debugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) { - await this.debuggerStoppedQueue.EnqueueAsync(e); + // We need to ensure this is run on a different thread than the one it's + // called on because it can cause PowerShellContext.OnDebuggerStopped to + // never hit the while loop. + Task.Run(() => this.debuggerStoppedQueue.Enqueue(e)); } public void Dispose() @@ -491,9 +494,10 @@ await this.AssertStateChange( // Abort execution and wait for the debugger to exit this.debugService.Abort(); + await this.AssertStateChange( PowerShellContextState.Ready, - PowerShellExecutionResult.Aborted); + PowerShellExecutionResult.Stopped); } [Fact] @@ -514,9 +518,10 @@ await this.AssertStateChange( // Abort execution and wait for the debugger to exit this.debugService.Abort(); + await this.AssertStateChange( PowerShellContextState.Ready, - PowerShellExecutionResult.Aborted); + PowerShellExecutionResult.Stopped); } [Fact] @@ -908,7 +913,7 @@ public async Task AssertDebuggerPaused() SynchronizationContext syncContext = SynchronizationContext.Current; DebuggerStoppedEventArgs eventArgs = - await this.debuggerStoppedQueue.DequeueAsync(); + await this.debuggerStoppedQueue.DequeueAsync(new CancellationTokenSource(5000).Token); Assert.Equal(0, eventArgs.OriginalEvent.Breakpoints.Count); } @@ -920,7 +925,7 @@ public async Task AssertDebuggerStopped( SynchronizationContext syncContext = SynchronizationContext.Current; DebuggerStoppedEventArgs eventArgs = - await this.debuggerStoppedQueue.DequeueAsync(); + await this.debuggerStoppedQueue.DequeueAsync(new CancellationTokenSource(5000).Token); @@ -936,7 +941,7 @@ private async Task AssertStateChange( PowerShellExecutionResult expectedResult = PowerShellExecutionResult.Completed) { SessionStateChangedEventArgs newState = - await this.sessionStateQueue.DequeueAsync(); + await this.sessionStateQueue.DequeueAsync(new CancellationTokenSource(5000).Token); Assert.Equal(expectedState, newState.NewSessionState); Assert.Equal(expectedResult, newState.ExecutionResult); diff --git a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs index 806a935b3..22a41d2e9 100644 --- a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs +++ b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs @@ -19,7 +19,7 @@ internal static class PowerShellContextFactory { public static PowerShellContext Create(ILogger logger) { - PowerShellContext powerShellContext = new PowerShellContext(logger); + PowerShellContext powerShellContext = new PowerShellContext(logger, isPSReadLineEnabled: false); powerShellContext.Initialize( PowerShellContextTests.TestProfilePaths, PowerShellContext.CreateRunspace(