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/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/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 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); } } 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;