From 26f6adad5db6bca049328fa0d4315bf61a142a6c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 2 Apr 2017 17:10:47 -0700 Subject: [PATCH 1/3] Interrupt integrated console command prompt when commands are executed This change fixes a general class of problems in the integrated console experience where commands that were executed internally (like extension commands or running Plaster templates) do not interrupt the command prompt, causing future input to be written over previous output lines. The fix is to have the command prompt be interrupted by any commands which write output or errors to the host. Fixes #411. --- .../Server/DebugAdapter.cs | 12 --- .../Server/LanguageServer.cs | 3 - .../Console/ConsoleService.cs | 72 +++++++++------ .../Session/ExecutionOptions.cs | 59 ++++++++++++ .../Session/ExecutionStatus.cs | 39 ++++++++ .../ExecutionStatusChangedEventArgs.cs | 52 +++++++++++ .../Session/PowerShellContext.cs | 91 +++++++++++++++++-- 7 files changed, 273 insertions(+), 55 deletions(-) create mode 100644 src/PowerShellEditorServices/Session/ExecutionOptions.cs create mode 100644 src/PowerShellEditorServices/Session/ExecutionStatus.cs create mode 100644 src/PowerShellEditorServices/Session/ExecutionStatusChangedEventArgs.cs diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index 4a5ef7d76..ffbb065de 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -100,9 +100,6 @@ protected override void Initialize() protected Task LaunchScript(RequestContext requestContext) { - // Ensure the read loop is stopped - this.editorSession.ConsoleService.CancelReadLoop(); - // Is this an untitled script? Task launchTask = null; @@ -144,9 +141,6 @@ private async Task OnExecutionCompleted(Task executeTask) if (this.isAttachSession) { - // Ensure the read loop is stopped - this.editorSession.ConsoleService.CancelReadLoop(); - // Pop the sessions if (this.editorSession.PowerShellContext.CurrentRunspace.Context == RunspaceContext.EnteredProcess) { @@ -165,12 +159,6 @@ private async Task OnExecutionCompleted(Task executeTask) Logger.WriteException("Caught exception while popping attached process after debugging", e); } } - - } - - if (!this.ownsEditorSession) - { - this.editorSession.ConsoleService.StartReadLoop(); } if (this.disconnectRequestContext != null) diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 56efe1eb2..cbd7db859 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -1108,9 +1108,6 @@ protected Task HandleEvaluateRequest( DebugAdapterMessages.EvaluateRequestArguments evaluateParams, RequestContext requestContext) { - // Cancel the read loop before executing - this.editorSession.ConsoleService.CancelReadLoop(); - // We don't await the result of the execution here because we want // to be able to receive further messages while the current script // is executing. This important in cases where the pipeline thread diff --git a/src/PowerShellEditorServices/Console/ConsoleService.cs b/src/PowerShellEditorServices/Console/ConsoleService.cs index 8cea74ee0..5d6a8f03d 100644 --- a/src/PowerShellEditorServices/Console/ConsoleService.cs +++ b/src/PowerShellEditorServices/Console/ConsoleService.cs @@ -79,6 +79,7 @@ public ConsoleService( this.powerShellContext.ConsoleHost = this; this.powerShellContext.DebuggerStop += PowerShellContext_DebuggerStop; this.powerShellContext.DebuggerResumed += PowerShellContext_DebuggerResumed; + this.powerShellContext.ExecutionStatusChanged += PowerShellContext_ExecutionStatusChanged; // Set the default prompt handler factory or create // a default if one is not provided @@ -140,31 +141,6 @@ public void CancelReadLoop() } } - /// - /// Called when a command string is received from the user. - /// If a prompt is currently active, the prompt handler is - /// asked to handle the string. Otherwise the string is - /// executed in the PowerShellContext. - /// - /// The input string to evaluate. - /// If true, the input will be echoed to the console. - public void ExecuteCommand(string inputString, bool echoToConsole) - { - this.CancelReadLoop(); - - if (this.activePromptHandler == null) - { - // Execute the script string but don't wait for completion - var executeTask = - this.powerShellContext - .ExecuteScriptString( - inputString, - echoToConsole, - true) - .ConfigureAwait(false); - } - } - /// /// Executes a script file at the specified path. /// @@ -338,11 +314,16 @@ await this.consoleReadLine.ReadCommandLine( { Console.Write(Environment.NewLine); - await this.powerShellContext.ExecuteScriptString( - commandString, - false, - true, - true); + var unusedTask = + this.powerShellContext + .ExecuteScriptString( + commandString, + false, + true, + true) + .ConfigureAwait(false); + + break; } } while (!cancellationToken.IsCancellationRequested); @@ -459,6 +440,37 @@ private void PowerShellContext_DebuggerResumed(object sender, System.Management. this.CancelReadLoop(); } + private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionStatusChangedEventArgs eventArgs) + { + if (this.EnableConsoleRepl) + { + // Any command which writes output to the host will affect + // the display of the prompt + if (eventArgs.ExecutionOptions.WriteOutputToHost || + eventArgs.ExecutionOptions.InterruptCommandPrompt) + { + if (eventArgs.ExecutionStatus != ExecutionStatus.Running) + { + // Execution has completed, start the input prompt + this.StartReadLoop(); + } + else + { + // A new command was started, cancel the input prompt + this.CancelReadLoop(); + } + } + else if ( + eventArgs.ExecutionOptions.WriteErrorsToHost && + (eventArgs.ExecutionStatus == ExecutionStatus.Failed || + eventArgs.HadErrors)) + { + this.CancelReadLoop(); + this.StartReadLoop(); + } + } + } + #endregion } } diff --git a/src/PowerShellEditorServices/Session/ExecutionOptions.cs b/src/PowerShellEditorServices/Session/ExecutionOptions.cs new file mode 100644 index 000000000..3372c7556 --- /dev/null +++ b/src/PowerShellEditorServices/Session/ExecutionOptions.cs @@ -0,0 +1,59 @@ +// +// 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 +{ + /// + /// Defines options for the execution of a command. + /// + public class ExecutionOptions + { + #region Properties + + /// + /// Gets or sets a boolean that determines whether command output + /// should be written to the host. + /// + public bool WriteOutputToHost { get; set; } + + /// + /// Gets or sets a boolean that determines whether command errors + /// should be written to the host. + /// + public bool WriteErrorsToHost { get; set; } + + /// + /// Gets or sets a boolean that determines whether the executed + /// command should be added to the command history. + /// + public bool AddToHistory { get; set; } + + /// + /// Gets or sets a boolean that determines whether the execution + /// of the command should interrupt the command prompt. Should + /// only be set if WriteOutputToHost is false but the command + /// should still interrupt the command prompt. + /// + public bool InterruptCommandPrompt { get; set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ExecutionOptions class with + /// default settings configured. + /// + public ExecutionOptions() + { + this.WriteOutputToHost = true; + this.WriteErrorsToHost = true; + this.AddToHistory = false; + this.InterruptCommandPrompt = false; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Session/ExecutionStatus.cs b/src/PowerShellEditorServices/Session/ExecutionStatus.cs new file mode 100644 index 000000000..233d0499e --- /dev/null +++ b/src/PowerShellEditorServices/Session/ExecutionStatus.cs @@ -0,0 +1,39 @@ +// +// 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 +{ + /// + /// Enumerates the possible execution results that can occur after + /// executing a command or script. + /// + public enum ExecutionStatus + { + /// + /// Indicates that execution has not yet started. + /// + Pending, + + /// + /// Indicates that the command is executing. + /// + Running, + + /// + /// Indicates that execution has failed. + /// + Failed, + + /// + /// Indicates that execution was aborted by the user. + /// + Aborted, + + /// + /// Indicates that execution completed successfully. + /// + Completed + } +} diff --git a/src/PowerShellEditorServices/Session/ExecutionStatusChangedEventArgs.cs b/src/PowerShellEditorServices/Session/ExecutionStatusChangedEventArgs.cs new file mode 100644 index 000000000..cd2dcaaf2 --- /dev/null +++ b/src/PowerShellEditorServices/Session/ExecutionStatusChangedEventArgs.cs @@ -0,0 +1,52 @@ +// +// 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 +{ + /// + /// Contains details about an executed + /// + public class ExecutionStatusChangedEventArgs + { + #region Properties + + /// + /// Gets the options used when the command was executed. + /// + public ExecutionOptions ExecutionOptions { get; private set; } + + /// + /// Gets the command execution's current status. + /// + public ExecutionStatus ExecutionStatus { get; private set; } + + /// + /// If true, the command execution had errors. + /// + public bool HadErrors { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ExecutionStatusChangedEventArgs class. + /// + /// The command execution's current status. + /// The options used when the command was executed. + /// If execution has completed, indicates whether there were errors. + public ExecutionStatusChangedEventArgs( + ExecutionStatus executionStatus, + ExecutionOptions executionOptions, + bool hadErrors) + { + this.ExecutionStatus = executionStatus; + this.ExecutionOptions = executionOptions; + this.HadErrors = hadErrors || (executionStatus == ExecutionStatus.Failed); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index 9ab7e839e..6a7b23433 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -373,13 +373,43 @@ public async Task> ExecuteCommand( /// An awaitable Task which will provide results once the command /// execution completes. /// - public async Task> ExecuteCommand( + public Task> ExecuteCommand( PSCommand psCommand, StringBuilder errorMessages, bool sendOutputToHost = false, bool sendErrorToHost = true, bool addToHistory = false) { + return + this.ExecuteCommand( + psCommand, + errorMessages, + new ExecutionOptions + { + WriteOutputToHost = sendOutputToHost, + WriteErrorsToHost = sendErrorToHost, + AddToHistory = addToHistory + }); + } + + /// + /// Executes a PSCommand against the session's runspace and returns + /// a collection of results of the expected type. + /// + /// The expected result type. + /// The PSCommand to be executed. + /// Error messages from PowerShell will be written to the StringBuilder. + /// Specifies options to be used when executing this command. + /// + /// An awaitable Task which will provide results once the command + /// execution completes. + /// + public async Task> ExecuteCommand( + PSCommand psCommand, + StringBuilder errorMessages, + ExecutionOptions executionOptions) + { + bool hadErrors = false; RunspaceHandle runspaceHandle = null; IEnumerable executionResult = Enumerable.Empty(); @@ -392,7 +422,10 @@ public async Task> ExecuteCommand( PipelineExecutionRequest executionRequest = new PipelineExecutionRequest( - this, psCommand, errorMessages, sendOutputToHost); + this, + psCommand, + errorMessages, + executionOptions.WriteOutputToHost); // Send the pipeline execution request to the pipeline thread this.pipelineResultTask = new TaskCompletionSource(); @@ -406,7 +439,7 @@ public async Task> ExecuteCommand( try { // Instruct PowerShell to send output and errors to the host - if (sendOutputToHost) + if (executionOptions.WriteOutputToHost) { psCommand.Commands[0].MergeMyResults( PipelineResultTypes.Error, @@ -417,13 +450,18 @@ public async Task> ExecuteCommand( endOfStatement: false)); } + this.OnExecutionStatusChanged( + ExecutionStatus.Running, + executionOptions, + false); + if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.AvailableForNestedCommand || this.debuggerStoppedTask != null) { executionResult = this.ExecuteCommandInDebugger( psCommand, - sendOutputToHost); + executionOptions.WriteOutputToHost); } else { @@ -452,7 +490,7 @@ await Task.Factory.StartNew>( this.powerShell.Commands = psCommand; PSInvocationSettings invocationSettings = new PSInvocationSettings(); - invocationSettings.AddToHistory = addToHistory; + invocationSettings.AddToHistory = executionOptions.AddToHistory; result = this.powerShell.Invoke(null, invocationSettings); } catch (RemoteException e) @@ -482,6 +520,8 @@ await Task.Factory.StartNew>( errorMessages?.Append(errorMessage); Logger.Write(LogLevel.Error, errorMessage); + + hadErrors = true; } else { @@ -489,15 +529,13 @@ await Task.Factory.StartNew>( LogLevel.Verbose, "Execution completed successfully."); } - - return executionResult; } } catch (PipelineStoppedException e) { Logger.Write( LogLevel.Error, - "Popeline stopped while executing command:\r\n\r\n" + e.ToString()); + "Pipeline stopped while executing command:\r\n\r\n" + e.ToString()); errorMessages?.Append(e.Message); } @@ -507,18 +545,28 @@ await Task.Factory.StartNew>( LogLevel.Error, "Runtime exception occurred while executing command:\r\n\r\n" + e.ToString()); + hadErrors = true; errorMessages?.Append(e.Message); - if (sendErrorToHost) + if (executionOptions.WriteErrorsToHost) { // Write the error to the host this.WriteExceptionToHost(e); } } + catch (Exception e) + { + this.OnExecutionStatusChanged( + ExecutionStatus.Failed, + executionOptions, + true); + + throw e; + } finally { // Get the new prompt before releasing the runspace handle - if (sendOutputToHost) + if (executionOptions.WriteOutputToHost) { SessionDetails sessionDetails = null; @@ -556,6 +604,11 @@ await Task.Factory.StartNew>( } } + this.OnExecutionStatusChanged( + ExecutionStatus.Completed, + executionOptions, + hadErrors); + return executionResult; } @@ -1077,6 +1130,24 @@ private void OnRunspaceChanged(object sender, RunspaceChangedEventArgs e) this.RunspaceChanged?.Invoke(sender, e); } + /// + /// Raised when the status of an executed command changes. + /// + public event EventHandler ExecutionStatusChanged; + + private void OnExecutionStatusChanged( + ExecutionStatus executionStatus, + ExecutionOptions executionOptions, + bool hadErrors) + { + this.ExecutionStatusChanged?.Invoke( + this, + new ExecutionStatusChangedEventArgs( + executionStatus, + executionOptions, + hadErrors)); + } + #endregion #region Private Methods From c242a1f3218c1a706e8a5bbf8083905c72d42b0d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 2 Apr 2017 17:17:16 -0700 Subject: [PATCH 2/3] Interrupt command prompt when a Plaster template is invoked This change fixes an issue where invoking a Plaster template from within VS Code causes Plaster prompts to be handled incorrectly because the command prompt has not been interrupted. The fix is to request that the command prompt be interrupted when Invoke-Plaster is called. Fixes PowerShell/vscode-powershell#643. --- .../Templates/TemplateService.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Templates/TemplateService.cs b/src/PowerShellEditorServices/Templates/TemplateService.cs index 619972796..fe3a39af2 100644 --- a/src/PowerShellEditorServices/Templates/TemplateService.cs +++ b/src/PowerShellEditorServices/Templates/TemplateService.cs @@ -174,7 +174,14 @@ public async Task CreateFromTemplate( var errorString = new System.Text.StringBuilder(); await this.powerShellContext.ExecuteCommand( - command, errorString, false, true); + command, + errorString, + new ExecutionOptions + { + WriteOutputToHost = false, + WriteErrorsToHost = true, + InterruptCommandPrompt = true + }); // If any errors were written out, creation was not successful return errorString.Length == 0; From ec2c814d59167999e47fe92b4fa6a8f6d3b5f57d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 2 Apr 2017 17:19:32 -0700 Subject: [PATCH 3/3] Fix issues when invoking default values of input and choice fields This change fixes a couple of issues that appear when the user presses Enter to accept default values of input or choice prompts. This issue appeared when running through the default New Project template that is shipped with Plaster. Fixes PowerShell/vscode-powershell#504. --- .../Console/ChoicePromptHandler.cs | 9 ++++++++- .../Session/SessionPSHostUserInterface.cs | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Console/ChoicePromptHandler.cs b/src/PowerShellEditorServices/Console/ChoicePromptHandler.cs index 0eb28cb66..73d3c1f10 100644 --- a/src/PowerShellEditorServices/Console/ChoicePromptHandler.cs +++ b/src/PowerShellEditorServices/Console/ChoicePromptHandler.cs @@ -199,8 +199,15 @@ private async Task StartPromptLoop( break; } - // If the handler returns null it means we should prompt again choiceIndexes = this.HandleResponse(responseString); + + // Return the default choice values if no choices were entered + if (choiceIndexes == null && string.IsNullOrEmpty(responseString)) + { + choiceIndexes = this.DefaultChoices; + } + + // If the user provided no choices, we should prompt again if (choiceIndexes != null) { break; diff --git a/src/PowerShellEditorServices/Session/SessionPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/SessionPSHostUserInterface.cs index 17ba19adc..5a50ab127 100644 --- a/src/PowerShellEditorServices/Session/SessionPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Session/SessionPSHostUserInterface.cs @@ -119,7 +119,9 @@ public override Dictionary Prompt( { psObjectDict.Add( keyValuePair.Key, - PSObject.AsPSObject(keyValuePair.Value)); + keyValuePair.Value != null + ? PSObject.AsPSObject(keyValuePair.Value) + : null); } }