From 3690435ccdbef08ad6bd795c8e3883491fd173fe Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 25 May 2017 14:15:26 -0700 Subject: [PATCH 1/5] Move SessionPSHost*.cs to Host\EditorServicesPSHost*.cs --- .../Session/{SessionPSHost.cs => Host/EditorServicesPSHost.cs} | 0 .../EditorServicesPSHostRawUserInterface.cs} | 0 .../EditorServicesPSHostUserInterface.cs} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/PowerShellEditorServices/Session/{SessionPSHost.cs => Host/EditorServicesPSHost.cs} (100%) rename src/PowerShellEditorServices/Session/{SessionPSHostRawUserInterface.cs => Host/EditorServicesPSHostRawUserInterface.cs} (100%) rename src/PowerShellEditorServices/Session/{SessionPSHostUserInterface.cs => Host/EditorServicesPSHostUserInterface.cs} (100%) diff --git a/src/PowerShellEditorServices/Session/SessionPSHost.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs similarity index 100% rename from src/PowerShellEditorServices/Session/SessionPSHost.cs rename to src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs diff --git a/src/PowerShellEditorServices/Session/SessionPSHostRawUserInterface.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostRawUserInterface.cs similarity index 100% rename from src/PowerShellEditorServices/Session/SessionPSHostRawUserInterface.cs rename to src/PowerShellEditorServices/Session/Host/EditorServicesPSHostRawUserInterface.cs diff --git a/src/PowerShellEditorServices/Session/SessionPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs similarity index 100% rename from src/PowerShellEditorServices/Session/SessionPSHostUserInterface.cs rename to src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs From 59edcb091d8d8a5ba0bd945b0faf177ba1634a4f Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 May 2017 07:05:37 -0700 Subject: [PATCH 2/5] Add IMessageDispatcher interface --- .../MessageProtocol/MessageDispatcher.cs | 2 +- .../Server/IMessageDispatcher.cs | 17 +++++++++++++++++ .../Session/EditorSession.cs | 10 +++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs index 8ad9557c7..4ba787307 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs @@ -13,7 +13,7 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol { - public class MessageDispatcher : IMessageHandlers + public class MessageDispatcher : IMessageHandlers, IMessageDispatcher { #region Fields diff --git a/src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs b/src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs new file mode 100644 index 000000000..45342a21b --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs @@ -0,0 +1,17 @@ +// +// 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.Tasks; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol +{ + public interface IMessageDispatcher + { + Task DispatchMessage( + Message messageToDispatch, + MessageWriter messageWriter); + } +} diff --git a/src/PowerShellEditorServices/Session/EditorSession.cs b/src/PowerShellEditorServices/Session/EditorSession.cs index e72e26588..13774f59d 100644 --- a/src/PowerShellEditorServices/Session/EditorSession.cs +++ b/src/PowerShellEditorServices/Session/EditorSession.cs @@ -5,6 +5,8 @@ using Microsoft.PowerShell.EditorServices.Console; using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Protocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Templates; using Microsoft.PowerShell.EditorServices.Utility; @@ -77,12 +79,14 @@ public class EditorSession /// public bool UsesConsoleHost { get; private set; } + public IMessageDispatcher MessageDispatcher { get; private set; } + #endregion #region Constructors /// - /// + /// /// /// An ILogger implementation used for writing log messages. public EditorSession(ILogger logger) @@ -109,6 +113,8 @@ public void StartSession( this.ConsoleService = consoleService; this.UsesConsoleHost = this.ConsoleService.EnableConsoleRepl; + // Initialize all services + this.MessageDispatcher = new MessageDispatcher(this.logger); this.LanguageService = new LanguageService(this.PowerShellContext, this.logger); this.ExtensionService = new ExtensionService(this.PowerShellContext); this.TemplateService = new TemplateService(this.PowerShellContext, this.logger); @@ -137,6 +143,8 @@ public void StartDebugSession( this.PowerShellContext = powerShellContext; this.ConsoleService = consoleService; + // Initialize all services + this.MessageDispatcher = new MessageDispatcher(this.logger); this.RemoteFileManager = new RemoteFileManager(this.PowerShellContext, editorOperations, logger); this.DebugService = new DebugService(this.PowerShellContext, this.RemoteFileManager, logger); From a1bc248975d1a9b86cc5d9b727f905ad03ef12d0 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 May 2017 20:07:09 -0700 Subject: [PATCH 3/5] Establish new host interface abstraction model, remove ConsoleService This change is a fairly large refactoring of our PSHost implementation to remove the ConsoleService and IConsoleHost types and instead use two different PSHostUserInterface implementations. Here's a detailed breakdown of the changes: - Centralized host UI behavior in EditorServicesPSHostUserInterface abstract class. This class now exposes overridable abstract methods that simplify PSHostUserInterface implementations that will be used with the EditorServicesPSHost. - Removed the ConsoleService class and put its behavior and abstractions into EditorServicesPSHostUserInterface. - Created TerminalPSHostUserInterface for the integrated terminal host experience. - Created ProtocolPSHostUserInterface for the protocol-based host experience. - Removed the concept of a "prompt handler context" because each host implementation will have a single way to deal with input and choice prompts. - Lifted direct management of the console interface out of the LanguageServer and DebugAdapter classes, now managed in the EditorServicesHost. - Disabled the ConsoleServiceTests until we decide how to test for this behavior at the level of the EditorServicesPSHostUserInterface. --- .../EditorServicesHost.cs | 49 +- .../PSHost}/PromptHandlers.cs | 87 +- .../PSHost/ProtocolPSHostUserInterface.cs | 188 +++ .../MessageProtocol/MessageDispatcher.cs | 22 +- .../MessageProtocol/ProtocolEndpoint.cs | 51 +- .../Server/DebugAdapter.cs | 73 +- .../Server/LanguageServer.cs | 79 +- .../Server/OutputDebouncer.cs | 4 +- .../Console/ConsoleChoicePromptHandler.cs | 41 +- .../Console/ConsoleInputPromptHandler.cs | 48 +- .../Console/ConsolePromptHandlerContext.cs | 74 - .../Console/ConsoleService.cs | 524 ------- .../Console/IPromptHandlerContext.cs | 33 - .../Console/TerminalChoicePromptHandler.cs | 63 + .../Console/TerminalInputPromptHandler.cs | 80 + .../Session/EditorSession.cs | 34 +- .../Session/Host/EditorServicesPSHost.cs | 74 +- .../Host/EditorServicesPSHostUserInterface.cs | 926 +++++++---- .../Session/Host/IHostInput.cs | 28 + .../Host/IHostOutput.cs} | 104 +- .../SimplePSHostRawUserInterface.cs | 31 +- ...e.cs => TerminalPSHostRawUserInterface.cs} | 30 +- .../Host/TerminalPSHostUserInterface.cs | 154 ++ .../Session/PowerShellContext.cs | 32 +- .../LanguageServerTests.cs | 5 +- .../OutputReader.cs | 8 +- .../Console/ConsoleServiceTests.cs | 1348 ++++++++--------- .../PowerShellContextFactory.cs | 47 +- 28 files changed, 2117 insertions(+), 2120 deletions(-) rename src/{PowerShellEditorServices.Protocol/Server => PowerShellEditorServices.Host/PSHost}/PromptHandlers.cs (69%) create mode 100644 src/PowerShellEditorServices.Host/PSHost/ProtocolPSHostUserInterface.cs delete mode 100644 src/PowerShellEditorServices/Console/ConsolePromptHandlerContext.cs delete mode 100644 src/PowerShellEditorServices/Console/ConsoleService.cs delete mode 100644 src/PowerShellEditorServices/Console/IPromptHandlerContext.cs create mode 100644 src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs create mode 100644 src/PowerShellEditorServices/Console/TerminalInputPromptHandler.cs create mode 100644 src/PowerShellEditorServices/Session/Host/IHostInput.cs rename src/PowerShellEditorServices/{Console/IConsoleHost.cs => Session/Host/IHostOutput.cs} (57%) rename src/PowerShellEditorServices/Session/{ => Host}/SimplePSHostRawUserInterface.cs (92%) rename src/PowerShellEditorServices/Session/Host/{EditorServicesPSHostRawUserInterface.cs => TerminalPSHostRawUserInterface.cs} (92%) create mode 100644 src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 0904b3c60..a928ebb3e 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -195,6 +195,8 @@ private async void OnLanguageServiceClientConnect( CreateSession( this.hostDetails, this.profilePaths, + protocolEndpoint, + messageDispatcher, this.enableConsoleRepl); this.languageServer = @@ -264,7 +266,10 @@ private void OnDebugServiceClientConnect(object sender, TcpSocketServerChannel s this.CreateDebugSession( this.hostDetails, profilePaths, - this.languageServer?.EditorOperations); + protocolEndpoint, + messageDispatcher, + this.languageServer?.EditorOperations, + false); this.debugAdapter = new DebugAdapter( @@ -318,23 +323,29 @@ public void WaitForCompletion() private EditorSession CreateSession( HostDetails hostDetails, ProfilePaths profilePaths, + IMessageSender messageSender, + IMessageHandlers messageHandlers, bool enableConsoleRepl) { EditorSession editorSession = new EditorSession(this.logger); PowerShellContext powerShellContext = new PowerShellContext(this.logger); - ConsoleServicePSHost psHost = - new ConsoleServicePSHost( + EditorServicesPSHostUserInterface hostUserInterface = + enableConsoleRepl + ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger) + : new ProtocolPSHostUserInterface(powerShellContext, messageSender, messageHandlers, this.logger); + + EditorServicesPSHost psHost = + new EditorServicesPSHost( powerShellContext, hostDetails, - enableConsoleRepl); + hostUserInterface, + this.logger); Runspace initialRunspace = PowerShellContext.CreateRunspace(psHost); - powerShellContext.Initialize(profilePaths, initialRunspace, true, psHost.ConsoleService); + powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); - editorSession.StartSession( - powerShellContext, - psHost.ConsoleService); + editorSession.StartSession(powerShellContext, hostUserInterface); return editorSession; } @@ -342,23 +353,31 @@ private EditorSession CreateSession( private EditorSession CreateDebugSession( HostDetails hostDetails, ProfilePaths profilePaths, - IEditorOperations editorOperations) + IMessageSender messageSender, + IMessageHandlers messageHandlers, + IEditorOperations editorOperations, + bool enableConsoleRepl) { EditorSession editorSession = new EditorSession(this.logger); PowerShellContext powerShellContext = new PowerShellContext(this.logger); - ConsoleServicePSHost psHost = - new ConsoleServicePSHost( + EditorServicesPSHostUserInterface hostUserInterface = + enableConsoleRepl + ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger) + : new ProtocolPSHostUserInterface(powerShellContext, messageSender, messageHandlers, this.logger); + + EditorServicesPSHost psHost = + new EditorServicesPSHost( powerShellContext, hostDetails, - enableConsoleRepl); + hostUserInterface, + this.logger); Runspace initialRunspace = PowerShellContext.CreateRunspace(psHost); - powerShellContext.Initialize(profilePaths, initialRunspace, true, psHost.ConsoleService); + powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); editorSession.StartDebugSession( powerShellContext, - psHost.ConsoleService, editorOperations); return editorSession; diff --git a/src/PowerShellEditorServices.Protocol/Server/PromptHandlers.cs b/src/PowerShellEditorServices.Host/PSHost/PromptHandlers.cs similarity index 69% rename from src/PowerShellEditorServices.Protocol/Server/PromptHandlers.cs rename to src/PowerShellEditorServices.Host/PSHost/PromptHandlers.cs index ff6097c5d..bae68616f 100644 --- a/src/PowerShellEditorServices.Protocol/Server/PromptHandlers.cs +++ b/src/PowerShellEditorServices.Host/PSHost/PromptHandlers.cs @@ -12,51 +12,24 @@ using System.Threading; using System.Security; -namespace Microsoft.PowerShell.EditorServices.Protocol.Server +namespace Microsoft.PowerShell.EditorServices.Host { - internal class ProtocolPromptHandlerContext : IPromptHandlerContext - { - private IMessageSender messageSender; - private ConsoleService consoleService; - - public ProtocolPromptHandlerContext( - IMessageSender messageSender, - ConsoleService consoleService) - { - this.messageSender = messageSender; - this.consoleService = consoleService; - } - - public ChoicePromptHandler GetChoicePromptHandler() - { - return new ProtocolChoicePromptHandler( - this.messageSender, - this.consoleService, - Logger.CurrentLogger); - } - - public InputPromptHandler GetInputPromptHandler() - { - return new ProtocolInputPromptHandler( - this.messageSender, - this.consoleService); - } - } - internal class ProtocolChoicePromptHandler : ConsoleChoicePromptHandler { + private IHostInput hostInput; private IMessageSender messageSender; - private ConsoleService consoleService; private TaskCompletionSource readLineTask; public ProtocolChoicePromptHandler( IMessageSender messageSender, - ConsoleService consoleService, + IHostInput hostInput, + IHostOutput hostOutput, ILogger logger) - : base(consoleService, logger) + : base(hostOutput, logger) { + this.hostInput = hostInput; + this.hostOutput = hostOutput; this.messageSender = messageSender; - this.consoleService = consoleService; } protected override void ShowPrompt(PromptStyle promptStyle) @@ -93,7 +66,7 @@ private void HandlePromptResponse( if (!response.PromptCancelled) { - this.consoleService.WriteOutput( + this.hostOutput.WriteOutput( response.ResponseText, OutputType.Normal); @@ -102,7 +75,7 @@ private void HandlePromptResponse( else { // Cancel the current prompt - this.consoleService.SendControlC(); + this.hostInput.SendControlC(); } } else @@ -117,7 +90,7 @@ private void HandlePromptResponse( } // Cancel the current prompt - this.consoleService.SendControlC(); + this.hostInput.SendControlC(); } this.readLineTask = null; @@ -126,37 +99,24 @@ private void HandlePromptResponse( internal class ProtocolInputPromptHandler : ConsoleInputPromptHandler { + private IHostInput hostInput; private IMessageSender messageSender; - private ConsoleService consoleService; private TaskCompletionSource readLineTask; public ProtocolInputPromptHandler( IMessageSender messageSender, - ConsoleService consoleService) - : base( - consoleService, - Microsoft.PowerShell.EditorServices.Utility.Logger.CurrentLogger) + IHostInput hostInput, + IHostOutput hostOutput, + ILogger logger) + : base(hostOutput, logger) { + this.hostInput = hostInput; + this.hostOutput = hostOutput; this.messageSender = messageSender; - this.consoleService = consoleService; - } - - protected override void ShowErrorMessage(Exception e) - { - // Use default behavior for writing the error message - base.ShowErrorMessage(e); - } - - protected override void ShowPromptMessage(string caption, string message) - { - // Use default behavior for writing the prompt message - base.ShowPromptMessage(caption, message); } protected override void ShowFieldPrompt(FieldDetails fieldDetails) { - // Write the prompt to the console first so that there's a record - // of it occurring base.ShowFieldPrompt(fieldDetails); messageSender @@ -186,7 +146,7 @@ private void HandlePromptResponse( if (!response.PromptCancelled) { - this.consoleService.WriteOutput( + this.hostOutput.WriteOutput( response.ResponseText, OutputType.Normal); @@ -195,7 +155,7 @@ private void HandlePromptResponse( else { // Cancel the current prompt - this.consoleService.SendControlC(); + this.hostInput.SendControlC(); } } else @@ -210,11 +170,16 @@ private void HandlePromptResponse( } // Cancel the current prompt - this.consoleService.SendControlC(); + this.hostInput.SendControlC(); } this.readLineTask = null; } + + protected override Task ReadSecureString(CancellationToken cancellationToken) + { + // TODO: Write a message to the console + throw new NotImplementedException(); + } } } - diff --git a/src/PowerShellEditorServices.Host/PSHost/ProtocolPSHostUserInterface.cs b/src/PowerShellEditorServices.Host/PSHost/ProtocolPSHostUserInterface.cs new file mode 100644 index 000000000..6b413798e --- /dev/null +++ b/src/PowerShellEditorServices.Host/PSHost/ProtocolPSHostUserInterface.cs @@ -0,0 +1,188 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// 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.Protocol.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.Server; +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Host +{ + internal class ProtocolPSHostUserInterface : EditorServicesPSHostUserInterface + { + #region Private Fields + + private IMessageSender messageSender; + private OutputDebouncer outputDebouncer; + private TaskCompletionSource commandLineInputTask; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ConsoleServicePSHostUserInterface + /// class with the given IConsoleHost implementation. + /// + /// + public ProtocolPSHostUserInterface( + PowerShellContext powerShellContext, + IMessageSender messageSender, + IMessageHandlers messageHandlers, + ILogger logger) + : base(powerShellContext, new SimplePSHostRawUserInterface(logger), logger) + { + this.messageSender = messageSender; + this.outputDebouncer = new OutputDebouncer(messageSender); + + messageHandlers.SetRequestHandler( + EvaluateRequest.Type, + this.HandleEvaluateRequest); + } + + public void Dispose() + { + // TODO: Need a clear API path for this + + // Make sure remaining output is flushed before exiting + if (this.outputDebouncer != null) + { + this.outputDebouncer.Flush().Wait(); + this.outputDebouncer = null; + } + } + + #endregion + + /// + /// Writes output of the given type to the user interface with + /// the given foreground and background colors. Also includes + /// a newline if requested. + /// + /// + /// The output string to be written. + /// + /// + /// If true, a newline should be appended to the output's contents. + /// + /// + /// Specifies the type of output to be written. + /// + /// + /// Specifies the foreground color of the output to be written. + /// + /// + /// Specifies the background color of the output to be written. + /// + public override void WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor) + { + // TODO: This should use a synchronous method! + this.outputDebouncer.Invoke( + new OutputWrittenEventArgs( + outputString, + includeNewLine, + outputType, + foregroundColor, + backgroundColor)).Wait(); + } + + /// + /// Sends a progress update event to the user. + /// + /// The source ID of the progress event. + /// The details of the activity's current progress. + protected override void UpdateProgress( + long sourceId, + ProgressDetails progressDetails) + { + } + + protected override async Task ReadCommandLine(CancellationToken cancellationToken) + { + this.commandLineInputTask = new TaskCompletionSource(); + return await this.commandLineInputTask.Task; + } + + protected override InputPromptHandler OnCreateInputPromptHandler() + { + return new ProtocolInputPromptHandler(this.messageSender, this, this, this.Logger); + } + + protected override ChoicePromptHandler OnCreateChoicePromptHandler() + { + return new ProtocolChoicePromptHandler(this.messageSender, this, this, this.Logger); + } + + protected async Task HandleEvaluateRequest( + EvaluateRequestArguments evaluateParams, + RequestContext requestContext) + { + // TODO: This needs to respect debug mode! + + var evaluateResponse = + new EvaluateResponseBody + { + Result = "", + VariablesReference = 0 + }; + + if (this.commandLineInputTask != null) + { + this.commandLineInputTask.SetResult(evaluateParams.Expression); + await requestContext.SendResult(evaluateResponse); + } + else + { + // Check for special commands + if (string.Equals("!ctrlc", evaluateParams.Expression, StringComparison.CurrentCultureIgnoreCase)) + { + this.powerShellContext.AbortExecution(); + await requestContext.SendResult(evaluateResponse); + } + else if (string.Equals("!break", evaluateParams.Expression, StringComparison.CurrentCultureIgnoreCase)) + { + // TODO: Need debugger commands interface + //editorSession.DebugService.Break(); + await requestContext.SendResult(evaluateResponse); + } + else + { + // 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 + // gets blocked by something in the script like a prompt to the user. + var executeTask = + this.powerShellContext.ExecuteScriptString( + evaluateParams.Expression, + writeInputToHost: true, + writeOutputToHost: true, + addToHistory: true); + + // Return the execution result after the task completes so that the + // caller knows when command execution completed. + Task unusedTask = + executeTask.ContinueWith( + (task) => + { + // Return an empty result since the result value is irrelevant + // for this request in the LanguageServer + return + requestContext.SendResult( + evaluateResponse); + }); + } + } + } + } +} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs index 4ba787307..714857564 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs @@ -42,17 +42,8 @@ public void SetRequestHandler( RequestType requestType, Func, Task> requestHandler) { - this.SetRequestHandler( - requestType, - requestHandler, - false); - } + bool overrideExisting = true; - public void SetRequestHandler( - RequestType requestType, - Func, Task> requestHandler, - bool overrideExisting) - { if (overrideExisting) { // Remove the existing handler so a new one can be set @@ -95,17 +86,8 @@ public void SetEventHandler( NotificationType eventType, Func eventHandler) { - this.SetEventHandler( - eventType, - eventHandler, - true); - } + bool overrideExisting = true; - public void SetEventHandler( - NotificationType eventType, - Func eventHandler, - bool overrideExisting) - { if (overrideExisting) { // Remove the existing handler so a new one can be set diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs index 037d70290..66c2aba4b 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs @@ -26,9 +26,10 @@ private enum ProtocolEndpointState Shutdown } - private ProtocolEndpointState currentState; private int currentMessageId; private ChannelBase protocolChannel; + private ProtocolEndpointState currentState; + private IMessageDispatcher messageDispatcher; private AsyncContextThread messageLoopThread; private TaskCompletionSource endpointExitedTask; private SynchronizationContext originalSynchronizationContext; @@ -53,13 +54,6 @@ private bool InMessageLoopThread protected ILogger Logger { get; private set; } - /// - /// Gets the MessageDispatcher which allows registration of - /// handlers for requests, responses, and events that are - /// transmitted through the channel. - /// - private MessageDispatcher MessageDispatcher { get; set; } - /// /// Initializes an instance of the protocol server using the /// specified channel for communication. @@ -72,13 +66,14 @@ private bool InMessageLoopThread /// public ProtocolEndpoint( ChannelBase protocolChannel, - MessageDispatcher messageDispatcher, + IMessageDispatcher messageDispatcher, ILogger logger) { this.protocolChannel = protocolChannel; - this.MessageDispatcher = messageDispatcher; - this.originalSynchronizationContext = SynchronizationContext.Current; + this.messageDispatcher = messageDispatcher; this.Logger = logger; + + this.originalSynchronizationContext = SynchronizationContext.Current; } /// @@ -247,36 +242,6 @@ await this.protocolChannel.MessageWriter.WriteEvent( #region Message Handling - public void SetRequestHandler( - RequestType requestType, - Func, Task> requestHandler) - { - this.MessageDispatcher.SetRequestHandler( - requestType, - requestHandler); - } - - public void SetEventHandler( - NotificationType eventType, - Func eventHandler) - { - this.MessageDispatcher.SetEventHandler( - eventType, - eventHandler, - false); - } - - public void SetEventHandler( - NotificationType eventType, - Func eventHandler, - bool overrideExisting) - { - this.MessageDispatcher.SetEventHandler( - eventType, - eventHandler, - overrideExisting); - } - private void HandleResponse(Message responseMessage) { TaskCompletionSource pendingRequestTask = null; @@ -411,7 +376,7 @@ private async Task ListenForMessages(CancellationToken cancellationToken) else { // Process the message - await this.MessageDispatcher.DispatchMessage( + await this.messageDispatcher.DispatchMessage( newMessage, this.protocolChannel.MessageWriter); } @@ -426,7 +391,7 @@ private void OnListenTaskCompleted(Task listenTask) Logger.Write( LogLevel.Error, string.Format( - "MessageDispatcher loop terminated due to unhandled exception:\r\n\r\n{0}", + "ProtocolEndpoint message loop terminated due to unhandled exception:\r\n\r\n{0}", listenTask.Exception.ToString())); this.OnUnhandledException(listenTask.Exception); diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index 024b06dc2..675f404e9 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -23,7 +23,6 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.Server public class DebugAdapter { private EditorSession editorSession; - private OutputDebouncer outputDebouncer; private bool noDebug; private ILogger Logger; @@ -32,7 +31,6 @@ public class DebugAdapter private bool isAttachSession; private bool waitingForAttach; private string scriptToLaunch; - private bool enableConsoleRepl; private bool ownsEditorSession; private bool executionCompleted; private IMessageSender messageSender; @@ -52,7 +50,6 @@ public DebugAdapter( this.messageSender = messageSender; this.messageHandlers = messageHandlers; this.ownsEditorSession = ownsEditorSession; - this.enableConsoleRepl = editorSession.UsesConsoleHost; } public void Start() @@ -117,12 +114,6 @@ private async Task OnExecutionCompleted(Task executeTask) this.executionCompleted = true; - // Make sure remaining output is flushed before exiting - if (this.outputDebouncer != null) - { - await this.outputDebouncer.Flush(); - } - this.UnregisterEventHandlers(); if (this.isAttachSession) @@ -167,12 +158,6 @@ protected void Stop() { Logger.Write(LogLevel.Normal, "Debug adapter is shutting down..."); - // Make sure remaining output is flushed before exiting - if (this.outputDebouncer != null) - { - this.outputDebouncer.Flush().Wait(); - } - if (this.editorSession != null) { this.editorSession.PowerShellContext.RunspaceChanged -= this.powerShellContext_RunspaceChanged; @@ -325,12 +310,6 @@ protected async Task HandleLaunchRequest( // debugging session this.isInteractiveDebugSession = string.IsNullOrEmpty(this.scriptToLaunch); - if (this.editorSession.ConsoleService.EnableConsoleRepl) - { - // TODO: Write this during DebugSession init - await this.WriteUseIntegratedConsoleMessage(); - } - // Send the InitializedEvent so that the debugger will continue // sending configuration requests await this.messageSender.SendEvent( @@ -788,31 +767,13 @@ protected async Task HandleEvaluateRequest( if (isFromRepl) { - if (!this.editorSession.ConsoleService.EnableConsoleRepl) - { - // Check for special commands - if (string.Equals("!ctrlc", evaluateParams.Expression, StringComparison.CurrentCultureIgnoreCase)) - { - editorSession.PowerShellContext.AbortExecution(); - } - else if (string.Equals("!break", evaluateParams.Expression, StringComparison.CurrentCultureIgnoreCase)) - { - editorSession.DebugService.Break(); - } - else - { - // Send the input through the console service - var notAwaited = - this.editorSession - .PowerShellContext - .ExecuteScriptString(evaluateParams.Expression, false, true) - .ConfigureAwait(false); - } - } - else - { - await this.WriteUseIntegratedConsoleMessage(); - } + // TODO: Do we send the input through the command handler? + // Send the input through the console service + var notAwaited = + this.editorSession + .PowerShellContext + .ExecuteScriptString(evaluateParams.Expression, false, true) + .ConfigureAwait(false); } else { @@ -862,12 +823,6 @@ private void RegisterEventHandlers() this.editorSession.PowerShellContext.RunspaceChanged += this.powerShellContext_RunspaceChanged; this.editorSession.DebugService.DebuggerStopped += this.DebugService_DebuggerStopped; this.editorSession.PowerShellContext.DebuggerResumed += this.powerShellContext_DebuggerResumed; - - if (!this.enableConsoleRepl) - { - this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten; - this.outputDebouncer = new OutputDebouncer(this.messageSender); - } } private void UnregisterEventHandlers() @@ -875,26 +830,12 @@ private void UnregisterEventHandlers() this.editorSession.PowerShellContext.RunspaceChanged -= this.powerShellContext_RunspaceChanged; this.editorSession.DebugService.DebuggerStopped -= this.DebugService_DebuggerStopped; this.editorSession.PowerShellContext.DebuggerResumed -= this.powerShellContext_DebuggerResumed; - - if (!this.enableConsoleRepl) - { - this.editorSession.ConsoleService.OutputWritten -= this.powerShellContext_OutputWritten; - } } #endregion #region Event Handlers - private async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e) - { - if (this.outputDebouncer != null) - { - // Queue the output for writing - await this.outputDebouncer.Invoke(e); - } - } - async void DebugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) { // Provide the reason for why the debugger has stopped script execution. diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index f52b95661..978a9c080 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -8,7 +8,6 @@ using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; -using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Templates; using Microsoft.PowerShell.EditorServices.Utility; using Newtonsoft.Json.Linq; @@ -20,8 +19,6 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using DebugAdapterMessages = Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; -using System.Collections; namespace Microsoft.PowerShell.EditorServices.Protocol.Server { @@ -35,7 +32,6 @@ public class LanguageServer private EditorSession editorSession; private IMessageSender messageSender; private IMessageHandlers messageHandlers; - private OutputDebouncer outputDebouncer; private LanguageServerEditorOperations editorOperations; private LanguageServerSettings currentSettings = new LanguageServerSettings(); @@ -76,21 +72,6 @@ public LanguageServer( this.editorSession.StartDebugService(this.editorOperations); this.editorSession.DebugService.DebuggerStopped += DebugService_DebuggerStopped; - - if (!this.editorSession.ConsoleService.EnableConsoleRepl) - { - // TODO: This should be handled in ProtocolPSHost - this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten; - - // Always send console prompts through the UI in the language service - this.editorSession.ConsoleService.PushPromptHandlerContext( - new ProtocolPromptHandlerContext( - this.messageSender, - this.editorSession.ConsoleService)); - } - - // Set up the output debouncer to throttle output event writes - this.outputDebouncer = new OutputDebouncer(this.messageSender); } /// @@ -136,8 +117,6 @@ public void Start() this.messageHandlers.SetRequestHandler(NewProjectFromTemplateRequest.Type, this.HandleNewProjectFromTemplateRequest); this.messageHandlers.SetRequestHandler(GetProjectTemplatesRequest.Type, this.HandleGetProjectTemplatesRequest); - this.messageHandlers.SetRequestHandler(DebugAdapterMessages.EvaluateRequest.Type, this.HandleEvaluateRequest); - this.messageHandlers.SetRequestHandler(GetPSSARulesRequest.Type, this.HandleGetPSSARulesRequest); this.messageHandlers.SetRequestHandler(SetPSSARulesRequest.Type, this.HandleSetPSSARulesRequest); @@ -153,18 +132,13 @@ public void Start() this.editorOperations).Wait(); } - protected async Task Stop() + protected Task Stop() { - // Stop the interactive terminal - // TODO: This can happen at the host level - this.editorSession.ConsoleService.CancelReadLoop(); - - // Make sure remaining output is flushed before exiting - await this.outputDebouncer.Flush(); - Logger.Write(LogLevel.Normal, "Language service is shutting down..."); // TODO: Raise an event so that the host knows to shut down + + return Task.FromResult(true); } #region Built-in Message Handlers @@ -605,8 +579,7 @@ protected async Task HandleDidChangeConfigurationNotification( if (!this.consoleReplStarted) { // Start the interactive terminal - // TODO: This can happen at the host level - this.editorSession.ConsoleService.StartReadLoop(); + this.editorSession.HostInput.StartCommandLoop(); this.consoleReplStarted = true; } @@ -1177,44 +1150,6 @@ await requestContext.SendResult( codeActionCommands.ToArray()); } - protected Task HandleEvaluateRequest( - DebugAdapterMessages.EvaluateRequestArguments evaluateParams, - RequestContext requestContext) - { - // 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 - // gets blocked by something in the script like a prompt to the user. - var executeTask = - this.editorSession.PowerShellContext.ExecuteScriptString( - evaluateParams.Expression, - writeInputToHost: true, - writeOutputToHost: true, - addToHistory: true); - - // Return the execution result after the task completes so that the - // caller knows when command execution completed. - executeTask.ContinueWith( - (task) => - { - // Start the command loop again - // TODO: This can happen inside the PSHost - this.editorSession.ConsoleService.StartReadLoop(); - - // Return an empty result since the result value is irrelevant - // for this request in the LanguageServer - return - requestContext.SendResult( - new DebugAdapterMessages.EvaluateResponseBody - { - Result = "", - VariablesReference = 0 - }); - }); - - return Task.FromResult(true); - } - #endregion #region Event Handlers @@ -1226,12 +1161,6 @@ await this.messageSender.SendEvent( new Protocol.LanguageServer.RunspaceDetails(e.NewRunspace)); } - private async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e) - { - // Queue the output for writing - await this.outputDebouncer.Invoke(e); - } - private async void ExtensionService_ExtensionAdded(object sender, EditorCommand e) { await this.messageSender.SendEvent( diff --git a/src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs b/src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs index e65c83b1c..20866655a 100644 --- a/src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs @@ -14,7 +14,7 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.Server /// Throttles output written via OutputEvents by batching all output /// written within a short time window and writing it all out at once. /// - internal class OutputDebouncer : AsyncDebouncer + public class OutputDebouncer : AsyncDebouncer { #region Private Fields @@ -72,7 +72,7 @@ protected override async Task OnInvoke(OutputWrittenEventArgs output) // Add to string (and include newline) this.currentOutputString += output.OutputText + - (output.IncludeNewLine ? + (output.IncludeNewLine ? System.Environment.NewLine : string.Empty); } diff --git a/src/PowerShellEditorServices/Console/ConsoleChoicePromptHandler.cs b/src/PowerShellEditorServices/Console/ConsoleChoicePromptHandler.cs index 347b6232b..74b51c876 100644 --- a/src/PowerShellEditorServices/Console/ConsoleChoicePromptHandler.cs +++ b/src/PowerShellEditorServices/Console/ConsoleChoicePromptHandler.cs @@ -14,11 +14,14 @@ namespace Microsoft.PowerShell.EditorServices.Console /// Provides a standard implementation of ChoicePromptHandler /// for use in the interactive console (REPL). /// - public class ConsoleChoicePromptHandler : ChoicePromptHandler + public abstract class ConsoleChoicePromptHandler : ChoicePromptHandler { #region Private Fields - private IConsoleHost consoleHost; + /// + /// The IHostOutput instance to use for this prompt. + /// + protected IHostOutput hostOutput; #endregion @@ -27,15 +30,17 @@ public class ConsoleChoicePromptHandler : ChoicePromptHandler /// /// Creates an instance of the ConsoleChoicePromptHandler class. /// - /// - /// The IConsoleHost implementation to use for writing to the + /// + /// The IHostOutput implementation to use for writing to the /// console. /// /// An ILogger implementation used for writing log messages. - public ConsoleChoicePromptHandler(IConsoleHost consoleHost, ILogger logger) - : base(logger) + public ConsoleChoicePromptHandler( + IHostOutput hostOutput, + ILogger logger) + : base(logger) { - this.consoleHost = consoleHost; + this.hostOutput = hostOutput; } #endregion @@ -52,12 +57,12 @@ protected override void ShowPrompt(PromptStyle promptStyle) { if (this.Caption != null) { - this.consoleHost.WriteOutput(this.Caption); + this.hostOutput.WriteOutput(this.Caption); } if (this.Message != null) { - this.consoleHost.WriteOutput(this.Message); + this.hostOutput.WriteOutput(this.Message); } } @@ -68,7 +73,7 @@ protected override void ShowPrompt(PromptStyle promptStyle) choice.Label[choice.HotKeyIndex].ToString().ToUpper() : string.Empty; - this.consoleHost.WriteOutput( + this.hostOutput.WriteOutput( string.Format( "[{0}] {1} ", hotKeyString, @@ -76,7 +81,7 @@ protected override void ShowPrompt(PromptStyle promptStyle) false); } - this.consoleHost.WriteOutput("[?] Help", false); + this.hostOutput.WriteOutput("[?] Help", false); var validDefaultChoices = this.DefaultChoices.Where( @@ -90,21 +95,12 @@ protected override void ShowPrompt(PromptStyle promptStyle) this.DefaultChoices .Select(choice => this.Choices[choice].Label)); - this.consoleHost.WriteOutput( + this.hostOutput.WriteOutput( $" (default is \"{choiceString}\"): ", false); } } - /// - /// Reads an input string from the user. - /// - /// A CancellationToken that can be used to cancel the prompt. - /// A Task that can be awaited to get the user's response. - protected override Task ReadInputString(CancellationToken cancellationToken) - { - return this.consoleHost.ReadSimpleLine(cancellationToken); - } /// /// Implements behavior to handle the user's response. @@ -121,7 +117,7 @@ protected override int[] HandleResponse(string responseString) // Print help text foreach (var choice in this.Choices) { - this.consoleHost.WriteOutput( + this.hostOutput.WriteOutput( string.Format( "{0} - {1}", (choice.HotKeyCharacter.HasValue ? @@ -137,4 +133,3 @@ protected override int[] HandleResponse(string responseString) } } } - diff --git a/src/PowerShellEditorServices/Console/ConsoleInputPromptHandler.cs b/src/PowerShellEditorServices/Console/ConsoleInputPromptHandler.cs index 672c227a5..8124633a7 100644 --- a/src/PowerShellEditorServices/Console/ConsoleInputPromptHandler.cs +++ b/src/PowerShellEditorServices/Console/ConsoleInputPromptHandler.cs @@ -15,11 +15,14 @@ namespace Microsoft.PowerShell.EditorServices.Console /// Provides a standard implementation of InputPromptHandler /// for use in the interactive console (REPL). /// - public class ConsoleInputPromptHandler : InputPromptHandler + public abstract class ConsoleInputPromptHandler : InputPromptHandler { #region Private Fields - private IConsoleHost consoleHost; + /// + /// The IHostOutput instance to use for this prompt. + /// + protected IHostOutput hostOutput; #endregion @@ -28,15 +31,17 @@ public class ConsoleInputPromptHandler : InputPromptHandler /// /// Creates an instance of the ConsoleInputPromptHandler class. /// - /// - /// The IConsoleHost implementation to use for writing to the + /// + /// The IHostOutput implementation to use for writing to the /// console. /// /// An ILogger implementation used for writing log messages. - public ConsoleInputPromptHandler(IConsoleHost consoleHost, ILogger logger) - : base(logger) + public ConsoleInputPromptHandler( + IHostOutput hostOutput, + ILogger logger) + : base(logger) { - this.consoleHost = consoleHost; + this.hostOutput = hostOutput; } #endregion @@ -53,12 +58,12 @@ protected override void ShowPromptMessage(string caption, string message) { if (!string.IsNullOrEmpty(caption)) { - this.consoleHost.WriteOutput(caption, true); + this.hostOutput.WriteOutput(caption, true); } if (!string.IsNullOrEmpty(message)) { - this.consoleHost.WriteOutput(message, true); + this.hostOutput.WriteOutput(message, true); } } @@ -73,7 +78,7 @@ protected override void ShowFieldPrompt(FieldDetails fieldDetails) // In this case don't write anything if (!string.IsNullOrEmpty(fieldDetails.Name)) { - this.consoleHost.WriteOutput( + this.hostOutput.WriteOutput( fieldDetails.Name + ": ", false); } @@ -89,33 +94,12 @@ protected override void ShowFieldPrompt(FieldDetails fieldDetails) /// protected override void ShowErrorMessage(Exception e) { - this.consoleHost.WriteOutput( + this.hostOutput.WriteOutput( e.Message, true, OutputType.Error); } - /// - /// Reads an input string from the user. - /// - /// A CancellationToken that can be used to cancel the prompt. - /// A Task that can be awaited to get the user's response. - protected override Task ReadInputString(CancellationToken cancellationToken) - { - return this.consoleHost.ReadSimpleLine(cancellationToken); - } - - /// - /// Reads a SecureString from the user. - /// - /// A CancellationToken that can be used to cancel the prompt. - /// A Task that can be awaited to get the user's response. - protected override Task ReadSecureString(CancellationToken cancellationToken) - { - return this.consoleHost.ReadSecureLine(cancellationToken); - } - #endregion } } - diff --git a/src/PowerShellEditorServices/Console/ConsolePromptHandlerContext.cs b/src/PowerShellEditorServices/Console/ConsolePromptHandlerContext.cs deleted file mode 100644 index f1c1a4808..000000000 --- a/src/PowerShellEditorServices/Console/ConsolePromptHandlerContext.cs +++ /dev/null @@ -1,74 +0,0 @@ -// -// 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 Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Provides a standard IPromptHandlerContext implementation for - /// use in the interactive console (REPL). - /// - public class ConsolePromptHandlerContext : IPromptHandlerContext - { - #region Private Fields - - private ILogger logger; - private IConsoleHost consoleHost; - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the ConsolePromptHandlerContext - /// class. - /// - /// - /// The IConsoleHost implementation to use for writing to the - /// console. - /// - /// An ILogger implementation used for writing log messages. - public ConsolePromptHandlerContext( - IConsoleHost consoleHost, - ILogger logger) - { - this.consoleHost = consoleHost; - this.logger = logger; - } - - #endregion - - #region Public Methods - - /// - /// Creates a new ChoicePromptHandler instance so that - /// the caller can display a choice prompt to the user. - /// - /// - /// A new ChoicePromptHandler instance. - /// - public ChoicePromptHandler GetChoicePromptHandler() - { - return new ConsoleChoicePromptHandler(this.consoleHost, this.logger); - } - - /// - /// Creates a new InputPromptHandler instance so that - /// the caller can display an input prompt to the user. - /// - /// - /// A new InputPromptHandler instance. - /// - public InputPromptHandler GetInputPromptHandler() - { - return new ConsoleInputPromptHandler(this.consoleHost, this.logger); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Console/ConsoleService.cs b/src/PowerShellEditorServices/Console/ConsoleService.cs deleted file mode 100644 index 60184f93b..000000000 --- a/src/PowerShellEditorServices/Console/ConsoleService.cs +++ /dev/null @@ -1,524 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using Microsoft.PowerShell.EditorServices.Utility; -using System.Collections.Generic; -using System.Threading.Tasks; -using System.Threading; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - using Microsoft.PowerShell.EditorServices.Session; - using System; - using System.Globalization; - using System.Linq; - using System.Management.Automation; - using System.Security; - - /// - /// Provides a high-level service for exposing an interactive - /// PowerShell console (REPL) to the user. - /// - public class ConsoleService : IConsoleHost - { - #region Fields - - private bool isReadLoopStarted; - private ConsoleReadLine consoleReadLine; - private PowerShellContext powerShellContext; - - CancellationTokenSource readLineCancellationToken; - - private PromptHandler activePromptHandler; - private Stack promptHandlerContextStack = - new Stack(); - - #endregion - - #region Properties - - /// - /// Gets or sets a boolean determining whether the console (terminal) - /// REPL should be used in this session. - /// - public bool EnableConsoleRepl { get; set; } - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the ConsoleService class. - /// - /// - /// The PowerShellContext that will be used for executing commands - /// against a runspace. - /// - public ConsoleService(PowerShellContext powerShellContext) - : this(powerShellContext, null) - { - } - - /// - /// Creates a new instance of the ConsoleService class. - /// - /// - /// The PowerShellContext that will be used for executing commands - /// against a runspace. - /// - /// - /// The default IPromptHandlerContext implementation to use for - /// displaying prompts to the user. - /// - public ConsoleService( - PowerShellContext powerShellContext, - IPromptHandlerContext defaultPromptHandlerContext) - { - // Register this instance as the IConsoleHost for the PowerShellContext - this.powerShellContext = powerShellContext; - 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 - if (defaultPromptHandlerContext == null) - { - defaultPromptHandlerContext = - new ConsolePromptHandlerContext(this, Logger.CurrentLogger); - } - - this.promptHandlerContextStack.Push( - defaultPromptHandlerContext); - - this.consoleReadLine = new ConsoleReadLine(powerShellContext); - - } - - #endregion - - #region Public Methods - - /// - /// Starts a terminal-based interactive console loop in the current process. - /// - public void StartReadLoop() - { - if (this.EnableConsoleRepl) - { - this.isReadLoopStarted = true; - this.InnerStartReadLoop(); - } - } - - /// - /// Cancels an active read loop. - /// - public void CancelReadLoop() - { - this.isReadLoopStarted = false; - this.InnerCancelReadLoop(); - } - - /// - /// Executes a script file at the specified path. - /// - /// The path to the script file to execute. - /// Arguments to pass to the script. - /// A Task that can be awaited for completion. - public async Task ExecuteScriptAtPath(string scriptPath, string arguments = null) - { - this.InnerCancelReadLoop(); - - // If we don't escape wildcard characters in the script path, the script can - // fail to execute if say the script name was foo][.ps1. - // Related to issue #123. - string escapedScriptPath = PowerShellContext.EscapePath(scriptPath, escapeSpaces: true); - - await this.powerShellContext.ExecuteScriptString( - $"{escapedScriptPath} {arguments}", - true, - true, - false); - - this.InnerStartReadLoop(); - } - - /// - /// Pushes a new IPromptHandlerContext onto the stack. This - /// is used when a prompt handler context is only needed for - /// a short series of command executions. - /// - /// - /// The IPromptHandlerContext instance to push onto the stack. - /// - public void PushPromptHandlerContext(IPromptHandlerContext promptHandlerContext) - { - // Push a new prompt handler factory for future prompts - this.promptHandlerContextStack.Push(promptHandlerContext); - } - - /// - /// Pops the most recent IPromptHandlerContext from the stack. - /// This is called when execution requiring a specific type of - /// prompt has completed and the previous prompt handler context - /// should be restored. - /// - public void PopPromptHandlerContext() - { - // The last item on the stack is the default handler, never pop it - if (this.promptHandlerContextStack.Count > 1) - { - this.promptHandlerContextStack.Pop(); - } - } - - /// - /// Cancels the currently executing command or prompt. - /// - public void SendControlC() - { - if (this.activePromptHandler != null) - { - this.activePromptHandler.CancelPrompt(); - } - else - { - // Cancel the current execution - this.powerShellContext.AbortExecution(); - } - } - - /// - /// Reads an input string from the user. - /// - /// A CancellationToken that can be used to cancel the prompt. - /// A Task that can be awaited to get the user's response. - public async Task ReadSimpleLine(CancellationToken cancellationToken) - { - string inputLine = await this.consoleReadLine.ReadSimpleLine(cancellationToken); - this.WriteOutput(string.Empty, true); - return inputLine; - } - - /// - /// Reads a SecureString from the user. - /// - /// A CancellationToken that can be used to cancel the prompt. - /// A Task that can be awaited to get the user's response. - public async Task ReadSecureLine(CancellationToken cancellationToken) - { - SecureString secureString = await this.consoleReadLine.ReadSecureLine(cancellationToken); - this.WriteOutput(string.Empty, true); - return secureString; - } - - #endregion - - #region Private Methods - - private async Task WritePromptStringToHost() - { - PSCommand promptCommand = new PSCommand().AddScript("prompt"); - - string promptString = - (await this.powerShellContext.ExecuteCommand(promptCommand, false, false)) - .Select(pso => pso.BaseObject) - .OfType() - .FirstOrDefault() ?? "PS> "; - - // Add the [DBG] prefix if we're stopped in the debugger - if (this.powerShellContext.IsDebuggerStopped) - { - promptString = - string.Format( - CultureInfo.InvariantCulture, - "[DBG]: {0}", - promptString); - } - - // Update the stored prompt string if the session is remote - if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote) - { - promptString = - string.Format( - CultureInfo.InvariantCulture, - "[{0}]: {1}", - this.powerShellContext.CurrentRunspace.Runspace.ConnectionInfo != null - ? this.powerShellContext.CurrentRunspace.Runspace.ConnectionInfo.ComputerName - : this.powerShellContext.CurrentRunspace.SessionDetails.ComputerName, - promptString); - } - - // Write the prompt string - this.WriteOutput(promptString, false); - } - - private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs) - { - // TODO: What do we display when we don't know why we stopped? - - if (eventArgs.Breakpoints.Count > 0) - { - // The breakpoint classes have nice ToString output so use that - this.WriteOutput( - Environment.NewLine + $"Hit {eventArgs.Breakpoints[0].ToString()}\n", - true, - OutputType.Normal, - ConsoleColor.Blue); - } - } - - private void InnerStartReadLoop() - { - if (this.EnableConsoleRepl) - { - if (this.readLineCancellationToken == null) - { - this.readLineCancellationToken = new CancellationTokenSource(); - - var terminalThreadTask = - Task.Factory.StartNew( - async () => - { - await this.StartReplLoop(this.readLineCancellationToken.Token); - }); - } - else - { - Logger.CurrentLogger.Write(LogLevel.Verbose, "InnerStartReadLoop called while read loop is already running"); - } - } - } - - private void InnerCancelReadLoop() - { - if (this.readLineCancellationToken != null) - { - // Set this to false so that Ctrl+C isn't trapped by any - // lingering ReadKey - Console.TreatControlCAsInput = false; - - this.readLineCancellationToken.Cancel(); - this.readLineCancellationToken = null; - } - } - - private async Task StartReplLoop(CancellationToken cancellationToken) - { - do - { - string commandString = null; - - await this.WritePromptStringToHost(); - - try - { - commandString = - await this.consoleReadLine.ReadCommandLine( - cancellationToken); - } - catch (PipelineStoppedException) - { - this.WriteOutput( - "^C", - true, - OutputType.Normal, - foregroundColor: ConsoleColor.Red); - } - catch (TaskCanceledException) - { - // Do nothing here, the while loop condition will exit. - } - catch (Exception e) // Narrow this if possible - { - this.WriteOutput( - $"\n\nAn error occurred while reading input:\n\n{e.ToString()}\n", - true, - OutputType.Error); - - Logger.CurrentLogger.WriteException("Caught exception while reading command line", e); - } - - if (commandString != null) - { - Console.Write(Environment.NewLine); - - if (!string.IsNullOrWhiteSpace(commandString)) - { - var unusedTask = - this.powerShellContext - .ExecuteScriptString( - commandString, - false, - true, - true) - .ConfigureAwait(false); - - break; - } - } - } - while (!cancellationToken.IsCancellationRequested); - } - - #endregion - - #region Events - - /// - /// An event that is raised when textual output of any type is - /// written to the session. - /// - public event EventHandler OutputWritten; - - #endregion - - #region IConsoleHost Implementation - - void IConsoleHost.WriteOutput(string outputString, bool includeNewLine, OutputType outputType, ConsoleColor foregroundColor, ConsoleColor backgroundColor) - { - if (this.EnableConsoleRepl) - { - ConsoleColor oldForegroundColor = Console.ForegroundColor; - ConsoleColor oldBackgroundColor = Console.BackgroundColor; - - Console.ForegroundColor = foregroundColor; - Console.BackgroundColor = backgroundColor; - - Console.Write(outputString + (includeNewLine ? Environment.NewLine : "")); - - Console.ForegroundColor = oldForegroundColor; - Console.BackgroundColor = oldBackgroundColor; - } - else - { - this.OutputWritten?.Invoke( - this, - new OutputWrittenEventArgs( - outputString, - includeNewLine, - outputType, - foregroundColor, - backgroundColor)); - } - } - - void IConsoleHost.UpdateProgress(long sourceId, ProgressDetails progressDetails) - { - //throw new NotImplementedException(); - } - - void IConsoleHost.ExitSession(int exitCode) - { - //throw new NotImplementedException(); - } - - ChoicePromptHandler IConsoleHost.GetChoicePromptHandler() - { - return this.GetPromptHandler( - factory => factory.GetChoicePromptHandler()); - } - - InputPromptHandler IConsoleHost.GetInputPromptHandler() - { - return this.GetPromptHandler( - factory => factory.GetInputPromptHandler()); - } - - private TPromptHandler GetPromptHandler( - Func factoryInvoker) - where TPromptHandler : PromptHandler - { - if (this.activePromptHandler != null) - { - Logger.CurrentLogger.Write( - LogLevel.Error, - "Prompt handler requested while another prompt is already active."); - } - - // Get the topmost prompt handler factory - IPromptHandlerContext promptHandlerContext = - this.promptHandlerContextStack.Peek(); - - TPromptHandler promptHandler = factoryInvoker(promptHandlerContext); - this.activePromptHandler = promptHandler; - this.activePromptHandler.PromptCancelled += activePromptHandler_PromptCancelled; - - return promptHandler; - } - - #endregion - - #region Event Handlers - - private void activePromptHandler_PromptCancelled(object sender, EventArgs e) - { - // Clean up the existing prompt - this.activePromptHandler.PromptCancelled -= activePromptHandler_PromptCancelled; - this.activePromptHandler = null; - } - - private void PowerShellContext_DebuggerStop(object sender, System.Management.Automation.DebuggerStopEventArgs e) - { - // Cancel any existing prompt first - this.InnerCancelReadLoop(); - - this.WriteDebuggerBanner(e); - this.InnerStartReadLoop(); - } - - private void PowerShellContext_DebuggerResumed(object sender, System.Management.Automation.DebuggerResumeAction e) - { - this.InnerCancelReadLoop(); - } - - private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionStatusChangedEventArgs eventArgs) - { - if (this.EnableConsoleRepl && this.isReadLoopStarted) - { - if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted) - { - // When aborted, cancel any lingering prompts - if (this.activePromptHandler != null) - { - this.activePromptHandler.CancelPrompt(); - this.WriteOutput(string.Empty); - } - } - 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) - { - // Execution has completed, start the input prompt - this.InnerStartReadLoop(); - } - else - { - // A new command was started, cancel the input prompt - this.InnerCancelReadLoop(); - this.WriteOutput(string.Empty); - } - } - else if ( - eventArgs.ExecutionOptions.WriteErrorsToHost && - (eventArgs.ExecutionStatus == ExecutionStatus.Failed || - eventArgs.HadErrors)) - { - this.InnerCancelReadLoop(); - this.WriteOutput(string.Empty); - this.InnerStartReadLoop(); - } - } - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Console/IPromptHandlerContext.cs b/src/PowerShellEditorServices/Console/IPromptHandlerContext.cs deleted file mode 100644 index f967ee0d8..000000000 --- a/src/PowerShellEditorServices/Console/IPromptHandlerContext.cs +++ /dev/null @@ -1,33 +0,0 @@ -// -// 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.Console -{ - /// - /// Defines an interface for requesting prompt handlers in - /// a given user interface context. - /// - public interface IPromptHandlerContext - { - /// - /// Creates a new ChoicePromptHandler instance so that - /// the caller can display a choice prompt to the user. - /// - /// - /// A new ChoicePromptHandler instance. - /// - ChoicePromptHandler GetChoicePromptHandler(); - - /// - /// Creates a new InputPromptHandler instance so that - /// the caller can display an input prompt to the user. - /// - /// - /// A new InputPromptHandler instance. - /// - InputPromptHandler GetInputPromptHandler(); - } -} - diff --git a/src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs b/src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs new file mode 100644 index 000000000..b99b959cd --- /dev/null +++ b/src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs @@ -0,0 +1,63 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides a standard implementation of ChoicePromptHandler + /// for use in the interactive console (REPL). + /// + internal class TerminalChoicePromptHandler : ConsoleChoicePromptHandler + { + #region Private Fields + + private ConsoleReadLine consoleReadLine; + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ConsoleChoicePromptHandler class. + /// + /// + /// The ConsoleReadLine instance to use for interacting with the terminal. + /// + /// + /// The IHostOutput implementation to use for writing to the + /// console. + /// + /// An ILogger implementation used for writing log messages. + public TerminalChoicePromptHandler( + ConsoleReadLine consoleReadLine, + IHostOutput hostOutput, + ILogger logger) + : base(hostOutput, logger) + { + this.hostOutput = hostOutput; + this.consoleReadLine = consoleReadLine; + } + + #endregion + + /// + /// Reads an input string from the user. + /// + /// A CancellationToken that can be used to cancel the prompt. + /// A Task that can be awaited to get the user's response. + protected override async Task ReadInputString(CancellationToken cancellationToken) + { + string inputString = await this.consoleReadLine.ReadSimpleLine(cancellationToken); + this.hostOutput.WriteOutput(string.Empty); + + return inputString; + } + } +} diff --git a/src/PowerShellEditorServices/Console/TerminalInputPromptHandler.cs b/src/PowerShellEditorServices/Console/TerminalInputPromptHandler.cs new file mode 100644 index 000000000..e77bf2f9a --- /dev/null +++ b/src/PowerShellEditorServices/Console/TerminalInputPromptHandler.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; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides a standard implementation of InputPromptHandler + /// for use in the interactive console (REPL). + /// + internal class TerminalInputPromptHandler : ConsoleInputPromptHandler + { + #region Private Fields + + private ConsoleReadLine consoleReadLine; + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ConsoleInputPromptHandler class. + /// + /// + /// The ConsoleReadLine instance to use for interacting with the terminal. + /// + /// + /// The IHostOutput implementation to use for writing to the + /// console. + /// + /// An ILogger implementation used for writing log messages. + public TerminalInputPromptHandler( + ConsoleReadLine consoleReadLine, + IHostOutput hostOutput, + ILogger logger) + : base(hostOutput, logger) + { + this.consoleReadLine = consoleReadLine; + } + + #endregion + + #region Public Methods + + /// + /// Reads an input string from the user. + /// + /// A CancellationToken that can be used to cancel the prompt. + /// A Task that can be awaited to get the user's response. + protected override async Task ReadInputString(CancellationToken cancellationToken) + { + string inputString = await this.consoleReadLine.ReadSimpleLine(cancellationToken); + this.hostOutput.WriteOutput(string.Empty); + + return inputString; + } + + /// + /// Reads a SecureString from the user. + /// + /// A CancellationToken that can be used to cancel the prompt. + /// A Task that can be awaited to get the user's response. + protected override async Task ReadSecureString(CancellationToken cancellationToken) + { + SecureString secureString = await this.consoleReadLine.ReadSecureLine(cancellationToken); + this.hostOutput.WriteOutput(string.Empty); + + return secureString; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Session/EditorSession.cs b/src/PowerShellEditorServices/Session/EditorSession.cs index 13774f59d..6c00581b4 100644 --- a/src/PowerShellEditorServices/Session/EditorSession.cs +++ b/src/PowerShellEditorServices/Session/EditorSession.cs @@ -5,8 +5,6 @@ using Microsoft.PowerShell.EditorServices.Console; using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Protocol; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Templates; using Microsoft.PowerShell.EditorServices.Utility; @@ -28,6 +26,11 @@ public class EditorSession #region Properties + /// + /// Gets the IHostInput implementation to use for this session. + /// + public IHostInput HostInput { get; private set; } + /// /// Gets the Workspace instance for this session. /// @@ -53,11 +56,6 @@ public class EditorSession /// public DebugService DebugService { get; private set; } - /// - /// Gets the ConsoleService instance for this session. - /// - public ConsoleService ConsoleService { get; private set; } - /// /// Gets the ExtensionService instance for this session. /// @@ -73,14 +71,6 @@ public class EditorSession /// public RemoteFileManager RemoteFileManager { get; private set; } - /// - /// Gets a boolean which is true if the integrated console host is - /// active in this session. - /// - public bool UsesConsoleHost { get; private set; } - - public IMessageDispatcher MessageDispatcher { get; private set; } - #endregion #region Constructors @@ -103,18 +93,15 @@ public EditorSession(ILogger logger) /// for the ConsoleService. /// /// - /// + /// public void StartSession( PowerShellContext powerShellContext, - ConsoleService consoleService) + IHostInput hostInput) { - // Initialize all services this.PowerShellContext = powerShellContext; - this.ConsoleService = consoleService; - this.UsesConsoleHost = this.ConsoleService.EnableConsoleRepl; + this.HostInput = hostInput; // Initialize all services - this.MessageDispatcher = new MessageDispatcher(this.logger); this.LanguageService = new LanguageService(this.PowerShellContext, this.logger); this.ExtensionService = new ExtensionService(this.PowerShellContext); this.TemplateService = new TemplateService(this.PowerShellContext, this.logger); @@ -130,21 +117,16 @@ public void StartSession( /// for the ConsoleService. /// /// - /// /// /// An IEditorOperations implementation used to interact with the editor. /// public void StartDebugSession( PowerShellContext powerShellContext, - ConsoleService consoleService, IEditorOperations editorOperations) { - // Initialize all services this.PowerShellContext = powerShellContext; - this.ConsoleService = consoleService; // Initialize all services - this.MessageDispatcher = new MessageDispatcher(this.logger); this.RemoteFileManager = new RemoteFileManager(this.PowerShellContext, editorOperations, logger); this.DebugService = new DebugService(this.PowerShellContext, this.RemoteFileManager, logger); diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs index cc54438ec..1210f8284 100644 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs +++ b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs @@ -17,38 +17,18 @@ namespace Microsoft.PowerShell.EditorServices /// ConsoleService and routes its calls to an IConsoleHost /// implementation. /// - public class ConsoleServicePSHost : PSHost, IHostSupportsInteractiveSession + public class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession { #region Private Fields + private ILogger Logger; private HostDetails hostDetails; - private IConsoleHost consoleHost; - private bool isNativeApplicationRunning; private Guid instanceId = Guid.NewGuid(); - private ConsoleServicePSHostUserInterface hostUserInterface; + private EditorServicesPSHostUserInterface hostUserInterface; private IHostSupportsInteractiveSession hostSupportsInteractiveSession; #endregion - #region Properties - - internal IConsoleHost ConsoleHost - { - get { return this.consoleHost; } - set - { - this.consoleHost = value; - this.hostUserInterface.ConsoleHost = value; - } - } - - /// - /// Gets the ConsoleServices owned by this host. - /// - public ConsoleService ConsoleService { get; private set; } - - #endregion - #region Constructors /// @@ -61,35 +41,20 @@ internal IConsoleHost ConsoleHost /// /// Provides details about the host application. /// - /// - /// Enables a terminal-based REPL for this session. + /// + /// The EditorServicesPSHostUserInterface implementation to use for this host. /// - public ConsoleServicePSHost( + /// An ILogger implementation to use for this host. + public EditorServicesPSHost( PowerShellContext powerShellContext, HostDetails hostDetails, - bool enableConsoleRepl) + EditorServicesPSHostUserInterface hostUserInterface, + ILogger logger) { + this.Logger = logger; this.hostDetails = hostDetails; - this.hostUserInterface = new ConsoleServicePSHostUserInterface(enableConsoleRepl); + this.hostUserInterface = hostUserInterface; this.hostSupportsInteractiveSession = powerShellContext; - - this.ConsoleService = new ConsoleService(powerShellContext); - this.ConsoleService.EnableConsoleRepl = enableConsoleRepl; - this.ConsoleHost = this.ConsoleService; - - System.Console.CancelKeyPress += - (obj, args) => - { - if (!this.isNativeApplicationRunning) - { - // We'll handle Ctrl+C - if (this.ConsoleHost != null) - { - args.Cancel = true; - this.consoleHost.SendControlC(); - } - } - }; } #endregion @@ -151,7 +116,7 @@ public override PSHostUserInterface UI /// public override void EnterNestedPrompt() { - Logger.CurrentLogger.Write(LogLevel.Verbose, "EnterNestedPrompt() called."); + Logger.Write(LogLevel.Verbose, "EnterNestedPrompt() called."); } /// @@ -159,7 +124,7 @@ public override void EnterNestedPrompt() /// public override void ExitNestedPrompt() { - Logger.CurrentLogger.Write(LogLevel.Verbose, "ExitNestedPrompt() called."); + Logger.Write(LogLevel.Verbose, "ExitNestedPrompt() called."); } /// @@ -167,8 +132,8 @@ public override void ExitNestedPrompt() /// public override void NotifyBeginApplication() { - Logger.CurrentLogger.Write(LogLevel.Verbose, "NotifyBeginApplication() called."); - this.isNativeApplicationRunning = true; + Logger.Write(LogLevel.Verbose, "NotifyBeginApplication() called."); + this.hostUserInterface.IsNativeApplicationRunning = true; } /// @@ -176,8 +141,8 @@ public override void NotifyBeginApplication() /// public override void NotifyEndApplication() { - Logger.CurrentLogger.Write(LogLevel.Verbose, "NotifyEndApplication() called."); - this.isNativeApplicationRunning = false; + Logger.Write(LogLevel.Verbose, "NotifyEndApplication() called."); + this.hostUserInterface.IsNativeApplicationRunning = false; } /// @@ -186,11 +151,6 @@ public override void NotifyEndApplication() /// public override void SetShouldExit(int exitCode) { - if (this.consoleHost != null) - { - this.consoleHost.ExitSession(exitCode); - } - if (this.IsRunspacePushed) { this.PopRunspace(); diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs index 968a98110..a818c9446 100644 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs @@ -14,6 +14,8 @@ using Microsoft.PowerShell.EditorServices.Console; using System.Threading; using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.PowerShell.EditorServices.Session; +using System.Globalization; namespace Microsoft.PowerShell.EditorServices { @@ -22,38 +24,65 @@ namespace Microsoft.PowerShell.EditorServices /// for the ConsoleService and routes its calls to an IConsoleHost /// implementation. /// - internal class ConsoleServicePSHostUserInterface : PSHostUserInterface, IHostUISupportsMultipleChoiceSelection + public abstract class EditorServicesPSHostUserInterface : + PSHostUserInterface, + IHostInput, + IHostOutput, + IHostUISupportsMultipleChoiceSelection { #region Private Fields - private IConsoleHost consoleHost; + private PromptHandler activePromptHandler; private PSHostRawUserInterface rawUserInterface; + private CancellationTokenSource commandLoopCancellationToken; + + /// + /// The PowerShellContext to use for executing commands. + /// + protected PowerShellContext powerShellContext; #endregion #region Public Constants + /// + /// Gets a const string for the console's debug message prefix. + /// public const string DebugMessagePrefix = "DEBUG: "; + + /// + /// Gets a const string for the console's warning message prefix. + /// public const string WarningMessagePrefix = "WARNING: "; + + /// + /// Gets a const string for the console's verbose message prefix. + /// public const string VerboseMessagePrefix = "VERBOSE: "; #endregion #region Properties - internal IConsoleHost ConsoleHost - { - get { return this.consoleHost; } - set - { - this.consoleHost = value; - } - } - #if !PowerShellv3 && !PowerShellv4 && !PowerShellv5r1 // Only available in Windows 10 Update 1 or higher + /// + /// Returns true if the host supports VT100 output codes. + /// public override bool SupportsVirtualTerminal => true; #endif + /// + /// Returns true if a native application is currently running. + /// + public bool IsNativeApplicationRunning { get; internal set; } + + private bool IsCommandLoopRunning { get; set; } + + /// + /// Gets the ILogger implementation used for this host. + /// + protected ILogger Logger { get; private set; } + #endregion #region Constructors @@ -62,120 +91,269 @@ internal IConsoleHost ConsoleHost /// Creates a new instance of the ConsoleServicePSHostUserInterface /// class with the given IConsoleHost implementation. /// - public ConsoleServicePSHostUserInterface(bool enableConsoleRepl) + /// The PowerShellContext to use for executing commands. + /// The PSHostRawUserInterface implementation to use for this host. + /// An ILogger implementation to use for this host. + public EditorServicesPSHostUserInterface( + PowerShellContext powerShellContext, + PSHostRawUserInterface rawUserInterface, + ILogger logger) { - if (enableConsoleRepl) + this.Logger = logger; + this.powerShellContext = powerShellContext; + this.rawUserInterface = rawUserInterface; + + this.powerShellContext.DebuggerStop += PowerShellContext_DebuggerStop; + this.powerShellContext.DebuggerResumed += PowerShellContext_DebuggerResumed; + this.powerShellContext.ExecutionStatusChanged += PowerShellContext_ExecutionStatusChanged; + } + + #endregion + + #region Public Methods + + void IHostInput.StartCommandLoop() + { + if (!this.IsCommandLoopRunning) { - // Set the output encoding to UTF-8 so that special - // characters are written to the console correctly - System.Console.OutputEncoding = System.Text.Encoding.UTF8; + this.IsCommandLoopRunning = true; + this.ShowCommandPrompt(); } + } - this.rawUserInterface = - enableConsoleRepl - ? (PSHostRawUserInterface)new ConsoleServicePSHostRawUserInterface() - : new SimplePSHostRawUserInterface(); + void IHostInput.StopCommandLoop() + { + if (this.IsCommandLoopRunning) + { + this.IsCommandLoopRunning = false; + this.CancelCommandPrompt(); + } + } + + private void ShowCommandPrompt() + { + if (this.commandLoopCancellationToken == null) + { + this.commandLoopCancellationToken = new CancellationTokenSource(); + + var commandLoopThreadTask = + Task.Factory.StartNew( + async () => + { + await this.StartReplLoop(this.commandLoopCancellationToken.Token); + }); + } + else + { + Logger.Write(LogLevel.Verbose, "StartReadLoop called while read loop is already running"); + } + } + + private void CancelCommandPrompt() + { + if (this.commandLoopCancellationToken != null) + { + // Set this to false so that Ctrl+C isn't trapped by any + // lingering ReadKey + // TOOD: Move this to Terminal impl! + //Console.TreatControlCAsInput = false; + + this.commandLoopCancellationToken.Cancel(); + this.commandLoopCancellationToken = null; + } + } + + /// + /// Cancels the currently executing command or prompt. + /// + public void SendControlC() + { + if (this.activePromptHandler != null) + { + this.activePromptHandler.CancelPrompt(); + } + else + { + // Cancel the current execution + this.powerShellContext.AbortExecution(); + } } #endregion + #region Abstract Methods + + /// + /// Requests that the HostUI implementation read a command line + /// from the user to be executed in the integrated console command + /// loop. + /// + /// + /// A CancellationToken used to cancel the command line request. + /// + /// A Task that can be awaited for the resulting input string. + protected abstract Task ReadCommandLine(CancellationToken cancellationToken); + + /// + /// Creates an InputPrompt handle to use for displaying input + /// prompts to the user. + /// + /// A new InputPromptHandler instance. + protected abstract InputPromptHandler OnCreateInputPromptHandler(); + + /// + /// Creates a ChoicePromptHandler to use for displaying a + /// choice prompt to the user. + /// + /// A new ChoicePromptHandler instance. + protected abstract ChoicePromptHandler OnCreateChoicePromptHandler(); + + /// + /// Writes output of the given type to the user interface with + /// the given foreground and background colors. Also includes + /// a newline if requested. + /// + /// + /// The output string to be written. + /// + /// + /// If true, a newline should be appended to the output's contents. + /// + /// + /// Specifies the type of output to be written. + /// + /// + /// Specifies the foreground color of the output to be written. + /// + /// + /// Specifies the background color of the output to be written. + /// + public abstract void WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor); + + /// + /// Sends a progress update event to the user. + /// + /// The source ID of the progress event. + /// The details of the activity's current progress. + protected abstract void UpdateProgress( + long sourceId, + ProgressDetails progressDetails); + + #endregion + + #region IHostInput Implementation + + #endregion + #region PSHostUserInterface Implementation + /// + /// + /// + /// + /// + /// + /// public override Dictionary Prompt( string promptCaption, string promptMessage, Collection fieldDescriptions) { - if (this.consoleHost != null) + FieldDetails[] fields = + fieldDescriptions + .Select(f => { return FieldDetails.Create(f, this.Logger); }) + .ToArray(); + + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + Task> promptTask = + this.CreateInputPromptHandler() + .PromptForInput( + promptCaption, + promptMessage, + fields, + cancellationToken.Token); + + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "Prompt", + cancellationToken); + + // Convert all values to PSObjects + var psObjectDict = new Dictionary(); + + // The result will be null if the prompt was cancelled + if (promptTask.Result != null) { - FieldDetails[] fields = - fieldDescriptions - .Select(f => { return FieldDetails.Create(f, Logger.CurrentLogger); }) - .ToArray(); - - CancellationTokenSource cancellationToken = new CancellationTokenSource(); - Task> promptTask = - this.consoleHost - .GetInputPromptHandler() - .PromptForInput( - promptCaption, - promptMessage, - fields, - cancellationToken.Token); - - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - promptTask, - "Prompt", - cancellationToken); - // Convert all values to PSObjects - var psObjectDict = new Dictionary(); - - // The result will be null if the prompt was cancelled - if (promptTask.Result != null) + foreach (var keyValuePair in promptTask.Result) { - // Convert all values to PSObjects - foreach (var keyValuePair in promptTask.Result) - { - psObjectDict.Add( - keyValuePair.Key, - keyValuePair.Value != null - ? PSObject.AsPSObject(keyValuePair.Value) - : null); - } + psObjectDict.Add( + keyValuePair.Key, + keyValuePair.Value != null + ? PSObject.AsPSObject(keyValuePair.Value) + : null); } - - // Return the result - return psObjectDict; - } - else - { - // Notify the caller that there's no implementation - throw new NotImplementedException(); } + + // Return the result + return psObjectDict; } + /// + /// + /// + /// + /// + /// + /// + /// public override int PromptForChoice( string promptCaption, string promptMessage, Collection choiceDescriptions, int defaultChoice) { - if (this.consoleHost != null) - { - ChoiceDetails[] choices = - choiceDescriptions - .Select(ChoiceDetails.Create) - .ToArray(); - - CancellationTokenSource cancellationToken = new CancellationTokenSource(); - Task promptTask = - this.consoleHost - .GetChoicePromptHandler() - .PromptForChoice( - promptCaption, - promptMessage, - choices, - defaultChoice, - cancellationToken.Token); - - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - promptTask, - "PromptForChoice", - cancellationToken); - - // Return the result - return promptTask.Result; - } - else - { - // Notify the caller that there's no implementation - throw new NotImplementedException(); - } + ChoiceDetails[] choices = + choiceDescriptions + .Select(ChoiceDetails.Create) + .ToArray(); + + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + Task promptTask = + this.CreateChoicePromptHandler() + .PromptForChoice( + promptCaption, + promptMessage, + choices, + defaultChoice, + cancellationToken.Token); + + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "PromptForChoice", + cancellationToken); + + // Return the result + return promptTask.Result; } + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public override PSCredential PromptForCredential( string promptCaption, string promptMessage, @@ -184,52 +362,50 @@ public override PSCredential PromptForCredential( PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) { - if (this.consoleHost != null) - { - CancellationTokenSource cancellationToken = new CancellationTokenSource(); - - Task> promptTask = - this.consoleHost - .GetInputPromptHandler() - .PromptForInput( - promptCaption, - promptMessage, - new FieldDetails[] { new CredentialFieldDetails("Credential", "Credential", userName) }, - cancellationToken.Token); - - Task unpackTask = - promptTask.ContinueWith( - task => + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + + Task> promptTask = + this.CreateInputPromptHandler() + .PromptForInput( + promptCaption, + promptMessage, + new FieldDetails[] { new CredentialFieldDetails("Credential", "Credential", userName) }, + cancellationToken.Token); + + Task unpackTask = + promptTask.ContinueWith( + task => + { + if (task.IsFaulted) { - if (task.IsFaulted) - { - throw task.Exception; - } - else if (task.IsCanceled) - { - throw new TaskCanceledException(task); - } - - // Return the value of the sole field - return (PSCredential)task.Result?["Credential"]; - }); + throw task.Exception; + } + else if (task.IsCanceled) + { + throw new TaskCanceledException(task); + } - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - unpackTask, - "PromptForCredential", - cancellationToken); + // Return the value of the sole field + return (PSCredential)task.Result?["Credential"]; + }); - return unpackTask.Result; - } - else - { - // Notify the caller that there's no implementation - throw new NotImplementedException( - "'Get-Credential' is not yet supported in this editor."); - } + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + unpackTask, + "PromptForCredential", + cancellationToken); + + return unpackTask.Result; } + /// + /// + /// + /// + /// + /// + /// + /// public override PSCredential PromptForCredential( string caption, string message, @@ -245,213 +421,363 @@ public override PSCredential PromptForCredential( PSCredentialUIOptions.Default); } + /// + /// + /// + /// public override PSHostRawUserInterface RawUI { get { return this.rawUserInterface; } } + /// + /// + /// + /// public override string ReadLine() { - if (this.consoleHost != null) - { - CancellationTokenSource cancellationToken = new CancellationTokenSource(); + CancellationTokenSource cancellationToken = new CancellationTokenSource(); - Task promptTask = - this.consoleHost - .GetInputPromptHandler() - .PromptForInput(cancellationToken.Token); + Task promptTask = + this.CreateInputPromptHandler() + .PromptForInput(cancellationToken.Token); - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - promptTask, - "ReadLine", - cancellationToken); + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "ReadLine", + cancellationToken); - return promptTask.Result; - } - else - { - // Notify the caller that there's no implementation - throw new NotImplementedException(); - } + return promptTask.Result; } + /// + /// + /// + /// public override SecureString ReadLineAsSecureString() { - if (this.consoleHost != null) - { - CancellationTokenSource cancellationToken = new CancellationTokenSource(); + CancellationTokenSource cancellationToken = new CancellationTokenSource(); - Task promptTask = - this.consoleHost - .GetInputPromptHandler() - .PromptForSecureInput(cancellationToken.Token); + Task promptTask = + this.CreateInputPromptHandler() + .PromptForSecureInput(cancellationToken.Token); - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - promptTask, - "ReadLineAsSecureString", - cancellationToken); + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "ReadLineAsSecureString", + cancellationToken); - return promptTask.Result; - } - else - { - // Notify the caller that there's no implementation - throw new NotImplementedException(); - } + return promptTask.Result; } + /// + /// + /// + /// + /// + /// public override void Write( ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) { - if (this.consoleHost != null) - { - this.consoleHost.WriteOutput( - value, - false, - OutputType.Normal, - foregroundColor, - backgroundColor); - } + this.WriteOutput( + value, + false, + OutputType.Normal, + foregroundColor, + backgroundColor); } + /// + /// + /// + /// public override void Write(string value) { - if (this.consoleHost != null) - { - this.consoleHost.WriteOutput( - value, - false, - OutputType.Normal, - this.rawUserInterface.ForegroundColor, - this.rawUserInterface.BackgroundColor); - } + this.WriteOutput( + value, + false, + OutputType.Normal, + this.rawUserInterface.ForegroundColor, + this.rawUserInterface.BackgroundColor); } + /// + /// + /// + /// public override void WriteLine(string value) { - if (this.consoleHost != null) - { - this.consoleHost.WriteOutput( - value, - true, - OutputType.Normal, - this.rawUserInterface.ForegroundColor, - this.rawUserInterface.BackgroundColor); - } + this.WriteOutput( + value, + true, + OutputType.Normal, + this.rawUserInterface.ForegroundColor, + this.rawUserInterface.BackgroundColor); } + /// + /// + /// + /// public override void WriteDebugLine(string message) { - if (this.consoleHost != null) - { - this.consoleHost.WriteOutput( - DebugMessagePrefix + message, - true, - OutputType.Debug, - foregroundColor: ConsoleColor.Yellow); - } + this.WriteOutput( + DebugMessagePrefix + message, + true, + OutputType.Debug, + foregroundColor: ConsoleColor.Yellow); } + /// + /// + /// + /// public override void WriteVerboseLine(string message) { - if (this.consoleHost != null) - { - this.consoleHost.WriteOutput( - VerboseMessagePrefix + message, - true, - OutputType.Verbose, - foregroundColor: ConsoleColor.Blue); - } + this.WriteOutput( + VerboseMessagePrefix + message, + true, + OutputType.Verbose, + foregroundColor: ConsoleColor.Blue); } + /// + /// + /// + /// public override void WriteWarningLine(string message) { - if (this.consoleHost != null) - { - this.consoleHost.WriteOutput( - WarningMessagePrefix + message, - true, - OutputType.Warning, - foregroundColor: ConsoleColor.Yellow); - } + this.WriteOutput( + WarningMessagePrefix + message, + true, + OutputType.Warning, + foregroundColor: ConsoleColor.Yellow); } + /// + /// + /// + /// public override void WriteErrorLine(string value) { - if (this.consoleHost != null) - { - this.consoleHost.WriteOutput( - value, - true, - OutputType.Error, - ConsoleColor.Red); - } + this.WriteOutput( + value, + true, + OutputType.Error, + ConsoleColor.Red); } + /// + /// + /// + /// + /// public override void WriteProgress( long sourceId, ProgressRecord record) { - if (this.consoleHost != null) - { - this.consoleHost.UpdateProgress( - sourceId, - ProgressDetails.Create(record)); - } + this.UpdateProgress( + sourceId, + ProgressDetails.Create(record)); } #endregion #region IHostUISupportsMultipleChoiceSelection Implementation + /// + /// + /// + /// + /// + /// + /// + /// public Collection PromptForChoice( string promptCaption, string promptMessage, Collection choiceDescriptions, IEnumerable defaultChoices) { - if (this.consoleHost != null) + ChoiceDetails[] choices = + choiceDescriptions + .Select(ChoiceDetails.Create) + .ToArray(); + + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + Task promptTask = + this.CreateChoicePromptHandler() + .PromptForChoice( + promptCaption, + promptMessage, + choices, + defaultChoices.ToArray(), + cancellationToken.Token); + + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "PromptForChoice", + cancellationToken); + + // Return the result + return new Collection(promptTask.Result.ToList()); + } + + #endregion + + #region Private Methods + + private async Task WritePromptStringToHost() + { + PSCommand promptCommand = new PSCommand().AddScript("prompt"); + + string promptString = + (await this.powerShellContext.ExecuteCommand(promptCommand, false, false)) + .Select(pso => pso.BaseObject) + .OfType() + .FirstOrDefault() ?? "PS> "; + + // Add the [DBG] prefix if we're stopped in the debugger + if (this.powerShellContext.IsDebuggerStopped) { - ChoiceDetails[] choices = - choiceDescriptions - .Select(ChoiceDetails.Create) - .ToArray(); - - CancellationTokenSource cancellationToken = new CancellationTokenSource(); - Task promptTask = - this.consoleHost - .GetChoicePromptHandler() - .PromptForChoice( - promptCaption, - promptMessage, - choices, - defaultChoices.ToArray(), - cancellationToken.Token); - - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - promptTask, - "PromptForChoice", - cancellationToken); - - // Return the result - return new Collection(promptTask.Result.ToList()); + promptString = + string.Format( + CultureInfo.InvariantCulture, + "[DBG]: {0}", + promptString); } - else + + // Update the stored prompt string if the session is remote + if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote) { - // Notify the caller that there's no implementation - throw new NotImplementedException(); + promptString = + string.Format( + CultureInfo.InvariantCulture, + "[{0}]: {1}", + this.powerShellContext.CurrentRunspace.Runspace.ConnectionInfo != null + ? this.powerShellContext.CurrentRunspace.Runspace.ConnectionInfo.ComputerName + : this.powerShellContext.CurrentRunspace.SessionDetails.ComputerName, + promptString); } + + // Write the prompt string + this.WriteOutput(promptString, false); } - #endregion + private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs) + { + // TODO: What do we display when we don't know why we stopped? - #region Private Methods + if (eventArgs.Breakpoints.Count > 0) + { + // The breakpoint classes have nice ToString output so use that + this.WriteOutput( + Environment.NewLine + $"Hit {eventArgs.Breakpoints[0].ToString()}\n", + true, + OutputType.Normal, + ConsoleColor.Blue); + } + } + + private async Task StartReplLoop(CancellationToken cancellationToken) + { + do + { + string commandString = null; + + await this.WritePromptStringToHost(); + + try + { + commandString = await this.ReadCommandLine(cancellationToken); + } + catch (PipelineStoppedException) + { + this.WriteOutput( + "^C", + true, + OutputType.Normal, + foregroundColor: ConsoleColor.Red); + } + catch (TaskCanceledException) + { + // Do nothing here, the while loop condition will exit. + } + catch (Exception e) // Narrow this if possible + { + this.WriteOutput( + $"\n\nAn error occurred while reading input:\n\n{e.ToString()}\n", + true, + OutputType.Error); + + Logger.WriteException("Caught exception while reading command line", e); + } + + if (commandString != null) + { + this.WriteOutput(string.Empty); + + if (!string.IsNullOrWhiteSpace(commandString)) + { + var unusedTask = + this.powerShellContext + .ExecuteScriptString( + commandString, + false, + true, + true) + .ConfigureAwait(false); + + break; + } + } + } + while (!cancellationToken.IsCancellationRequested); + } + + private InputPromptHandler CreateInputPromptHandler() + { + if (this.activePromptHandler != null) + { + Logger.Write( + LogLevel.Error, + "Prompt handler requested while another prompt is already active."); + } + + InputPromptHandler inputPromptHandler = this.OnCreateInputPromptHandler(); + this.activePromptHandler = inputPromptHandler; + this.activePromptHandler.PromptCancelled += activePromptHandler_PromptCancelled; + + return inputPromptHandler; + } + + private ChoicePromptHandler CreateChoicePromptHandler() + { + if (this.activePromptHandler != null) + { + Logger.Write( + LogLevel.Error, + "Prompt handler requested while another prompt is already active."); + } + + ChoicePromptHandler choicePromptHandler = this.OnCreateChoicePromptHandler(); + this.activePromptHandler = choicePromptHandler; + this.activePromptHandler.PromptCancelled += activePromptHandler_PromptCancelled; + return choicePromptHandler; + } + + private void activePromptHandler_PromptCancelled(object sender, EventArgs e) + { + // Clean up the existing prompt + this.activePromptHandler.PromptCancelled -= activePromptHandler_PromptCancelled; + this.activePromptHandler = null; + } private void WaitForPromptCompletion( Task promptTask, string promptFunctionName, @@ -468,7 +794,7 @@ private void WaitForPromptCompletion( // The Wait() call has timed out, cancel the prompt cancellationToken.Cancel(); - this.consoleHost.WriteOutput("\r\nPrompt has been cancelled due to a timeout.\r\n"); + this.WriteOutput("\r\nPrompt has been cancelled due to a timeout.\r\n"); throw new PipelineStoppedException(); } } @@ -476,7 +802,7 @@ private void WaitForPromptCompletion( { // Find the right InnerException Exception innerException = e.InnerException; - if (innerException is AggregateException) + while (innerException is AggregateException) { innerException = innerException.InnerException; } @@ -504,6 +830,64 @@ private void WaitForPromptCompletion( } } + private void PowerShellContext_DebuggerStop(object sender, System.Management.Automation.DebuggerStopEventArgs e) + { + // Cancel any existing prompt first + this.CancelCommandPrompt(); + + this.WriteDebuggerBanner(e); + this.ShowCommandPrompt(); + } + + private void PowerShellContext_DebuggerResumed(object sender, System.Management.Automation.DebuggerResumeAction e) + { + this.CancelCommandPrompt(); + } + + 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) + { + // When aborted, cancel any lingering prompts + if (this.activePromptHandler != null) + { + this.activePromptHandler.CancelPrompt(); + this.WriteOutput(string.Empty); + } + } + 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) + { + // 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); + } + } + else if ( + eventArgs.ExecutionOptions.WriteErrorsToHost && + (eventArgs.ExecutionStatus == ExecutionStatus.Failed || + eventArgs.HadErrors)) + { + this.CancelCommandPrompt(); + this.WriteOutput(string.Empty); + this.ShowCommandPrompt(); + } + } + } + #endregion } } diff --git a/src/PowerShellEditorServices/Session/Host/IHostInput.cs b/src/PowerShellEditorServices/Session/Host/IHostInput.cs new file mode 100644 index 000000000..28c79839d --- /dev/null +++ b/src/PowerShellEditorServices/Session/Host/IHostInput.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 +{ + /// + /// Provides methods for integrating with the host's input system. + /// + public interface IHostInput + { + /// + /// Starts the host's interactive command loop. + /// + void StartCommandLoop(); + + /// + /// Stops the host's interactive command loop. + /// + void StopCommandLoop(); + + /// + /// Cancels the currently executing command or prompt. + /// + void SendControlC(); + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices/Console/IConsoleHost.cs b/src/PowerShellEditorServices/Session/Host/IHostOutput.cs similarity index 57% rename from src/PowerShellEditorServices/Console/IConsoleHost.cs rename to src/PowerShellEditorServices/Session/Host/IHostOutput.cs index 950fb3fd8..31389043d 100644 --- a/src/PowerShellEditorServices/Console/IConsoleHost.cs +++ b/src/PowerShellEditorServices/Session/Host/IHostOutput.cs @@ -4,17 +4,14 @@ // using System; -using System.Security; -using System.Threading; -using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices { /// - /// Provides a simplified interface for implementing a PowerShell - /// host that will be used for an interactive console. + /// Provides a simplified interface for writing output to a + /// PowerShell host implementation. /// - public interface IConsoleHost + public interface IHostOutput { /// /// Writes output of the given type to the user interface with @@ -42,83 +39,34 @@ void WriteOutput( OutputType outputType, ConsoleColor foregroundColor, ConsoleColor backgroundColor); - - /// - /// Creates a ChoicePromptHandler to use for displaying a - /// choice prompt to the user. - /// - /// A new ChoicePromptHandler instance. - ChoicePromptHandler GetChoicePromptHandler(); - - /// - /// Creates an InputPrompt handle to use for displaying input - /// prompts to the user. - /// - /// A new InputPromptHandler instance. - InputPromptHandler GetInputPromptHandler(); - - /// - /// Reads an input string from the user. - /// - /// A CancellationToken that can be used to cancel the prompt. - /// A Task that can be awaited to get the user's response. - Task ReadSimpleLine(CancellationToken cancellationToken); - - /// - /// Reads a SecureString from the user. - /// - /// A CancellationToken that can be used to cancel the prompt. - /// A Task that can be awaited to get the user's response. - Task ReadSecureLine(CancellationToken cancellationToken); - - /// - /// Cancels the currently executing command or prompt. - /// - void SendControlC(); - - /// - /// Sends a progress update event to the user. - /// - /// The source ID of the progress event. - /// The details of the activity's current progress. - void UpdateProgress( - long sourceId, - ProgressDetails progressDetails); - - /// - /// Notifies the IConsoleHost implementation that the PowerShell - /// session is exiting. - /// - /// The error code that identifies the session exit result. - void ExitSession(int exitCode); } /// - /// Provides helpful extension methods for the IConsoleHost interface. + /// Provides helpful extension methods for the IHostOutput interface. /// - public static class IConsoleHostExtensions + public static class IHostOutputExtensions { /// /// Writes normal output with a newline to the user interface. /// - /// - /// The IConsoleHost implementation to use for WriteOutput calls. + /// + /// The IHostOutput implementation to use for WriteOutput calls. /// /// /// The output string to be written. /// public static void WriteOutput( - this IConsoleHost consoleHost, + this IHostOutput hostOutput, string outputString) { - consoleHost.WriteOutput(outputString, true); + hostOutput.WriteOutput(outputString, true); } /// /// Writes normal output to the user interface. /// - /// - /// The IConsoleHost implementation to use for WriteOutput calls. + /// + /// The IHostOutput implementation to use for WriteOutput calls. /// /// /// The output string to be written. @@ -127,11 +75,11 @@ public static void WriteOutput( /// If true, a newline should be appended to the output's contents. /// public static void WriteOutput( - this IConsoleHost consoleHost, + this IHostOutput hostOutput, string outputString, bool includeNewLine) { - consoleHost.WriteOutput( + hostOutput.WriteOutput( outputString, includeNewLine, OutputType.Normal); @@ -141,8 +89,8 @@ public static void WriteOutput( /// Writes output of a particular type to the user interface /// with a newline ending. /// - /// - /// The IConsoleHost implementation to use for WriteOutput calls. + /// + /// The IHostOutput implementation to use for WriteOutput calls. /// /// /// The output string to be written. @@ -151,11 +99,11 @@ public static void WriteOutput( /// Specifies the type of output to be written. /// public static void WriteOutput( - this IConsoleHost consoleHost, + this IHostOutput hostOutput, string outputString, OutputType outputType) { - consoleHost.WriteOutput( + hostOutput.WriteOutput( outputString, true, OutputType.Normal); @@ -164,8 +112,8 @@ public static void WriteOutput( /// /// Writes output of a particular type to the user interface. /// - /// - /// The IConsoleHost implementation to use for WriteOutput calls. + /// + /// The IHostOutput implementation to use for WriteOutput calls. /// /// /// The output string to be written. @@ -177,12 +125,12 @@ public static void WriteOutput( /// Specifies the type of output to be written. /// public static void WriteOutput( - this IConsoleHost consoleHost, + this IHostOutput hostOutput, string outputString, bool includeNewLine, OutputType outputType) { - consoleHost.WriteOutput( + hostOutput.WriteOutput( outputString, includeNewLine, outputType, @@ -194,8 +142,8 @@ public static void WriteOutput( /// Writes output of a particular type to the user interface using /// a particular foreground color. /// - /// - /// The IConsoleHost implementation to use for WriteOutput calls. + /// + /// The IHostOutput implementation to use for WriteOutput calls. /// /// /// The output string to be written. @@ -210,13 +158,13 @@ public static void WriteOutput( /// Specifies the foreground color of the output to be written. /// public static void WriteOutput( - this IConsoleHost consoleHost, + this IHostOutput hostOutput, string outputString, bool includeNewLine, OutputType outputType, ConsoleColor foregroundColor) { - consoleHost.WriteOutput( + hostOutput.WriteOutput( outputString, includeNewLine, outputType, diff --git a/src/PowerShellEditorServices/Session/SimplePSHostRawUserInterface.cs b/src/PowerShellEditorServices/Session/Host/SimplePSHostRawUserInterface.cs similarity index 92% rename from src/PowerShellEditorServices/Session/SimplePSHostRawUserInterface.cs rename to src/PowerShellEditorServices/Session/Host/SimplePSHostRawUserInterface.cs index 85993107f..f12b03919 100644 --- a/src/PowerShellEditorServices/Session/SimplePSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/SimplePSHostRawUserInterface.cs @@ -3,7 +3,6 @@ // 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.Management.Automation.Host; @@ -13,24 +12,16 @@ namespace Microsoft.PowerShell.EditorServices /// /// Provides an simple implementation of the PSHostRawUserInterface class. /// - internal class SimplePSHostRawUserInterface : PSHostRawUserInterface + public class SimplePSHostRawUserInterface : PSHostRawUserInterface { #region Private Fields private const int DefaultConsoleHeight = 100; private const int DefaultConsoleWidth = 120; - private Size currentBufferSize = new Size(DefaultConsoleWidth, DefaultConsoleHeight); - - #endregion + private ILogger Logger; - #region Properties - - internal IConsoleHost ConsoleHost - { - get; - set; - } + private Size currentBufferSize = new Size(DefaultConsoleWidth, DefaultConsoleHeight); #endregion @@ -40,8 +31,10 @@ internal IConsoleHost ConsoleHost /// Creates a new instance of the SimplePSHostRawUserInterface /// class with the given IConsoleHost implementation. /// - public SimplePSHostRawUserInterface() + /// The ILogger implementation to use for this instance. + public SimplePSHostRawUserInterface(ILogger logger) { + this.Logger = logger; this.ForegroundColor = ConsoleColor.White; this.BackgroundColor = ConsoleColor.Black; } @@ -159,7 +152,7 @@ public override Size MaxWindowSize /// A KeyInfo struct with details about the current keypress. public override KeyInfo ReadKey(ReadKeyOptions options) { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.ReadKey was called"); @@ -171,7 +164,7 @@ public override KeyInfo ReadKey(ReadKeyOptions options) /// public override void FlushInputBuffer() { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.FlushInputBuffer was called"); } @@ -183,7 +176,7 @@ public override void FlushInputBuffer() /// A BufferCell array with the requested buffer contents. public override BufferCell[,] GetBufferContents(Rectangle rectangle) { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.GetBufferContents was called"); @@ -203,7 +196,7 @@ public override void ScrollBufferContents( Rectangle clip, BufferCell fill) { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.ScrollBufferContents was called"); } @@ -217,7 +210,7 @@ public override void SetBufferContents( Rectangle rectangle, BufferCell fill) { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.SetBufferContents was called"); } @@ -231,7 +224,7 @@ public override void SetBufferContents( Coordinates origin, BufferCell[,] contents) { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.SetBufferContents was called"); } diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostRawUserInterface.cs b/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs similarity index 92% rename from src/PowerShellEditorServices/Session/Host/EditorServicesPSHostRawUserInterface.cs rename to src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs index 6f4d5bfae..29daec059 100644 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs @@ -14,13 +14,29 @@ namespace Microsoft.PowerShell.EditorServices /// for the ConsoleService and routes its calls to an IConsoleHost /// implementation. /// - internal class ConsoleServicePSHostRawUserInterface : PSHostRawUserInterface + internal class TerminalPSHostRawUserInterface : PSHostRawUserInterface { #region Private Fields private const int DefaultConsoleHeight = 100; private const int DefaultConsoleWidth = 120; + private ILogger Logger; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the TerminalPSHostRawUserInterface + /// class with the given IConsoleHost implementation. + /// + /// The ILogger implementation to use for this instance. + public TerminalPSHostRawUserInterface(ILogger logger) + { + this.Logger = logger; + } + #endregion #region PSHostRawUserInterface Implementation @@ -168,7 +184,7 @@ public override Size MaxWindowSize /// A KeyInfo struct with details about the current keypress. public override KeyInfo ReadKey(ReadKeyOptions options) { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.ReadKey was called"); @@ -180,7 +196,7 @@ public override KeyInfo ReadKey(ReadKeyOptions options) /// public override void FlushInputBuffer() { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.FlushInputBuffer was called"); } @@ -192,7 +208,7 @@ public override void FlushInputBuffer() /// A BufferCell array with the requested buffer contents. public override BufferCell[,] GetBufferContents(Rectangle rectangle) { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.GetBufferContents was called"); @@ -212,7 +228,7 @@ public override void ScrollBufferContents( Rectangle clip, BufferCell fill) { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.ScrollBufferContents was called"); } @@ -236,7 +252,7 @@ public override void SetBufferContents( } else { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.SetBufferContents was called with a specific region"); } @@ -251,7 +267,7 @@ public override void SetBufferContents( Coordinates origin, BufferCell[,] contents) { - Logger.CurrentLogger.Write( + Logger.Write( LogLevel.Warning, "PSHostRawUserInterface.SetBufferContents was called"); } diff --git a/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs new file mode 100644 index 000000000..e64321205 --- /dev/null +++ b/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs @@ -0,0 +1,154 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Console; + +namespace Microsoft.PowerShell.EditorServices +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.PowerShell.EditorServices.Utility; + + /// + /// Provides an EditorServicesPSHostUserInterface implementation + /// that integrates with the user's terminal UI. + /// + public class TerminalPSHostUserInterface : EditorServicesPSHostUserInterface + { + #region Private Fields + + private ConsoleReadLine consoleReadLine; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ConsoleServicePSHostUserInterface + /// class with the given IConsoleHost implementation. + /// + /// The PowerShellContext to use for executing commands. + /// An ILogger implementation to use for this host. + public TerminalPSHostUserInterface( + PowerShellContext powerShellContext, + ILogger logger) + : base( + powerShellContext, + new TerminalPSHostRawUserInterface(logger), + logger) + { + this.consoleReadLine = new ConsoleReadLine(powerShellContext); + + // Set the output encoding to UTF-8 so that special + // characters are written to the console correctly + System.Console.OutputEncoding = System.Text.Encoding.UTF8; + + System.Console.CancelKeyPress += + (obj, args) => + { + if (!this.IsNativeApplicationRunning) + { + // We'll handle Ctrl+C + args.Cancel = true; + this.SendControlC(); + } + }; + } + + #endregion + + /// + /// Requests that the HostUI implementation read a command line + /// from the user to be executed in the integrated console command + /// loop. + /// + /// + /// A CancellationToken used to cancel the command line request. + /// + /// A Task that can be awaited for the resulting input string. + protected override Task ReadCommandLine(CancellationToken cancellationToken) + { + return this.consoleReadLine.ReadCommandLine(cancellationToken); + } + + /// + /// Creates an InputPrompt handle to use for displaying input + /// prompts to the user. + /// + /// A new InputPromptHandler instance. + protected override InputPromptHandler OnCreateInputPromptHandler() + { + return new TerminalInputPromptHandler( + this.consoleReadLine, + this, + this.Logger); + } + + /// + /// Creates a ChoicePromptHandler to use for displaying a + /// choice prompt to the user. + /// + /// A new ChoicePromptHandler instance. + protected override ChoicePromptHandler OnCreateChoicePromptHandler() + { + return new TerminalChoicePromptHandler( + this.consoleReadLine, + this, + this.Logger); + } + + /// + /// Writes output of the given type to the user interface with + /// the given foreground and background colors. Also includes + /// a newline if requested. + /// + /// + /// The output string to be written. + /// + /// + /// If true, a newline should be appended to the output's contents. + /// + /// + /// Specifies the type of output to be written. + /// + /// + /// Specifies the foreground color of the output to be written. + /// + /// + /// Specifies the background color of the output to be written. + /// + public override void WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor) + { + ConsoleColor oldForegroundColor = System.Console.ForegroundColor; + ConsoleColor oldBackgroundColor = System.Console.BackgroundColor; + + System.Console.ForegroundColor = foregroundColor; + System.Console.BackgroundColor = backgroundColor; + + System.Console.Write(outputString + (includeNewLine ? Environment.NewLine : "")); + + System.Console.ForegroundColor = oldForegroundColor; + System.Console.BackgroundColor = oldBackgroundColor; + } + + /// + /// Sends a progress update event to the user. + /// + /// The source ID of the progress event. + /// The details of the activity's current progress. + protected override void UpdateProgress( + long sourceId, + ProgressDetails progressDetails) + { + + } + } +} diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index a0c962b80..303a404a6 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -89,10 +89,10 @@ public PowerShellVersionDetails LocalPowerShellVersion } /// - /// Gets or sets an IConsoleHost implementation for use in + /// Gets or sets an IHostOutput implementation for use in /// writing output to the console. /// - private IConsoleHost ConsoleHost { get; set; } + private IHostOutput ConsoleWriter { get; set; } /// /// Gets details pertaining to the current runspace. @@ -108,7 +108,7 @@ public RunspaceDetails CurrentRunspace #region Constructors /// - /// + /// /// /// An ILogger implementation used for writing log messages. public PowerShellContext(ILogger logger) @@ -121,15 +121,19 @@ public PowerShellContext(ILogger logger) /// /// /// - /// + /// + /// The EditorServicesPSHostUserInterface to use for this instance. + /// + /// An ILogger implementation to use for this instance. /// public static Runspace CreateRunspace( HostDetails hostDetails, PowerShellContext powerShellContext, - bool enableConsoleRepl) + EditorServicesPSHostUserInterface hostUserInterface, + ILogger logger) { - var psHost = new ConsoleServicePSHost(powerShellContext, hostDetails, enableConsoleRepl); - powerShellContext.ConsoleHost = psHost.ConsoleService; + var psHost = new EditorServicesPSHost(powerShellContext, hostDetails, hostUserInterface, logger); + powerShellContext.ConsoleWriter = hostUserInterface; return CreateRunspace(psHost); } @@ -174,18 +178,18 @@ public void Initialize( /// An object containing the profile paths for the session. /// The initial runspace to use for this instance. /// If true, the PowerShellContext owns this runspace. - /// An IConsoleHost implementation. Optional. + /// An IHostOutput implementation. Optional. public void Initialize( ProfilePaths profilePaths, Runspace initialRunspace, bool ownsInitialRunspace, - IConsoleHost consoleHost) + IHostOutput consoleHost) { Validate.IsNotNull("initialRunspace", initialRunspace); this.ownsInitialRunspace = ownsInitialRunspace; this.SessionState = PowerShellContextState.NotStarted; - this.ConsoleHost = consoleHost; + this.ConsoleWriter = consoleHost; // Get the PowerShell runtime version this.LocalPowerShellVersion = @@ -1211,9 +1215,9 @@ internal void WriteOutput( bool includeNewLine, OutputType outputType) { - if (this.ConsoleHost != null) + if (this.ConsoleWriter != null) { - this.ConsoleHost.WriteOutput( + this.ConsoleWriter.WriteOutput( outputString, includeNewLine, outputType); @@ -1281,9 +1285,9 @@ private void WriteError( private void WriteError(string errorMessage) { - if (this.ConsoleHost != null) + if (this.ConsoleWriter != null) { - this.ConsoleHost.WriteOutput( + this.ConsoleWriter.WriteOutput( errorMessage, true, OutputType.Error, diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index abe684242..a89a7e264 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -772,6 +772,8 @@ public async Task ServiceLoadsProfilesOnDemand() File.Exists(currentUserCurrentHostPath), "Copied profile path does not exist!"); + OutputReader outputReader = new OutputReader(this.messageHandlers); + // Send the configuration change to cause profiles to be loaded await this.languageServiceClient.SendEvent( DidChangeConfigurationNotification.Type, @@ -790,7 +792,8 @@ await this.languageServiceClient.SendEvent( } }); - OutputReader outputReader = new OutputReader(this.messageHandlers); + // Wait for the prompt to be written once the profile loads + Assert.StartsWith("PS ", await outputReader.ReadLine(waitForNewLine: false)); Task evaluateTask = this.SendRequest( diff --git a/test/PowerShellEditorServices.Test.Host/OutputReader.cs b/test/PowerShellEditorServices.Test.Host/OutputReader.cs index 989d193e5..91bd980e8 100644 --- a/test/PowerShellEditorServices.Test.Host/OutputReader.cs +++ b/test/PowerShellEditorServices.Test.Host/OutputReader.cs @@ -8,8 +8,6 @@ using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -30,7 +28,7 @@ public OutputReader(IMessageHandlers messageHandlers) this.OnOutputEvent); } - public async Task ReadLine(string expectedOutputCategory = "stdout") + public async Task ReadLine(string expectedOutputCategory = "stdout", bool waitForNewLine = true) { try { @@ -100,6 +98,10 @@ public async Task ReadLine(string expectedOutputCategory = "stdout") // At this point, the state of lineHasNewLine will determine // whether the loop continues to wait for another output // event that completes the current line. + if (!waitForNewLine) + { + break; + } } return nextOutputString; diff --git a/test/PowerShellEditorServices.Test/Console/ConsoleServiceTests.cs b/test/PowerShellEditorServices.Test/Console/ConsoleServiceTests.cs index 37356d5fc..14dda1fa0 100644 --- a/test/PowerShellEditorServices.Test/Console/ConsoleServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Console/ConsoleServiceTests.cs @@ -1,674 +1,674 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// 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.Collections.Generic; -using System.Linq; -using System.Security; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.PowerShell.EditorServices.Test.Console -{ - public class ConsoleServiceTests : IDisposable - { - private ConsoleService consoleService; - private PowerShellContext powerShellContext; - private TestConsolePromptHandlerContext promptHandlerContext; - - private Dictionary outputPerType = - new Dictionary(); - - const string TestOutputString = "This is a test."; - - const string PromptCaption = "Test Prompt"; - const string PromptMessage = "Make a selection"; - const int PromptDefault = 1; - - static readonly Tuple[] PromptChoices = - new Tuple[] - { - new Tuple("&Apple", "Help for Apple"), - new Tuple("Ba&nana", "Help for Banana"), - new Tuple("Orange", "Help for Orange") - }; - - static readonly Tuple[] PromptFields = - new Tuple[] - { - new Tuple("Name", typeof(string)), - new Tuple("Age", typeof(int)), - new Tuple("Books", typeof(string[])), - }; - - public ConsoleServiceTests() - { - this.powerShellContext = new PowerShellContext(new NullLogger()); - ConsoleServicePSHost psHost = - new ConsoleServicePSHost( - powerShellContext, - PowerShellContextTests.TestHostDetails, - false); - - this.consoleService = psHost.ConsoleService; - - this.powerShellContext.Initialize( - null, - PowerShellContext.CreateRunspace(psHost), - true); - - this.promptHandlerContext = - new TestConsolePromptHandlerContext(); - - this.consoleService.PushPromptHandlerContext(this.promptHandlerContext); - this.consoleService.OutputWritten += OnOutputWritten; - promptHandlerContext.ConsoleHost = this.consoleService; - } - - public void Dispose() - { - this.powerShellContext.Dispose(); - } - - [Fact] - public async Task ReceivesNormalOutput() - { - await this.powerShellContext.ExecuteScriptString( - string.Format( - "\"{0}\"", - TestOutputString)); - - // Prompt strings are returned as normal output, ignore the prompt - string[] normalOutputLines = - this.GetOutputForType(OutputType.Normal) - .Split( - new string[] { Environment.NewLine }, - StringSplitOptions.None); - - // The output should be 2 lines: the expected string and - // an empty line. - Assert.Equal(2, normalOutputLines.Length); - Assert.Equal( - TestOutputString, - normalOutputLines[0]); - } - - [Fact] - public async Task ReceivesErrorOutput() - { - await this.powerShellContext.ExecuteScriptString( - string.Format( - "Write-Error \"{0}\"", - TestOutputString)); - - string errorString = this.GetOutputForType(OutputType.Error).Split('\r')[0]; - - Assert.Equal( - string.Format("Write-Error \"{0}\" : {0}", TestOutputString), - errorString); - } - - [Fact] - public async Task ReceivesVerboseOutput() - { - // Since setting VerbosePreference causes other message to - // be written out when we run our test, run a command preemptively - // to flush out unwanted verbose messages - await this.powerShellContext.ExecuteScriptString("Write-Verbose \"Preloading\""); - - await this.powerShellContext.ExecuteScriptString( - string.Format( - "$VerbosePreference = \"Continue\"; Write-Verbose \"{0}\"", - TestOutputString)); - - Assert.Equal( - ConsoleServicePSHostUserInterface.VerboseMessagePrefix + TestOutputString + Environment.NewLine, - this.GetOutputForType(OutputType.Verbose)); - } - - [Fact] - public async Task ReceivesDebugOutput() - { - // Since setting VerbosePreference causes other message to - // be written out when we run our test, run a command preemptively - // to flush out unwanted verbose messages - await this.powerShellContext.ExecuteScriptString("Write-Verbose \"Preloading\""); - - await this.powerShellContext.ExecuteScriptString( - string.Format( - "$DebugPreference = \"Continue\"; Write-Debug \"{0}\"", - TestOutputString)); - - Assert.Equal( - ConsoleServicePSHostUserInterface.DebugMessagePrefix + TestOutputString + Environment.NewLine, - this.GetOutputForType(OutputType.Debug)); - } - - [Fact] - public async Task ReceivesWarningOutput() - { - await this.powerShellContext.ExecuteScriptString( - string.Format( - "Write-Warning \"{0}\"", - TestOutputString)); - - Assert.Equal( - ConsoleServicePSHostUserInterface.WarningMessagePrefix + TestOutputString + Environment.NewLine, - this.GetOutputForType(OutputType.Warning)); - } - - [Fact] - public async Task ReceivesChoicePrompt() - { - string choiceScript = - this.GetChoicePromptString( - PromptCaption, - PromptMessage, - PromptChoices, - PromptDefault); - - var promptTask = this.promptHandlerContext.WaitForChoicePrompt(); - var executeTask = this.powerShellContext.ExecuteScriptString(choiceScript); - - // Wait for the prompt to be shown - var promptHandler = await promptTask; - - // Respond to the prompt and wait for the prompt to complete - await promptHandler.ReturnInputString("apple"); - await executeTask; - - string[] outputLines = - this.GetOutputForType(OutputType.Normal) - .Split( - new string[] { Environment.NewLine }, - StringSplitOptions.None); - - Assert.Equal(PromptCaption, outputLines[0]); - Assert.Equal(PromptMessage, outputLines[1]); - Assert.Equal("[A] Apple [N] Banana [] Orange [?] Help (default is \"Banana\"): apple", outputLines[2]); - Assert.Equal("0", outputLines[3]); - } - - [Fact] - public async Task CancelsChoicePrompt() - { - string choiceScript = - this.GetChoicePromptString( - PromptCaption, - PromptMessage, - PromptChoices, - PromptDefault); - - var promptTask = this.promptHandlerContext.WaitForChoicePrompt(); - var executeTask = this.powerShellContext.ExecuteScriptString(choiceScript); - - // Wait for the prompt to be shown - await promptTask; - - // Cancel the prompt and wait for the execution to complete - this.consoleService.SendControlC(); - await executeTask; - - string[] outputLines = - this.GetOutputForType(OutputType.Normal) - .Split( - new string[] { Environment.NewLine }, - StringSplitOptions.None); - - Assert.Equal(PromptCaption, outputLines[0]); - Assert.Equal(PromptMessage, outputLines[1]); - Assert.Equal("[A] Apple [N] Banana [] Orange [?] Help (default is \"Banana\"): ", outputLines[2]); - } - - [Fact] - public async Task ReceivesChoicePromptHelp() - { - string choiceScript = - this.GetChoicePromptString( - PromptCaption, - PromptMessage, - PromptChoices, - PromptDefault); - - var promptTask = this.promptHandlerContext.WaitForChoicePrompt(); - var executeTask = this.powerShellContext.ExecuteScriptString(choiceScript); - - // Wait for the prompt to be shown - var promptHandler = await promptTask; - - // Respond to the prompt and wait for the help prompt to appear - await promptHandler.ReturnInputString("?"); - await promptHandler.ReturnInputString("A"); - await executeTask; - - string[] outputLines = - this.GetOutputForType(OutputType.Normal) - .Split( - new string[] { Environment.NewLine }, - StringSplitOptions.None); - - // Help lines start after initial prompt, skip 3 lines - Assert.Equal("A - Help for Apple", outputLines[3]); - Assert.Equal("N - Help for Banana", outputLines[4]); - Assert.Equal("Orange - Help for Orange", outputLines[5]); - } - - [Fact] - public async Task ReceivesInputPrompt() - { - string inputScript = - this.GetInputPromptString( - PromptCaption, - PromptMessage, - PromptFields); - - var promptTask = this.promptHandlerContext.WaitForInputPrompt(); - var executeTask = this.powerShellContext.ExecuteScriptString(inputScript); - - // Wait for the prompt to be shown - var promptHandler = await promptTask; - - // Respond to the prompt and wait for execution to complete - await promptHandler.ReturnInputString("John"); - await promptHandler.ReturnInputString("40"); - await promptHandler.ReturnInputString("Windows PowerShell In Action"); - await promptHandler.ReturnInputString(""); - await executeTask; - - string[] outputLines = - this.GetOutputForType(OutputType.Normal) - .Split( - new string[] { Environment.NewLine }, - StringSplitOptions.None); - - Assert.Equal(PromptCaption, outputLines[0]); - Assert.Equal(PromptMessage, outputLines[1]); - Assert.Equal("Name: John", outputLines[2]); - Assert.Equal("Age: 40", outputLines[3]); - Assert.Equal("Books[0]: Windows PowerShell In Action", outputLines[4]); - Assert.Equal("Books[1]: ", outputLines[5]); - Assert.Equal("Name John", outputLines[9].Trim()); - Assert.Equal("Age 40", outputLines[10].Trim()); - Assert.Equal("Books {Windows PowerShell In Action}", outputLines[11].Trim()); - } - - [Fact] - public async Task CancelsInputPrompt() - { - string inputScript = - this.GetInputPromptString( - PromptCaption, - PromptMessage, - PromptFields); - - var promptTask = this.promptHandlerContext.WaitForInputPrompt(); - var executeTask = this.powerShellContext.ExecuteScriptString(inputScript); - - // Wait for the prompt to be shown - await promptTask; - - // Cancel the prompt and wait for execution to complete - this.consoleService.SendControlC(); - await executeTask; - - string[] outputLines = - this.GetOutputForType(OutputType.Normal) - .Split( - new string[] { Environment.NewLine }, - StringSplitOptions.None); - - Assert.Equal(PromptCaption, outputLines[0]); - Assert.Equal(PromptMessage, outputLines[1]); - Assert.Equal("Name: ", outputLines[2]); - } - - [Fact] - public async Task ReceivesReadHostPrompt() - { - var promptTask = this.promptHandlerContext.WaitForInputPrompt(); - var executeTask = this.powerShellContext.ExecuteScriptString("Read-Host"); - - // Wait for the prompt to be shown - TestConsoleInputPromptHandler promptHandler = await promptTask; - - // Respond to the prompt and wait for execution to complete - await promptHandler.ReturnInputString("John"); - await executeTask; - - string[] outputLines = - this.GetOutputForType(OutputType.Normal) - .Split( - new string[] { Environment.NewLine }, - StringSplitOptions.None); - - Assert.Equal("John", outputLines[0]); - Assert.Equal("John", outputLines[1]); - } - - [Fact] - public async Task CancelsReadHostPrompt() - { - var promptTask = this.promptHandlerContext.WaitForInputPrompt(); - var executeTask = this.powerShellContext.ExecuteScriptString("Read-Host"); - - // Wait for the prompt to be shown - await promptTask; - - // Cancel the prompt and wait for execution to complete - this.consoleService.SendControlC(); - await executeTask; - - // No output will be written from a cancelled Read-Host prompt - Assert.Null(this.GetOutputForType(OutputType.Normal)); - } - - [Fact] - public async Task ReceivesReadHostPromptWithFieldName() - { - var promptTask = this.promptHandlerContext.WaitForInputPrompt(); - var executeTask = this.powerShellContext.ExecuteScriptString("Read-Host -Prompt \"Name\""); - - // Wait for the prompt to be shown - TestConsoleInputPromptHandler promptHandler = await promptTask; - - // Respond to the prompt and wait for execution to complete - await promptHandler.ReturnInputString("John"); - await executeTask; - - string[] outputLines = - this.GetOutputForType(OutputType.Normal) - .Split( - new string[] { Environment.NewLine }, - StringSplitOptions.None); - - Assert.Equal("Name: John", outputLines[0]); - Assert.Equal("John", outputLines[1]); - } - - #region Helper Methods - - void OnOutputWritten(object sender, OutputWrittenEventArgs e) - { - string storedOutputString = null; - if (!this.outputPerType.TryGetValue(e.OutputType, out storedOutputString)) - { - this.outputPerType.Add(e.OutputType, null); - } - - if (storedOutputString == null) - { - storedOutputString = e.OutputText; - } - else - { - storedOutputString += e.OutputText; - } - - if (e.IncludeNewLine) - { - storedOutputString += Environment.NewLine; - } - - this.outputPerType[e.OutputType] = storedOutputString; - } - - private string GetOutputForType(OutputType outputLineType) - { - string outputString = null; - - this.outputPerType.TryGetValue(outputLineType, out outputString); - - return outputString; - } - - private string GetChoicePromptString( - string caption, - string message, - Tuple[] choices, - int defaultChoice) - { - StringBuilder scriptBuilder = new StringBuilder(); - - scriptBuilder.AppendFormat( - "$caption = {0}\r\n", - caption != null ? - "\"" + caption + "\"" : - "$null"); - - scriptBuilder.AppendFormat( - "$message = {0}\r\n", - message != null ? - "\"" + message + "\"" : - "$null"); - - scriptBuilder.AppendLine("$choices = [System.Management.Automation.Host.ChoiceDescription[]]("); - - List choiceItems = new List(); - foreach (var choice in choices) - { - choiceItems.Add( - string.Format( - " (new-Object System.Management.Automation.Host.ChoiceDescription \"{0}\",\"{1}\")", - choice.Item1, - choice.Item2)); - } - - scriptBuilder.AppendFormat( - "{0})\r\n", - string.Join(",\r\n", choiceItems)); - - scriptBuilder.AppendFormat( - "$host.ui.PromptForChoice($caption, $message, $choices, {0})\r\n", - defaultChoice); - - return scriptBuilder.ToString(); - } - - private string GetInputPromptString( - string caption, - string message, - Tuple[] fields) - { - StringBuilder scriptBuilder = new StringBuilder(); - - scriptBuilder.AppendFormat( - "$caption = {0}\r\n", - caption != null ? - "\"" + caption + "\"" : - "$null"); - - scriptBuilder.AppendFormat( - "$message = {0}\r\n", - message != null ? - "\"" + message + "\"" : - "$null"); - - foreach (var field in fields) - { - scriptBuilder.AppendFormat( - "${0}Field = New-Object System.Management.Automation.Host.FieldDescription \"{0}\"\r\n${0}Field.SetParameterType([{1}])\r\n", - field.Item1, - field.Item2.FullName); - } - - scriptBuilder.AppendFormat( - "$fields = [System.Management.Automation.Host.FieldDescription[]]({0})\r\n", - string.Join( - ", ", - fields.Select( - f => string.Format("${0}Field", f.Item1)))); - - scriptBuilder.AppendLine( - "$host.ui.Prompt($caption, $message, $fields)"); - - return scriptBuilder.ToString(); - } - - #endregion - } - - internal class TestConsolePromptHandlerContext : IPromptHandlerContext - { - private TaskCompletionSource choicePromptShownTask; - private TaskCompletionSource inputPromptShownTask; - - public IConsoleHost ConsoleHost { get; set; } - - public ChoicePromptHandler GetChoicePromptHandler() - { - return new TestConsoleChoicePromptHandler( - this.ConsoleHost, - this.choicePromptShownTask); - } - - public InputPromptHandler GetInputPromptHandler() - { - return new TestConsoleInputPromptHandler( - this.ConsoleHost, - this.inputPromptShownTask); - } - - public Task WaitForChoicePrompt() - { - this.choicePromptShownTask = new TaskCompletionSource(); - return this.choicePromptShownTask.Task; - } - - public Task WaitForInputPrompt() - { - this.inputPromptShownTask = new TaskCompletionSource(); - return this.inputPromptShownTask.Task; - } - } - - internal class TestConsoleChoicePromptHandler : ConsoleChoicePromptHandler - { - private IConsoleHost consoleHost; - private TaskCompletionSource promptShownTask; - private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - - private TaskCompletionSource linePromptTask; - private AsyncQueue> linePromptQueue = - new AsyncQueue>(); - - public TestConsoleChoicePromptHandler( - IConsoleHost consoleHost, - TaskCompletionSource promptShownTask) - : base(consoleHost, new NullLogger()) - { - this.consoleHost = consoleHost; - this.promptShownTask = promptShownTask; - } - - public async Task ReturnInputString(string inputString) - { - var promptTask = await this.linePromptQueue.DequeueAsync(); - this.consoleHost.WriteOutput(inputString); - promptTask.SetResult(inputString); - } - - protected override async Task ReadInputString(CancellationToken cancellationToken) - { - TaskCompletionSource promptTask = new TaskCompletionSource(); - await this.linePromptQueue.EnqueueAsync(promptTask); - - if (this.cancellationTokenSource.IsCancellationRequested) - { - this.linePromptTask.TrySetCanceled(); - } - - this.linePromptTask = promptTask; - return await promptTask.Task; - } - - protected override void ShowPrompt(PromptStyle promptStyle) - { - base.ShowPrompt(promptStyle); - - if (this.promptShownTask != null && - this.promptShownTask.Task.Status != TaskStatus.RanToCompletion) - { - this.promptShownTask.SetResult(this); - } - } - - protected override void OnPromptCancelled() - { - this.cancellationTokenSource.Cancel(); - - if (this.linePromptTask != null) - { - this.linePromptTask.TrySetCanceled(); - } - } - } - - internal class TestConsoleInputPromptHandler : ConsoleInputPromptHandler - { - private IConsoleHost consoleHost; - private TaskCompletionSource promptShownTask; - private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - - private TaskCompletionSource linePromptTask; - private AsyncQueue> linePromptQueue = - new AsyncQueue>(); - - public TestConsoleInputPromptHandler( - IConsoleHost consoleHost, - TaskCompletionSource promptShownTask) - : base(consoleHost, new NullLogger()) - { - this.consoleHost = consoleHost; - this.promptShownTask = promptShownTask; - } - - public async Task ReturnInputString(string inputString) - { - var promptTask = await this.linePromptQueue.DequeueAsync(); - this.consoleHost.WriteOutput(inputString); - promptTask.SetResult(inputString); - } - - protected override async Task ReadInputString(CancellationToken cancellationToken) - { - TaskCompletionSource promptTask = new TaskCompletionSource(); - await this.linePromptQueue.EnqueueAsync(promptTask); - - if (this.cancellationTokenSource.IsCancellationRequested) - { - this.linePromptTask.TrySetCanceled(); - } - - this.linePromptTask = promptTask; - return await promptTask.Task; - } - - protected override void ShowFieldPrompt(FieldDetails fieldDetails) - { - base.ShowFieldPrompt(fieldDetails); - - // Raise the task for the first field prompt shown - if (this.promptShownTask != null && - this.promptShownTask.Task.Status == TaskStatus.WaitingForActivation) - { - this.promptShownTask.SetResult(this); - } - } - - protected override void OnPromptCancelled() - { - this.cancellationTokenSource.Cancel(); - - if (this.linePromptTask != null) - { - this.linePromptTask.TrySetCanceled(); - } - } - } -} +// // +// // Copyright (c) Microsoft. All rights reserved. +// // 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.Collections.Generic; +// using System.Linq; +// using System.Security; +// using System.Text; +// using System.Threading; +// using System.Threading.Tasks; +// using Xunit; + +// namespace Microsoft.PowerShell.EditorServices.Test.Console +// { +// public class ConsoleServiceTests : IDisposable +// { +// private ConsoleService consoleService; +// private PowerShellContext powerShellContext; +// private TestConsolePromptHandlerContext promptHandlerContext; + +// private Dictionary outputPerType = +// new Dictionary(); + +// const string TestOutputString = "This is a test."; + +// const string PromptCaption = "Test Prompt"; +// const string PromptMessage = "Make a selection"; +// const int PromptDefault = 1; + +// static readonly Tuple[] PromptChoices = +// new Tuple[] +// { +// new Tuple("&Apple", "Help for Apple"), +// new Tuple("Ba&nana", "Help for Banana"), +// new Tuple("Orange", "Help for Orange") +// }; + +// static readonly Tuple[] PromptFields = +// new Tuple[] +// { +// new Tuple("Name", typeof(string)), +// new Tuple("Age", typeof(int)), +// new Tuple("Books", typeof(string[])), +// }; + +// public ConsoleServiceTests() +// { +// this.powerShellContext = new PowerShellContext(); +// EditorServicesPSHost psHost = +// new EditorServicesPSHost( +// powerShellContext, +// PowerShellContextTests.TestHostDetails, +// false); + +// this.consoleService = psHost.ConsoleService; + +// this.powerShellContext.Initialize( +// null, +// PowerShellContext.CreateRunspace(psHost), +// true); + +// this.promptHandlerContext = +// new TestConsolePromptHandlerContext(); + +// this.consoleService.PushPromptHandlerContext(this.promptHandlerContext); +// this.consoleService.OutputWritten += OnOutputWritten; +// promptHandlerContext.ConsoleHost = this.consoleService; +// } + +// public void Dispose() +// { +// this.powerShellContext.Dispose(); +// } + +// [Fact] +// public async Task ReceivesNormalOutput() +// { +// await this.powerShellContext.ExecuteScriptString( +// string.Format( +// "\"{0}\"", +// TestOutputString)); + +// // Prompt strings are returned as normal output, ignore the prompt +// string[] normalOutputLines = +// this.GetOutputForType(OutputType.Normal) +// .Split( +// new string[] { Environment.NewLine }, +// StringSplitOptions.None); + +// // The output should be 2 lines: the expected string and +// // an empty line. +// Assert.Equal(2, normalOutputLines.Length); +// Assert.Equal( +// TestOutputString, +// normalOutputLines[0]); +// } + +// [Fact] +// public async Task ReceivesErrorOutput() +// { +// await this.powerShellContext.ExecuteScriptString( +// string.Format( +// "Write-Error \"{0}\"", +// TestOutputString)); + +// string errorString = this.GetOutputForType(OutputType.Error).Split('\r')[0]; + +// Assert.Equal( +// string.Format("Write-Error \"{0}\" : {0}", TestOutputString), +// errorString); +// } + +// [Fact] +// public async Task ReceivesVerboseOutput() +// { +// // Since setting VerbosePreference causes other message to +// // be written out when we run our test, run a command preemptively +// // to flush out unwanted verbose messages +// await this.powerShellContext.ExecuteScriptString("Write-Verbose \"Preloading\""); + +// await this.powerShellContext.ExecuteScriptString( +// string.Format( +// "$VerbosePreference = \"Continue\"; Write-Verbose \"{0}\"", +// TestOutputString)); + +// Assert.Equal( +// EditorServicesPSHostUserInterface.VerboseMessagePrefix + TestOutputString + Environment.NewLine, +// this.GetOutputForType(OutputType.Verbose)); +// } + +// [Fact] +// public async Task ReceivesDebugOutput() +// { +// // Since setting VerbosePreference causes other message to +// // be written out when we run our test, run a command preemptively +// // to flush out unwanted verbose messages +// await this.powerShellContext.ExecuteScriptString("Write-Verbose \"Preloading\""); + +// await this.powerShellContext.ExecuteScriptString( +// string.Format( +// "$DebugPreference = \"Continue\"; Write-Debug \"{0}\"", +// TestOutputString)); + +// Assert.Equal( +// EditorServicesPSHostUserInterface.DebugMessagePrefix + TestOutputString + Environment.NewLine, +// this.GetOutputForType(OutputType.Debug)); +// } + +// [Fact] +// public async Task ReceivesWarningOutput() +// { +// await this.powerShellContext.ExecuteScriptString( +// string.Format( +// "Write-Warning \"{0}\"", +// TestOutputString)); + +// Assert.Equal( +// EditorServicesPSHostUserInterface.WarningMessagePrefix + TestOutputString + Environment.NewLine, +// this.GetOutputForType(OutputType.Warning)); +// } + +// [Fact] +// public async Task ReceivesChoicePrompt() +// { +// string choiceScript = +// this.GetChoicePromptString( +// PromptCaption, +// PromptMessage, +// PromptChoices, +// PromptDefault); + +// var promptTask = this.promptHandlerContext.WaitForChoicePrompt(); +// var executeTask = this.powerShellContext.ExecuteScriptString(choiceScript); + +// // Wait for the prompt to be shown +// var promptHandler = await promptTask; + +// // Respond to the prompt and wait for the prompt to complete +// await promptHandler.ReturnInputString("apple"); +// await executeTask; + +// string[] outputLines = +// this.GetOutputForType(OutputType.Normal) +// .Split( +// new string[] { Environment.NewLine }, +// StringSplitOptions.None); + +// Assert.Equal(PromptCaption, outputLines[0]); +// Assert.Equal(PromptMessage, outputLines[1]); +// Assert.Equal("[A] Apple [N] Banana [] Orange [?] Help (default is \"Banana\"): apple", outputLines[2]); +// Assert.Equal("0", outputLines[3]); +// } + +// [Fact] +// public async Task CancelsChoicePrompt() +// { +// string choiceScript = +// this.GetChoicePromptString( +// PromptCaption, +// PromptMessage, +// PromptChoices, +// PromptDefault); + +// var promptTask = this.promptHandlerContext.WaitForChoicePrompt(); +// var executeTask = this.powerShellContext.ExecuteScriptString(choiceScript); + +// // Wait for the prompt to be shown +// await promptTask; + +// // Cancel the prompt and wait for the execution to complete +// this.consoleService.SendControlC(); +// await executeTask; + +// string[] outputLines = +// this.GetOutputForType(OutputType.Normal) +// .Split( +// new string[] { Environment.NewLine }, +// StringSplitOptions.None); + +// Assert.Equal(PromptCaption, outputLines[0]); +// Assert.Equal(PromptMessage, outputLines[1]); +// Assert.Equal("[A] Apple [N] Banana [] Orange [?] Help (default is \"Banana\"): ", outputLines[2]); +// } + +// [Fact] +// public async Task ReceivesChoicePromptHelp() +// { +// string choiceScript = +// this.GetChoicePromptString( +// PromptCaption, +// PromptMessage, +// PromptChoices, +// PromptDefault); + +// var promptTask = this.promptHandlerContext.WaitForChoicePrompt(); +// var executeTask = this.powerShellContext.ExecuteScriptString(choiceScript); + +// // Wait for the prompt to be shown +// var promptHandler = await promptTask; + +// // Respond to the prompt and wait for the help prompt to appear +// await promptHandler.ReturnInputString("?"); +// await promptHandler.ReturnInputString("A"); +// await executeTask; + +// string[] outputLines = +// this.GetOutputForType(OutputType.Normal) +// .Split( +// new string[] { Environment.NewLine }, +// StringSplitOptions.None); + +// // Help lines start after initial prompt, skip 3 lines +// Assert.Equal("A - Help for Apple", outputLines[3]); +// Assert.Equal("N - Help for Banana", outputLines[4]); +// Assert.Equal("Orange - Help for Orange", outputLines[5]); +// } + +// [Fact] +// public async Task ReceivesInputPrompt() +// { +// string inputScript = +// this.GetInputPromptString( +// PromptCaption, +// PromptMessage, +// PromptFields); + +// var promptTask = this.promptHandlerContext.WaitForInputPrompt(); +// var executeTask = this.powerShellContext.ExecuteScriptString(inputScript); + +// // Wait for the prompt to be shown +// var promptHandler = await promptTask; + +// // Respond to the prompt and wait for execution to complete +// await promptHandler.ReturnInputString("John"); +// await promptHandler.ReturnInputString("40"); +// await promptHandler.ReturnInputString("Windows PowerShell In Action"); +// await promptHandler.ReturnInputString(""); +// await executeTask; + +// string[] outputLines = +// this.GetOutputForType(OutputType.Normal) +// .Split( +// new string[] { Environment.NewLine }, +// StringSplitOptions.None); + +// Assert.Equal(PromptCaption, outputLines[0]); +// Assert.Equal(PromptMessage, outputLines[1]); +// Assert.Equal("Name: John", outputLines[2]); +// Assert.Equal("Age: 40", outputLines[3]); +// Assert.Equal("Books[0]: Windows PowerShell In Action", outputLines[4]); +// Assert.Equal("Books[1]: ", outputLines[5]); +// Assert.Equal("Name John", outputLines[9].Trim()); +// Assert.Equal("Age 40", outputLines[10].Trim()); +// Assert.Equal("Books {Windows PowerShell In Action}", outputLines[11].Trim()); +// } + +// [Fact] +// public async Task CancelsInputPrompt() +// { +// string inputScript = +// this.GetInputPromptString( +// PromptCaption, +// PromptMessage, +// PromptFields); + +// var promptTask = this.promptHandlerContext.WaitForInputPrompt(); +// var executeTask = this.powerShellContext.ExecuteScriptString(inputScript); + +// // Wait for the prompt to be shown +// await promptTask; + +// // Cancel the prompt and wait for execution to complete +// this.consoleService.SendControlC(); +// await executeTask; + +// string[] outputLines = +// this.GetOutputForType(OutputType.Normal) +// .Split( +// new string[] { Environment.NewLine }, +// StringSplitOptions.None); + +// Assert.Equal(PromptCaption, outputLines[0]); +// Assert.Equal(PromptMessage, outputLines[1]); +// Assert.Equal("Name: ", outputLines[2]); +// } + +// [Fact] +// public async Task ReceivesReadHostPrompt() +// { +// var promptTask = this.promptHandlerContext.WaitForInputPrompt(); +// var executeTask = this.powerShellContext.ExecuteScriptString("Read-Host"); + +// // Wait for the prompt to be shown +// TestConsoleInputPromptHandler promptHandler = await promptTask; + +// // Respond to the prompt and wait for execution to complete +// await promptHandler.ReturnInputString("John"); +// await executeTask; + +// string[] outputLines = +// this.GetOutputForType(OutputType.Normal) +// .Split( +// new string[] { Environment.NewLine }, +// StringSplitOptions.None); + +// Assert.Equal("John", outputLines[0]); +// Assert.Equal("John", outputLines[1]); +// } + +// [Fact] +// public async Task CancelsReadHostPrompt() +// { +// var promptTask = this.promptHandlerContext.WaitForInputPrompt(); +// var executeTask = this.powerShellContext.ExecuteScriptString("Read-Host"); + +// // Wait for the prompt to be shown +// await promptTask; + +// // Cancel the prompt and wait for execution to complete +// this.consoleService.SendControlC(); +// await executeTask; + +// // No output will be written from a cancelled Read-Host prompt +// Assert.Null(this.GetOutputForType(OutputType.Normal)); +// } + +// [Fact] +// public async Task ReceivesReadHostPromptWithFieldName() +// { +// var promptTask = this.promptHandlerContext.WaitForInputPrompt(); +// var executeTask = this.powerShellContext.ExecuteScriptString("Read-Host -Prompt \"Name\""); + +// // Wait for the prompt to be shown +// TestConsoleInputPromptHandler promptHandler = await promptTask; + +// // Respond to the prompt and wait for execution to complete +// await promptHandler.ReturnInputString("John"); +// await executeTask; + +// string[] outputLines = +// this.GetOutputForType(OutputType.Normal) +// .Split( +// new string[] { Environment.NewLine }, +// StringSplitOptions.None); + +// Assert.Equal("Name: John", outputLines[0]); +// Assert.Equal("John", outputLines[1]); +// } + +// #region Helper Methods + +// void OnOutputWritten(object sender, OutputWrittenEventArgs e) +// { +// string storedOutputString = null; +// if (!this.outputPerType.TryGetValue(e.OutputType, out storedOutputString)) +// { +// this.outputPerType.Add(e.OutputType, null); +// } + +// if (storedOutputString == null) +// { +// storedOutputString = e.OutputText; +// } +// else +// { +// storedOutputString += e.OutputText; +// } + +// if (e.IncludeNewLine) +// { +// storedOutputString += Environment.NewLine; +// } + +// this.outputPerType[e.OutputType] = storedOutputString; +// } + +// private string GetOutputForType(OutputType outputLineType) +// { +// string outputString = null; + +// this.outputPerType.TryGetValue(outputLineType, out outputString); + +// return outputString; +// } + +// private string GetChoicePromptString( +// string caption, +// string message, +// Tuple[] choices, +// int defaultChoice) +// { +// StringBuilder scriptBuilder = new StringBuilder(); + +// scriptBuilder.AppendFormat( +// "$caption = {0}\r\n", +// caption != null ? +// "\"" + caption + "\"" : +// "$null"); + +// scriptBuilder.AppendFormat( +// "$message = {0}\r\n", +// message != null ? +// "\"" + message + "\"" : +// "$null"); + +// scriptBuilder.AppendLine("$choices = [System.Management.Automation.Host.ChoiceDescription[]]("); + +// List choiceItems = new List(); +// foreach (var choice in choices) +// { +// choiceItems.Add( +// string.Format( +// " (new-Object System.Management.Automation.Host.ChoiceDescription \"{0}\",\"{1}\")", +// choice.Item1, +// choice.Item2)); +// } + +// scriptBuilder.AppendFormat( +// "{0})\r\n", +// string.Join(",\r\n", choiceItems)); + +// scriptBuilder.AppendFormat( +// "$host.ui.PromptForChoice($caption, $message, $choices, {0})\r\n", +// defaultChoice); + +// return scriptBuilder.ToString(); +// } + +// private string GetInputPromptString( +// string caption, +// string message, +// Tuple[] fields) +// { +// StringBuilder scriptBuilder = new StringBuilder(); + +// scriptBuilder.AppendFormat( +// "$caption = {0}\r\n", +// caption != null ? +// "\"" + caption + "\"" : +// "$null"); + +// scriptBuilder.AppendFormat( +// "$message = {0}\r\n", +// message != null ? +// "\"" + message + "\"" : +// "$null"); + +// foreach (var field in fields) +// { +// scriptBuilder.AppendFormat( +// "${0}Field = New-Object System.Management.Automation.Host.FieldDescription \"{0}\"\r\n${0}Field.SetParameterType([{1}])\r\n", +// field.Item1, +// field.Item2.FullName); +// } + +// scriptBuilder.AppendFormat( +// "$fields = [System.Management.Automation.Host.FieldDescription[]]({0})\r\n", +// string.Join( +// ", ", +// fields.Select( +// f => string.Format("${0}Field", f.Item1)))); + +// scriptBuilder.AppendLine( +// "$host.ui.Prompt($caption, $message, $fields)"); + +// return scriptBuilder.ToString(); +// } + +// #endregion +// } + +// internal class TestConsolePromptHandlerContext : IPromptHandlerContext +// { +// private TaskCompletionSource choicePromptShownTask; +// private TaskCompletionSource inputPromptShownTask; + +// public IHostOutput ConsoleHost { get; set; } + +// public ChoicePromptHandler GetChoicePromptHandler() +// { +// return new TestConsoleChoicePromptHandler( +// this.ConsoleHost, +// this.choicePromptShownTask); +// } + +// public InputPromptHandler GetInputPromptHandler() +// { +// return new TestConsoleInputPromptHandler( +// this.ConsoleHost, +// this.inputPromptShownTask); +// } + +// public Task WaitForChoicePrompt() +// { +// this.choicePromptShownTask = new TaskCompletionSource(); +// return this.choicePromptShownTask.Task; +// } + +// public Task WaitForInputPrompt() +// { +// this.inputPromptShownTask = new TaskCompletionSource(); +// return this.inputPromptShownTask.Task; +// } +// } + +// internal class TestConsoleChoicePromptHandler : ConsoleChoicePromptHandler +// { +// private IHostOutput hostOutput; +// private TaskCompletionSource promptShownTask; +// private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + +// private TaskCompletionSource linePromptTask; +// private AsyncQueue> linePromptQueue = +// new AsyncQueue>(); + +// public TestConsoleChoicePromptHandler( +// IHostOutput hostOutput, +// TaskCompletionSource promptShownTask) +// : base(hostOutput) +// { +// this.hostOutput = hostOutput; +// this.promptShownTask = promptShownTask; +// } + +// public async Task ReturnInputString(string inputString) +// { +// var promptTask = await this.linePromptQueue.DequeueAsync(); +// this.hostOutput.WriteOutput(inputString); +// promptTask.SetResult(inputString); +// } + +// protected override async Task ReadInputString(CancellationToken cancellationToken) +// { +// TaskCompletionSource promptTask = new TaskCompletionSource(); +// await this.linePromptQueue.EnqueueAsync(promptTask); + +// if (this.cancellationTokenSource.IsCancellationRequested) +// { +// this.linePromptTask.TrySetCanceled(); +// } + +// this.linePromptTask = promptTask; +// return await promptTask.Task; +// } + +// protected override void ShowPrompt(PromptStyle promptStyle) +// { +// base.ShowPrompt(promptStyle); + +// if (this.promptShownTask != null && +// this.promptShownTask.Task.Status != TaskStatus.RanToCompletion) +// { +// this.promptShownTask.SetResult(this); +// } +// } + +// protected override void OnPromptCancelled() +// { +// this.cancellationTokenSource.Cancel(); + +// if (this.linePromptTask != null) +// { +// this.linePromptTask.TrySetCanceled(); +// } +// } +// } + +// internal class TestConsoleInputPromptHandler : ConsoleInputPromptHandler +// { +// private IHostOutput hostOutput; +// private TaskCompletionSource promptShownTask; +// private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + +// private TaskCompletionSource linePromptTask; +// private AsyncQueue> linePromptQueue = +// new AsyncQueue>(); + +// public TestConsoleInputPromptHandler( +// IHostOutput hostOutput, +// TaskCompletionSource promptShownTask) +// : base(hostOutput) +// { +// this.hostOutput = hostOutput; +// this.promptShownTask = promptShownTask; +// } + +// public async Task ReturnInputString(string inputString) +// { +// var promptTask = await this.linePromptQueue.DequeueAsync(); +// this.hostOutput.WriteOutput(inputString); +// promptTask.SetResult(inputString); +// } + +// protected override async Task ReadInputString(CancellationToken cancellationToken) +// { +// TaskCompletionSource promptTask = new TaskCompletionSource(); +// await this.linePromptQueue.EnqueueAsync(promptTask); + +// if (this.cancellationTokenSource.IsCancellationRequested) +// { +// this.linePromptTask.TrySetCanceled(); +// } + +// this.linePromptTask = promptTask; +// return await promptTask.Task; +// } + +// protected override void ShowFieldPrompt(FieldDetails fieldDetails) +// { +// base.ShowFieldPrompt(fieldDetails); + +// // Raise the task for the first field prompt shown +// if (this.promptShownTask != null && +// this.promptShownTask.Task.Status == TaskStatus.WaitingForActivation) +// { +// this.promptShownTask.SetResult(this); +// } +// } + +// protected override void OnPromptCancelled() +// { +// this.cancellationTokenSource.Cancel(); + +// if (this.linePromptTask != null) +// { +// this.linePromptTask.TrySetCanceled(); +// } +// } +// } +// } \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs index 61d429a91..8afea7c46 100644 --- a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs +++ b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs @@ -8,21 +8,64 @@ using System; using System.IO; using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.PowerShell.EditorServices.Console; +using System.Threading; +using System.Threading.Tasks; +using System.Management.Automation.Host; namespace Microsoft.PowerShell.EditorServices.Test { internal static class PowerShellContextFactory { - public static PowerShellContext Create(ILogger logger) { PowerShellContext powerShellContext = new PowerShellContext(logger); powerShellContext.Initialize( PowerShellContextTests.TestProfilePaths, - PowerShellContext.CreateRunspace(PowerShellContextTests.TestHostDetails, powerShellContext, false), + PowerShellContext.CreateRunspace( + PowerShellContextTests.TestHostDetails, + powerShellContext, + new TestPSHostUserInterface(powerShellContext, logger), + logger), true); return powerShellContext; } } + + public class TestPSHostUserInterface : EditorServicesPSHostUserInterface + { + public TestPSHostUserInterface( + PowerShellContext powerShellContext, + ILogger logger) + : base( + powerShellContext, + new SimplePSHostRawUserInterface(logger), + new NullLogger()) + { + } + + public override void WriteOutput(string outputString, bool includeNewLine, OutputType outputType, ConsoleColor foregroundColor, ConsoleColor backgroundColor) + { + } + + protected override ChoicePromptHandler OnCreateChoicePromptHandler() + { + throw new NotImplementedException(); + } + + protected override InputPromptHandler OnCreateInputPromptHandler() + { + throw new NotImplementedException(); + } + + protected override Task ReadCommandLine(CancellationToken cancellationToken) + { + return Task.FromResult("USER COMMAND"); + } + + protected override void UpdateProgress(long sourceId, ProgressDetails progressDetails) + { + } + } } From d5d5b269c950229bb2387e335a5891ab52625647 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Jun 2017 12:10:03 -0700 Subject: [PATCH 4/5] Remove static Logger class from codebase Since we've finally removed the last usages of the static Logger class, we can now remove it from the codebase. All logging needs should be served by ILogger instances going forward. --- .../EditorServicesHost.cs | 797 +++++++++--------- .../Utility/Logger.cs | 47 -- .../DebugAdapterTests.cs | 2 - .../LanguageServerTests.cs | 2 - .../Utility/LoggerTests.cs | 2 - 5 files changed, 398 insertions(+), 452 deletions(-) delete mode 100644 src/PowerShellEditorServices/Utility/Logger.cs diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index a928ebb3e..182166732 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -1,402 +1,401 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; -using Microsoft.PowerShell.EditorServices.Protocol.Server; -using Microsoft.PowerShell.EditorServices.Session; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Management.Automation.Runspaces; -using System.Reflection; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Host -{ - public enum EditorServicesHostStatus - { - Started, - Failed, - Ended - } - - /// - /// Provides a simplified interface for hosting the language and debug services - /// over the named pipe server protocol. - /// - public class EditorServicesHost - { - #region Private Fields - - private ILogger logger; - private bool enableConsoleRepl; - private HostDetails hostDetails; - private ProfilePaths profilePaths; - private string bundledModulesPath; - private DebugAdapter debugAdapter; - private EditorSession editorSession; - private HashSet featureFlags; - private LanguageServer languageServer; - - private TcpSocketServerListener languageServiceListener; - private TcpSocketServerListener debugServiceListener; - - private TaskCompletionSource serverCompletedTask; - - #endregion - - #region Properties - - public EditorServicesHostStatus Status { get; private set; } - - public int LanguageServicePort { get; private set; } - - public int DebugServicePort { get; private set; } - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the EditorServicesHost class and waits for - /// the debugger to attach if waitForDebugger is true. - /// - /// The details of the host which is launching PowerShell Editor Services. - /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. - /// If true, causes the host to wait for the debugger to attach before proceeding. - public EditorServicesHost( - HostDetails hostDetails, - string bundledModulesPath, - bool enableConsoleRepl, - bool waitForDebugger, - string[] featureFlags) - { - Validate.IsNotNull(nameof(hostDetails), hostDetails); - - this.hostDetails = hostDetails; - this.enableConsoleRepl = enableConsoleRepl; - this.bundledModulesPath = bundledModulesPath; - this.featureFlags = new HashSet(featureFlags ?? new string[0]); - -#if DEBUG - if (waitForDebugger) - { - if (Debugger.IsAttached) - { - Debugger.Break(); - } - else - { - Debugger.Launch(); - } - } -#endif - - // Catch unhandled exceptions for logging purposes -#if !CoreCLR - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; -#endif - } - - #endregion - - #region Public Methods - - /// - /// Starts the Logger for the specified file path and log level. - /// - /// The path of the log file to be written. - /// The minimum level of log messages to be written. - public void StartLogging(string logFilePath, LogLevel logLevel) - { - this.logger = new FileLogger(logFilePath, logLevel); - Logger.Initialize(this.logger); - -#if CoreCLR - FileVersionInfo fileVersionInfo = - FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); - - // TODO #278: Need the correct dependency package for this to work correctly - //string osVersionString = RuntimeInformation.OSDescription; - //string processArchitecture = RuntimeInformation.ProcessArchitecture == Architecture.X64 ? "64-bit" : "32-bit"; - //string osArchitecture = RuntimeInformation.OSArchitecture == Architecture.X64 ? "64-bit" : "32-bit"; -#else - FileVersionInfo fileVersionInfo = - FileVersionInfo.GetVersionInfo(this.GetType().Assembly.Location); - string osVersionString = Environment.OSVersion.VersionString; - string processArchitecture = Environment.Is64BitProcess ? "64-bit" : "32-bit"; - string osArchitecture = Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit"; -#endif - - string newLine = Environment.NewLine; - - this.logger.Write( - LogLevel.Normal, - string.Format( - $"PowerShell Editor Services Host v{fileVersionInfo.FileVersion} starting (pid {Process.GetCurrentProcess().Id})..." + newLine + newLine + - " Host application details:" + newLine + newLine + - $" Name: {this.hostDetails.Name}" + newLine + - $" ProfileId: {this.hostDetails.ProfileId}" + newLine + - $" Version: {this.hostDetails.Version}" + newLine + -#if !CoreCLR - $" Arch: {processArchitecture}" + newLine + newLine + - " Operating system details:" + newLine + newLine + - $" Version: {osVersionString}" + newLine + - $" Arch: {osArchitecture}")); -#else - "")); -#endif - } - - /// - /// Starts the language service with the specified TCP socket port. - /// - /// The port number for the language service. - /// The object containing the profile paths to load for this session. - public void StartLanguageService(int languageServicePort, ProfilePaths profilePaths) - { - this.profilePaths = profilePaths; - - this.languageServiceListener = - new TcpSocketServerListener( - MessageProtocolType.LanguageServer, - languageServicePort, - this.logger); - - this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnect; - this.languageServiceListener.Start(); - - this.logger.Write( - LogLevel.Normal, - string.Format( - "Language service started, listening on port {0}", - languageServicePort)); - } - - private async void OnLanguageServiceClientConnect( - object sender, - TcpSocketServerChannel serverChannel) - { - MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger); - - ProtocolEndpoint protocolEndpoint = - new ProtocolEndpoint( - serverChannel, - messageDispatcher, - this.logger); - - this.editorSession = - CreateSession( - this.hostDetails, - this.profilePaths, - protocolEndpoint, - messageDispatcher, - this.enableConsoleRepl); - - this.languageServer = - new LanguageServer( - this.editorSession, - messageDispatcher, - protocolEndpoint, - this.logger); - - await this.editorSession.PowerShellContext.ImportCommandsModule( - Path.Combine( - Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), - @"..\..\Commands")); - - this.languageServer.Start(); - protocolEndpoint.Start(); - } - - /// - /// Starts the debug service with the specified TCP socket port. - /// - /// The port number for the debug service. - public void StartDebugService( - int debugServicePort, - ProfilePaths profilePaths, - bool useExistingSession) - { - this.debugServiceListener = - new TcpSocketServerListener( - MessageProtocolType.DebugAdapter, - debugServicePort, - this.logger); - - this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect; - this.debugServiceListener.Start(); - - this.logger.Write( - LogLevel.Normal, - string.Format( - "Debug service started, listening on port {0}", - debugServicePort)); - } - - private void OnDebugServiceClientConnect(object sender, TcpSocketServerChannel serverChannel) - { - MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger); - - ProtocolEndpoint protocolEndpoint = - new ProtocolEndpoint( - serverChannel, - messageDispatcher, - this.logger); - - if (this.enableConsoleRepl) - { - this.debugAdapter = - new DebugAdapter( - this.editorSession, - false, - messageDispatcher, - protocolEndpoint, - this.logger); - } - else - { - EditorSession debugSession = - this.CreateDebugSession( - this.hostDetails, - profilePaths, - protocolEndpoint, - messageDispatcher, - this.languageServer?.EditorOperations, - false); - - this.debugAdapter = - new DebugAdapter( - debugSession, - true, - messageDispatcher, - protocolEndpoint, - this.logger); - } - - this.debugAdapter.SessionEnded += - (obj, args) => - { - this.logger.Write( - LogLevel.Normal, - "Previous debug session ended, restarting debug service listener..."); - - this.debugServiceListener.Start(); - }; - - this.debugAdapter.Start(); - protocolEndpoint.Start(); - } - - /// - /// Stops the language or debug services if either were started. - /// - public void StopServices() - { - // TODO: Need a new way to shut down the services - - this.languageServer = null; - - this.debugAdapter = null; - } - - /// - /// Waits for either the language or debug service to shut down. - /// - public void WaitForCompletion() - { - // TODO: We need a way to know when to complete this task! - this.serverCompletedTask = new TaskCompletionSource(); - this.serverCompletedTask.Task.Wait(); - } - - #endregion - - #region Private Methods - - private EditorSession CreateSession( - HostDetails hostDetails, - ProfilePaths profilePaths, - IMessageSender messageSender, - IMessageHandlers messageHandlers, - bool enableConsoleRepl) - { - EditorSession editorSession = new EditorSession(this.logger); - PowerShellContext powerShellContext = new PowerShellContext(this.logger); - - EditorServicesPSHostUserInterface hostUserInterface = - enableConsoleRepl - ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger) - : new ProtocolPSHostUserInterface(powerShellContext, messageSender, messageHandlers, this.logger); - - EditorServicesPSHost psHost = - new EditorServicesPSHost( - powerShellContext, - hostDetails, - hostUserInterface, - this.logger); - - Runspace initialRunspace = PowerShellContext.CreateRunspace(psHost); - powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); - - editorSession.StartSession(powerShellContext, hostUserInterface); - - return editorSession; - } - - private EditorSession CreateDebugSession( - HostDetails hostDetails, - ProfilePaths profilePaths, - IMessageSender messageSender, - IMessageHandlers messageHandlers, - IEditorOperations editorOperations, - bool enableConsoleRepl) - { - EditorSession editorSession = new EditorSession(this.logger); - PowerShellContext powerShellContext = new PowerShellContext(this.logger); - - EditorServicesPSHostUserInterface hostUserInterface = - enableConsoleRepl - ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger) - : new ProtocolPSHostUserInterface(powerShellContext, messageSender, messageHandlers, this.logger); - - EditorServicesPSHost psHost = - new EditorServicesPSHost( - powerShellContext, - hostDetails, +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using Microsoft.PowerShell.EditorServices.Protocol.Server; +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Management.Automation.Runspaces; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Host +{ + public enum EditorServicesHostStatus + { + Started, + Failed, + Ended + } + + /// + /// Provides a simplified interface for hosting the language and debug services + /// over the named pipe server protocol. + /// + public class EditorServicesHost + { + #region Private Fields + + private ILogger logger; + private bool enableConsoleRepl; + private HostDetails hostDetails; + private ProfilePaths profilePaths; + private string bundledModulesPath; + private DebugAdapter debugAdapter; + private EditorSession editorSession; + private HashSet featureFlags; + private LanguageServer languageServer; + + private TcpSocketServerListener languageServiceListener; + private TcpSocketServerListener debugServiceListener; + + private TaskCompletionSource serverCompletedTask; + + #endregion + + #region Properties + + public EditorServicesHostStatus Status { get; private set; } + + public int LanguageServicePort { get; private set; } + + public int DebugServicePort { get; private set; } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the EditorServicesHost class and waits for + /// the debugger to attach if waitForDebugger is true. + /// + /// The details of the host which is launching PowerShell Editor Services. + /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. + /// If true, causes the host to wait for the debugger to attach before proceeding. + public EditorServicesHost( + HostDetails hostDetails, + string bundledModulesPath, + bool enableConsoleRepl, + bool waitForDebugger, + string[] featureFlags) + { + Validate.IsNotNull(nameof(hostDetails), hostDetails); + + this.hostDetails = hostDetails; + this.enableConsoleRepl = enableConsoleRepl; + this.bundledModulesPath = bundledModulesPath; + this.featureFlags = new HashSet(featureFlags ?? new string[0]); + +#if DEBUG + if (waitForDebugger) + { + if (Debugger.IsAttached) + { + Debugger.Break(); + } + else + { + Debugger.Launch(); + } + } +#endif + + // Catch unhandled exceptions for logging purposes +#if !CoreCLR + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; +#endif + } + + #endregion + + #region Public Methods + + /// + /// Starts the Logger for the specified file path and log level. + /// + /// The path of the log file to be written. + /// The minimum level of log messages to be written. + public void StartLogging(string logFilePath, LogLevel logLevel) + { + this.logger = new FileLogger(logFilePath, logLevel); + +#if CoreCLR + FileVersionInfo fileVersionInfo = + FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); + + // TODO #278: Need the correct dependency package for this to work correctly + //string osVersionString = RuntimeInformation.OSDescription; + //string processArchitecture = RuntimeInformation.ProcessArchitecture == Architecture.X64 ? "64-bit" : "32-bit"; + //string osArchitecture = RuntimeInformation.OSArchitecture == Architecture.X64 ? "64-bit" : "32-bit"; +#else + FileVersionInfo fileVersionInfo = + FileVersionInfo.GetVersionInfo(this.GetType().Assembly.Location); + string osVersionString = Environment.OSVersion.VersionString; + string processArchitecture = Environment.Is64BitProcess ? "64-bit" : "32-bit"; + string osArchitecture = Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit"; +#endif + + string newLine = Environment.NewLine; + + this.logger.Write( + LogLevel.Normal, + string.Format( + $"PowerShell Editor Services Host v{fileVersionInfo.FileVersion} starting (pid {Process.GetCurrentProcess().Id})..." + newLine + newLine + + " Host application details:" + newLine + newLine + + $" Name: {this.hostDetails.Name}" + newLine + + $" ProfileId: {this.hostDetails.ProfileId}" + newLine + + $" Version: {this.hostDetails.Version}" + newLine + +#if !CoreCLR + $" Arch: {processArchitecture}" + newLine + newLine + + " Operating system details:" + newLine + newLine + + $" Version: {osVersionString}" + newLine + + $" Arch: {osArchitecture}")); +#else + "")); +#endif + } + + /// + /// Starts the language service with the specified TCP socket port. + /// + /// The port number for the language service. + /// The object containing the profile paths to load for this session. + public void StartLanguageService(int languageServicePort, ProfilePaths profilePaths) + { + this.profilePaths = profilePaths; + + this.languageServiceListener = + new TcpSocketServerListener( + MessageProtocolType.LanguageServer, + languageServicePort, + this.logger); + + this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnect; + this.languageServiceListener.Start(); + + this.logger.Write( + LogLevel.Normal, + string.Format( + "Language service started, listening on port {0}", + languageServicePort)); + } + + private async void OnLanguageServiceClientConnect( + object sender, + TcpSocketServerChannel serverChannel) + { + MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger); + + ProtocolEndpoint protocolEndpoint = + new ProtocolEndpoint( + serverChannel, + messageDispatcher, + this.logger); + + this.editorSession = + CreateSession( + this.hostDetails, + this.profilePaths, + protocolEndpoint, + messageDispatcher, + this.enableConsoleRepl); + + this.languageServer = + new LanguageServer( + this.editorSession, + messageDispatcher, + protocolEndpoint, + this.logger); + + await this.editorSession.PowerShellContext.ImportCommandsModule( + Path.Combine( + Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), + @"..\..\Commands")); + + this.languageServer.Start(); + protocolEndpoint.Start(); + } + + /// + /// Starts the debug service with the specified TCP socket port. + /// + /// The port number for the debug service. + public void StartDebugService( + int debugServicePort, + ProfilePaths profilePaths, + bool useExistingSession) + { + this.debugServiceListener = + new TcpSocketServerListener( + MessageProtocolType.DebugAdapter, + debugServicePort, + this.logger); + + this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect; + this.debugServiceListener.Start(); + + this.logger.Write( + LogLevel.Normal, + string.Format( + "Debug service started, listening on port {0}", + debugServicePort)); + } + + private void OnDebugServiceClientConnect(object sender, TcpSocketServerChannel serverChannel) + { + MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger); + + ProtocolEndpoint protocolEndpoint = + new ProtocolEndpoint( + serverChannel, + messageDispatcher, + this.logger); + + if (this.enableConsoleRepl) + { + this.debugAdapter = + new DebugAdapter( + this.editorSession, + false, + messageDispatcher, + protocolEndpoint, + this.logger); + } + else + { + EditorSession debugSession = + this.CreateDebugSession( + this.hostDetails, + profilePaths, + protocolEndpoint, + messageDispatcher, + this.languageServer?.EditorOperations, + false); + + this.debugAdapter = + new DebugAdapter( + debugSession, + true, + messageDispatcher, + protocolEndpoint, + this.logger); + } + + this.debugAdapter.SessionEnded += + (obj, args) => + { + this.logger.Write( + LogLevel.Normal, + "Previous debug session ended, restarting debug service listener..."); + + this.debugServiceListener.Start(); + }; + + this.debugAdapter.Start(); + protocolEndpoint.Start(); + } + + /// + /// Stops the language or debug services if either were started. + /// + public void StopServices() + { + // TODO: Need a new way to shut down the services + + this.languageServer = null; + + this.debugAdapter = null; + } + + /// + /// Waits for either the language or debug service to shut down. + /// + public void WaitForCompletion() + { + // TODO: We need a way to know when to complete this task! + this.serverCompletedTask = new TaskCompletionSource(); + this.serverCompletedTask.Task.Wait(); + } + + #endregion + + #region Private Methods + + private EditorSession CreateSession( + HostDetails hostDetails, + ProfilePaths profilePaths, + IMessageSender messageSender, + IMessageHandlers messageHandlers, + bool enableConsoleRepl) + { + EditorSession editorSession = new EditorSession(this.logger); + PowerShellContext powerShellContext = new PowerShellContext(this.logger); + + EditorServicesPSHostUserInterface hostUserInterface = + enableConsoleRepl + ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger) + : new ProtocolPSHostUserInterface(powerShellContext, messageSender, messageHandlers, this.logger); + + EditorServicesPSHost psHost = + new EditorServicesPSHost( + powerShellContext, + hostDetails, + hostUserInterface, + this.logger); + + Runspace initialRunspace = PowerShellContext.CreateRunspace(psHost); + powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); + + editorSession.StartSession(powerShellContext, hostUserInterface); + + return editorSession; + } + + private EditorSession CreateDebugSession( + HostDetails hostDetails, + ProfilePaths profilePaths, + IMessageSender messageSender, + IMessageHandlers messageHandlers, + IEditorOperations editorOperations, + bool enableConsoleRepl) + { + EditorSession editorSession = new EditorSession(this.logger); + PowerShellContext powerShellContext = new PowerShellContext(this.logger); + + EditorServicesPSHostUserInterface hostUserInterface = + enableConsoleRepl + ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger) + : new ProtocolPSHostUserInterface(powerShellContext, messageSender, messageHandlers, this.logger); + + EditorServicesPSHost psHost = + new EditorServicesPSHost( + powerShellContext, + hostDetails, hostUserInterface, this.logger); - - Runspace initialRunspace = PowerShellContext.CreateRunspace(psHost); - powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); - - editorSession.StartDebugSession( - powerShellContext, - editorOperations); - - return editorSession; - } - -#if !CoreCLR - private void CurrentDomain_UnhandledException( - object sender, - UnhandledExceptionEventArgs e) - { - // Log the exception - this.logger.Write( - LogLevel.Error, - string.Format( - "FATAL UNHANDLED EXCEPTION:\r\n\r\n{0}", - e.ExceptionObject.ToString())); - } -#endif - - #endregion - } + + Runspace initialRunspace = PowerShellContext.CreateRunspace(psHost); + powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); + + editorSession.StartDebugSession( + powerShellContext, + editorOperations); + + return editorSession; + } + +#if !CoreCLR + private void CurrentDomain_UnhandledException( + object sender, + UnhandledExceptionEventArgs e) + { + // Log the exception + this.logger.Write( + LogLevel.Error, + string.Format( + "FATAL UNHANDLED EXCEPTION:\r\n\r\n{0}", + e.ExceptionObject.ToString())); + } +#endif + + #endregion + } } \ No newline at end of file diff --git a/src/PowerShellEditorServices/Utility/Logger.cs b/src/PowerShellEditorServices/Utility/Logger.cs deleted file mode 100644 index 930fcc369..000000000 --- a/src/PowerShellEditorServices/Utility/Logger.cs +++ /dev/null @@ -1,47 +0,0 @@ -// -// 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.Utility -{ - /// - /// Provides a simple logging interface. May be replaced with a - /// more robust solution at a later date. - /// - public static class Logger - { - /// - /// Gets the current static ILogger instance. This property - /// is temporary and will be removed in an upcoming commit. - /// - public static ILogger CurrentLogger { get; private set; } - - /// - /// Initializes the Logger for the current session. - /// - /// - /// Specifies the ILogger implementation to use for the static interface. - /// - public static void Initialize(ILogger logger) - { - if (CurrentLogger != null) - { - CurrentLogger.Dispose(); - } - - CurrentLogger = logger; - } - - /// - /// Closes the Logger. - /// - public static void Close() - { - if (CurrentLogger != null) - { - CurrentLogger.Dispose(); - } - } - } -} diff --git a/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs b/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs index b6909a036..16b7ef62d 100644 --- a/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs +++ b/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs @@ -40,8 +40,6 @@ public async Task InitializeAsync() testLogPath + "-client.log", LogLevel.Verbose); - Logger.Initialize(this.logger); - testLogPath += "-server.log"; System.Console.WriteLine(" Output log at path: {0}", testLogPath); diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index a89a7e264..532808bd9 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -43,8 +43,6 @@ public async Task InitializeAsync() testLogPath + "-client.log", LogLevel.Verbose); - Logger.Initialize(this.logger); - testLogPath += "-server.log"; System.Console.WriteLine(" Output log at path: {0}", testLogPath); diff --git a/test/PowerShellEditorServices.Test/Utility/LoggerTests.cs b/test/PowerShellEditorServices.Test/Utility/LoggerTests.cs index a343ad4a9..5ab202675 100644 --- a/test/PowerShellEditorServices.Test/Utility/LoggerTests.cs +++ b/test/PowerShellEditorServices.Test/Utility/LoggerTests.cs @@ -118,8 +118,6 @@ private string GetLogLevelName(LogLevel logLevel) private string ReadLogContents() { - Logger.Close(); - return string.Join( "\r\n", From d10c0cc5516dc4cc48c9c2f1e72c5a2a38e82bc5 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Jun 2017 12:11:03 -0700 Subject: [PATCH 5/5] Add 'Build Release Configuration' task in tasks.json --- .vscode/tasks.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 24a5f3504..da0904197 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -27,7 +27,14 @@ "taskName": "Build", "suppressTaskName": true, "isBuildCommand": true, - "args": [ "Invoke-Build Build" ] + "args": [ "Invoke-Build Build" ], + "problemMatcher": "$msCompile" + }, + { + "taskName": "Build Release Configuration", + "suppressTaskName": true, + "args": [ "Invoke-Build Build -Configuration Release" ], + "problemMatcher": "$msCompile" }, { "taskName": "Test",