From dd116ccde0bcdb657a1462ead7a34948cc5039ae Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 7 Aug 2019 19:33:38 -0700 Subject: [PATCH 1/3] Add powershellcontext --- .../Hosting/EditorServicesHost.cs | 125 +- .../PowerShellEditorServices.Engine.csproj | 19 +- .../Components/ComponentRegistry.cs | 84 + .../Components/IComponentRegistry.cs | 61 + .../IComponentRegistryExtensions.cs | 87 + .../Console/ChoiceDetails.cs | 132 + .../Console/ChoicePromptHandler.cs | 354 +++ .../Console/CollectionFieldDetails.cs | 138 + .../Console/ConsoleChoicePromptHandler.cs | 133 + .../Console/ConsoleInputPromptHandler.cs | 102 + .../PowerShellContext/Console/ConsoleProxy.cs | 196 ++ .../Console/ConsoleReadLine.cs | 616 ++++ .../Console/CredentialFieldDetails.cs | 122 + .../PowerShellContext/Console/FieldDetails.cs | 234 ++ .../Console/IConsoleOperations.cs | 140 + .../Console/InputPromptHandler.cs | 331 +++ .../Console/PromptHandler.cs | 55 + .../Console/TerminalChoicePromptHandler.cs | 62 + .../Console/TerminalInputPromptHandler.cs | 79 + .../Console/UnixConsoleOperations.cs | 298 ++ .../Console/WindowsConsoleOperations.cs | 76 + .../Extensions/EditorCommand.cs | 89 + .../Extensions/EditorCommandAttribute.cs | 33 + .../Extensions/EditorContext.cs | 117 + .../Extensions/EditorObject.cs | 111 + .../Extensions/EditorWindow.cs | 83 + .../Extensions/EditorWorkspace.cs | 76 + .../Extensions/ExtensionService.cs | 217 ++ .../Extensions/FileContext.cs | 280 ++ .../Extensions/IEditorOperations.cs | 128 + .../PowerShellContextService.cs | 2506 +++++++++++++++++ .../Capabilities/DscBreakpointCapability.cs | 166 ++ .../Session/ExecutionOptions.cs | 92 + .../Session/ExecutionStatus.cs | 39 + .../ExecutionStatusChangedEventArgs.cs | 52 + .../Session/ExecutionTarget.cs | 28 + .../Session/Host/EditorServicesPSHost.cs | 373 +++ .../Host/EditorServicesPSHostUserInterface.cs | 1068 +++++++ .../Session/Host/IHostInput.cs | 28 + .../Session/Host/IHostOutput.cs | 175 ++ .../Host/SimplePSHostRawUserInterface.cs | 225 ++ .../Host/TerminalPSHostRawUserInterface.cs | 330 +++ .../Host/TerminalPSHostUserInterface.cs | 180 ++ .../Session/IPromptContext.cs | 67 + .../Session/IRunspaceCapability.cs | 12 + .../Session/IVersionSpecificOperations.cs | 33 + .../Session/InvocationEventQueue.cs | 263 ++ .../Session/LegacyReadLineContext.cs | 56 + .../PowerShellContext/Session/OutputType.cs | 41 + .../Session/OutputWrittenEventArgs.cs | 65 + .../Session/PSReadLinePromptContext.cs | 203 ++ .../Session/PSReadLineProxy.cs | 118 + .../Session/PipelineExecutionRequest.cs | 80 + .../Session/PowerShell5Operations.cs | 107 + .../Session/PowerShellContextState.cs | 47 + .../Session/PowerShellExecutionResult.cs | 40 + .../Session/PowerShellVersionDetails.cs | 166 ++ .../Session/ProgressDetails.cs | 33 + .../PowerShellContext/Session/PromptNest.cs | 564 ++++ .../Session/PromptNestFrame.cs | 137 + .../Session/PromptNestFrameType.cs | 21 + .../Session/RemoteFileManager.cs | 785 ++++++ .../Session/RunspaceChangedEventArgs.cs | 67 + .../Session/RunspaceDetails.cs | 321 +++ .../Session/RunspaceHandle.cs | 60 + .../Session/SessionDetails.cs | 63 + .../Session/SessionStateChangedEventArgs.cs | 48 + .../Session/ThreadController.cs | 131 + .../Handlers/ConfigurationHandler.cs | 45 +- .../Utility/AsyncLock.cs | 128 + .../Utility/AsyncQueue.cs | 224 ++ .../Utility/AsyncUtils.cs | 25 + .../EditorServicesHost.cs | 6 + .../PowerShellEditorServices.Host.csproj | 2 +- 74 files changed, 13742 insertions(+), 56 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs create mode 100644 src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs create mode 100644 src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs create mode 100644 src/PowerShellEditorServices.Engine/Utility/AsyncUtils.cs diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 9512b7918..017f99a60 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -4,10 +4,13 @@ // using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Host; +using System.Management.Automation.Runspaces; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; @@ -60,11 +63,19 @@ public class EditorServicesHost private readonly HostDetails _hostDetails; + private readonly PSHost _internalHost; + + private readonly bool _enableConsoleRepl; + + private readonly HashSet _featureFlags; + + private readonly string[] _additionalModules; + private ILanguageServer _languageServer; - private readonly Extensions.Logging.ILogger _logger; + private Microsoft.Extensions.Logging.ILogger _logger; - private readonly ILoggerFactory _factory; + private ILoggerFactory _factory; #endregion @@ -126,24 +137,15 @@ public EditorServicesHost( Validate.IsNotNull(nameof(internalHost), internalHost); _serviceCollection = new ServiceCollection(); - - Log.Logger = new LoggerConfiguration().Enrich.FromLogContext() - .WriteTo.Console() - .CreateLogger(); - _factory = new LoggerFactory().AddSerilog(Log.Logger); - _logger = _factory.CreateLogger(); - _hostDetails = hostDetails; - /* - this.hostDetails = hostDetails; - this.enableConsoleRepl = enableConsoleRepl; - this.bundledModulesPath = bundledModulesPath; - this.additionalModules = additionalModules ?? Array.Empty(); - this.featureFlags = new HashSet(featureFlags ?? Array.Empty(); - this.serverCompletedTask = new TaskCompletionSource(); - this.internalHost = internalHost; - */ + //this._hostDetails = hostDetails; + this._enableConsoleRepl = enableConsoleRepl; + //this.bundledModulesPath = bundledModulesPath; + this._additionalModules = additionalModules ?? Array.Empty(); + this._featureFlags = new HashSet(featureFlags ?? Array.Empty()); + //this.serverCompletedTask = new TaskCompletionSource(); + this._internalHost = internalHost; #if DEBUG if (waitForDebugger) @@ -174,6 +176,12 @@ public EditorServicesHost( /// The minimum level of log messages to be written. public void StartLogging(string logFilePath, PsesLogLevel logLevel) { + Log.Logger = new LoggerConfiguration().Enrich.FromLogContext() + .WriteTo.File(logFilePath) + .CreateLogger(); + _factory = new LoggerFactory().AddSerilog(Log.Logger); + _logger = _factory.CreateLogger(); + FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); @@ -184,7 +192,7 @@ public void StartLogging(string logFilePath, PsesLogLevel logLevel) string buildTime = BuildInfo.BuildTime?.ToString("s", System.Globalization.CultureInfo.InvariantCulture) ?? ""; string logHeader = $@" -PowerShell Editor Services Host v{fileVersionInfo.FileVersion} starting (PID {Process.GetCurrentProcess().Id} +PowerShell Editor Services Host v{fileVersionInfo.FileVersion} starting (PID {Process.GetCurrentProcess().Id}) Host application details: @@ -219,23 +227,69 @@ public void StartLanguageService( { while (System.Diagnostics.Debugger.IsAttached) { - Console.WriteLine($"{Process.GetCurrentProcess().Id}"); + System.Console.WriteLine($"{Process.GetCurrentProcess().Id}"); Thread.Sleep(2000); } _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); - _serviceCollection.AddSingleton(); - _serviceCollection.AddSingleton(); - _serviceCollection.AddSingleton(); - _serviceCollection.AddSingleton( - (provider) => { - return AnalysisService.Create( - provider.GetService(), - provider.GetService(), - _factory.CreateLogger()); - } - ); + var logger = _factory.CreateLogger(); + var powerShellContext = new PowerShellContextService( + logger, + _featureFlags.Contains("PSReadLine")); + + // TODO: Bring this back + //EditorServicesPSHostUserInterface hostUserInterface = + // _enableConsoleRepl + // ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost) + // : new ProtocolPSHostUserInterface(powerShellContext, messageSender, logger); + EditorServicesPSHostUserInterface hostUserInterface = + (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost); + + + EditorServicesPSHost psHost = + new EditorServicesPSHost( + powerShellContext, + _hostDetails, + hostUserInterface, + logger); + + Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); + powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); + + powerShellContext.ImportCommandsModuleAsync( + Path.Combine( + Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), + @"..\Commands")); + + // TODO: This can be moved to the point after the $psEditor object + // gets initialized when that is done earlier than LanguageServer.Initialize + foreach (string module in this._additionalModules) + { + var command = + new System.Management.Automation.PSCommand() + .AddCommand("Microsoft.PowerShell.Core\\Import-Module") + .AddParameter("Name", module); + + powerShellContext.ExecuteCommandAsync( + command, + sendOutputToHost: false, + sendErrorToHost: true); + } + + _serviceCollection + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(powerShellContext) + .AddSingleton( + (provider) => { + return AnalysisService.Create( + provider.GetService(), + provider.GetService(), + _factory.CreateLogger()); + } + ); _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) { @@ -248,10 +302,11 @@ public void StartLanguageService( _logger.LogInformation("Starting language server"); - Task.Factory.StartNew(() => _languageServer.StartAsync(), - CancellationToken.None, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); + Task.Run(_languageServer.StartAsync); + //Task.Factory.StartNew(() => _languageServer.StartAsync(), + // CancellationToken.None, + // TaskCreationOptions.LongRunning, + // TaskScheduler.Default); _logger.LogInformation( string.Format( diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj index c0fb4a3df..ea20c7c90 100644 --- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj +++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj @@ -10,15 +10,32 @@ Latest + + latest + + + + + + latest + - + + + + + + + + + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs new file mode 100644 index 000000000..9a1de6d01 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Components +{ + /// + /// Provides a default implementation for the IComponentRegistry + /// interface. + /// + public class ComponentRegistry : IComponentRegistry + { + private Dictionary componentRegistry = + new Dictionary(); + + /// + /// Registers an instance of the specified component type + /// or throws an ArgumentException if an instance has + /// already been registered. + /// + /// + /// The component type that the instance represents. + /// + /// + /// The instance of the component to be registered. + /// + /// + /// The provided component instance for convenience in assignment + /// statements. + /// + public object Register(Type componentType, object componentInstance) + { + this.componentRegistry.Add(componentType, componentInstance); + return componentInstance; + } + + + /// + /// Gets the registered instance of the specified + /// component type or throws a KeyNotFoundException if + /// no instance has been registered. + /// + /// + /// The component type for which an instance will be retrieved. + /// + /// The implementation of the specified type. + public object Get(Type componentType) + { + return this.componentRegistry[componentType]; + } + + /// + /// Attempts to retrieve the instance of the specified + /// component type and, if found, stores it in the + /// componentInstance parameter. + /// + /// + /// The out parameter in which the found instance will be stored. + /// + /// + /// The component type for which an instance will be retrieved. + /// + /// + /// True if a registered instance was found, false otherwise. + /// + public bool TryGet(Type componentType, out object componentInstance) + { + componentInstance = null; + + if (this.componentRegistry.TryGetValue(componentType, out componentInstance)) + { + return componentInstance != null; + } + + return false; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs new file mode 100644 index 000000000..1acd59588 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Components +{ + /// + /// Specifies the contract for a registry of component interfaces. + /// + public interface IComponentRegistry + { + /// + /// Registers an instance of the specified component type + /// or throws an ArgumentException if an instance has + /// already been registered. + /// + /// + /// The component type that the instance represents. + /// + /// + /// The instance of the component to be registered. + /// + /// + /// The provided component instance for convenience in assignment + /// statements. + /// + object Register( + Type componentType, + object componentInstance); + + /// + /// Gets the registered instance of the specified + /// component type or throws a KeyNotFoundException if + /// no instance has been registered. + /// + /// + /// The component type for which an instance will be retrieved. + /// + /// The implementation of the specified type. + object Get(Type componentType); + + /// + /// Attempts to retrieve the instance of the specified + /// component type and, if found, stores it in the + /// componentInstance parameter. + /// + /// + /// The component type for which an instance will be retrieved. + /// + /// + /// The out parameter in which the found instance will be stored. + /// + /// + /// True if a registered instance was found, false otherwise. + /// + bool TryGet(Type componentType, out object componentInstance); + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs new file mode 100644 index 000000000..cbbb119c1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs @@ -0,0 +1,87 @@ +// +// 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.Components +{ + /// + /// Provides generic helper methods for working with IComponentRegistry + /// methods. + /// + public static class IComponentRegistryExtensions + { + /// + /// Registers an instance of the specified component type + /// or throws an ArgumentException if an instance has + /// already been registered. + /// + /// + /// The IComponentRegistry instance. + /// + /// + /// The instance of the component to be registered. + /// + /// + /// The provided component instance for convenience in assignment + /// statements. + /// + public static TComponent Register( + this IComponentRegistry componentRegistry, + TComponent componentInstance) + where TComponent : class + { + return + (TComponent)componentRegistry.Register( + typeof(TComponent), + componentInstance); + } + + /// + /// Gets the registered instance of the specified + /// component type or throws a KeyNotFoundException if + /// no instance has been registered. + /// + /// + /// The IComponentRegistry instance. + /// + /// The implementation of the specified type. + public static TComponent Get( + this IComponentRegistry componentRegistry) + where TComponent : class + { + return (TComponent)componentRegistry.Get(typeof(TComponent)); + } + + /// + /// Attempts to retrieve the instance of the specified + /// component type and, if found, stores it in the + /// componentInstance parameter. + /// + /// + /// The IComponentRegistry instance. + /// + /// + /// The out parameter in which the found instance will be stored. + /// + /// + /// True if a registered instance was found, false otherwise. + /// + public static bool TryGet( + this IComponentRegistry componentRegistry, + out TComponent componentInstance) + where TComponent : class + { + object componentObject = null; + componentInstance = null; + + if (componentRegistry.TryGet(typeof(TComponent), out componentObject)) + { + componentInstance = componentObject as TComponent; + return componentInstance != null; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs new file mode 100644 index 000000000..d8121b6c1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs @@ -0,0 +1,132 @@ +// +// 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.Management.Automation.Host; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Contains the details about a choice that should be displayed + /// to the user. This class is meant to be serializable to the + /// user's UI. + /// + public class ChoiceDetails + { + #region Private Fields + + private string hotKeyString; + + #endregion + + #region Properties + + /// + /// Gets the label for the choice. + /// + public string Label { get; set; } + + /// + /// Gets the index of the hot key character for the choice. + /// + public int HotKeyIndex { get; set; } + + /// + /// Gets the hot key character. + /// + public char? HotKeyCharacter { get; set; } + + /// + /// Gets the help string that describes the choice. + /// + public string HelpMessage { get; set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ChoiceDetails class with + /// the provided details. + /// + public ChoiceDetails() + { + // Parameterless constructor for deserialization. + } + + /// + /// Creates an instance of the ChoiceDetails class with + /// the provided details. + /// + /// + /// The label of the choice. An ampersand '&' may be inserted + /// before the character that will used as a hot key for the + /// choice. + /// + /// + /// A help message that describes the purpose of the choice. + /// + public ChoiceDetails(string label, string helpMessage) + { + this.HelpMessage = helpMessage; + + this.HotKeyIndex = label.IndexOf('&'); + if (this.HotKeyIndex >= 0) + { + this.Label = label.Remove(this.HotKeyIndex, 1); + + if (this.HotKeyIndex < this.Label.Length) + { + this.hotKeyString = this.Label[this.HotKeyIndex].ToString().ToUpper(); + this.HotKeyCharacter = this.hotKeyString[0]; + } + } + else + { + this.Label = label; + } + } + + /// + /// Creates a new instance of the ChoicePromptDetails class + /// based on a ChoiceDescription from the PowerShell layer. + /// + /// + /// A ChoiceDescription on which this instance will be based. + /// + /// A new ChoicePromptDetails instance. + public static ChoiceDetails Create(ChoiceDescription choiceDescription) + { + return new ChoiceDetails( + choiceDescription.Label, + choiceDescription.HelpMessage); + } + + #endregion + + #region Public Methods + + /// + /// Compares an input string to this choice to determine + /// whether the input string is a match. + /// + /// + /// The input string to compare to the choice. + /// + /// True if the input string is a match for the choice. + public bool MatchesInput(string inputString) + { + // Make sure the input string is trimmed of whitespace + inputString = inputString.Trim(); + + // Is it the hotkey? + return + string.Equals(inputString, this.hotKeyString, StringComparison.CurrentCultureIgnoreCase) || + string.Equals(inputString, this.Label, StringComparison.CurrentCultureIgnoreCase); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs new file mode 100644 index 000000000..6f49bab73 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs @@ -0,0 +1,354 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Indicates the style of prompt to be displayed. + /// + public enum PromptStyle + { + /// + /// Indicates that the full prompt should be displayed + /// with all relevant details. + /// + Full, + + /// + /// Indicates that a minimal prompt should be displayed, + /// generally used after the full prompt has already been + /// displayed and the options must be displayed again. + /// + Minimal + } + + /// + /// Provides a base implementation for IPromptHandler classes + /// that present the user a set of options from which a selection + /// should be made. + /// + public abstract class ChoicePromptHandler : PromptHandler + { + #region Private Fields + + private CancellationTokenSource promptCancellationTokenSource = + new CancellationTokenSource(); + private TaskCompletionSource> cancelTask = + new TaskCompletionSource>(); + + #endregion + + /// + /// + /// + /// An ILogger implementation used for writing log messages. + public ChoicePromptHandler(ILogger logger) : base(logger) + { + } + + #region Properties + + /// + /// Returns true if the choice prompt allows multiple selections. + /// + protected bool IsMultiChoice { get; private set; } + + /// + /// Gets the caption (title) string to display with the prompt. + /// + protected string Caption { get; private set; } + + /// + /// Gets the descriptive message to display with the prompt. + /// + protected string Message { get; private set; } + + /// + /// Gets the array of choices from which the user must select. + /// + protected ChoiceDetails[] Choices { get; private set; } + + /// + /// Gets the index of the default choice so that the user + /// interface can make it easy to select this option. + /// + protected int[] DefaultChoices { get; private set; } + + #endregion + + #region Public Methods + + /// + /// Prompts the user to make a choice using the provided details. + /// + /// + /// The caption string which will be displayed to the user. + /// + /// + /// The descriptive message which will be displayed to the user. + /// + /// + /// The list of choices from which the user will select. + /// + /// + /// The default choice to highlight for the user. + /// + /// + /// A CancellationToken that can be used to cancel the prompt. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's choice. + /// + public async Task PromptForChoiceAsync( + string promptCaption, + string promptMessage, + ChoiceDetails[] choices, + int defaultChoice, + CancellationToken cancellationToken) + { + // TODO: Guard against multiple calls + + this.Caption = promptCaption; + this.Message = promptMessage; + this.Choices = choices; + + this.DefaultChoices = + defaultChoice == -1 + ? new int[] { } + : new int[] { defaultChoice }; + + // Cancel the TaskCompletionSource if the caller cancels the task + cancellationToken.Register(this.CancelPrompt, true); + + // Convert the int[] result to int + return await this.WaitForTaskAsync( + this.StartPromptLoopAsync(this.promptCancellationTokenSource.Token) + .ContinueWith( + task => + { + if (task.IsFaulted) + { + throw task.Exception; + } + else if (task.IsCanceled) + { + throw new TaskCanceledException(task); + } + + return this.GetSingleResult(task.Result); + })); + } + + /// + /// Prompts the user to make a choice of one or more options using the + /// provided details. + /// + /// + /// The caption string which will be displayed to the user. + /// + /// + /// The descriptive message which will be displayed to the user. + /// + /// + /// The list of choices from which the user will select. + /// + /// + /// The default choice(s) to highlight for the user. + /// + /// + /// A CancellationToken that can be used to cancel the prompt. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's choices. + /// + public async Task PromptForChoiceAsync( + string promptCaption, + string promptMessage, + ChoiceDetails[] choices, + int[] defaultChoices, + CancellationToken cancellationToken) + { + // TODO: Guard against multiple calls + + this.Caption = promptCaption; + this.Message = promptMessage; + this.Choices = choices; + this.DefaultChoices = defaultChoices; + this.IsMultiChoice = true; + + // Cancel the TaskCompletionSource if the caller cancels the task + cancellationToken.Register(this.CancelPrompt, true); + + return await this.WaitForTaskAsync( + this.StartPromptLoopAsync( + this.promptCancellationTokenSource.Token)); + } + + private async Task WaitForTaskAsync(Task taskToWait) + { + Task finishedTask = + await Task.WhenAny( + this.cancelTask.Task, + taskToWait); + + if (this.cancelTask.Task.IsCanceled) + { + throw new PipelineStoppedException(); + } + + return taskToWait.Result; + } + + private async Task StartPromptLoopAsync( + CancellationToken cancellationToken) + { + int[] choiceIndexes = null; + + // Show the prompt to the user + this.ShowPrompt(PromptStyle.Full); + + while (!cancellationToken.IsCancellationRequested) + { + string responseString = await this.ReadInputStringAsync(cancellationToken); + if (responseString == null) + { + // If the response string is null, the prompt has been cancelled + break; + } + + choiceIndexes = this.HandleResponse(responseString); + + // Return the default choice values if no choices were entered + if (choiceIndexes == null && string.IsNullOrEmpty(responseString)) + { + choiceIndexes = this.DefaultChoices; + } + + // If the user provided no choices, we should prompt again + if (choiceIndexes != null) + { + break; + } + + // The user did not respond with a valid choice, + // show the prompt again to give another chance + this.ShowPrompt(PromptStyle.Minimal); + } + + if (cancellationToken.IsCancellationRequested) + { + // Throw a TaskCanceledException to stop the pipeline + throw new TaskCanceledException(); + } + + return choiceIndexes?.ToArray(); + } + + /// + /// Implements behavior to handle the user's response. + /// + /// The string representing the user's response. + /// + /// True if the prompt is complete, false if the prompt is + /// still waiting for a valid response. + /// + protected virtual int[] HandleResponse(string responseString) + { + List choiceIndexes = new List(); + + // Clean up the response string and split it + var choiceStrings = + responseString.Trim().Split( + new char[] { ',' }, + StringSplitOptions.RemoveEmptyEntries); + + foreach (string choiceString in choiceStrings) + { + for (int i = 0; i < this.Choices.Length; i++) + { + if (this.Choices[i].MatchesInput(choiceString)) + { + choiceIndexes.Add(i); + + // If this is a single-choice prompt, break out after + // the first matched choice + if (!this.IsMultiChoice) + { + break; + } + } + } + } + + if (choiceIndexes.Count == 0) + { + // The user did not respond with a valid choice, + // show the prompt again to give another chance + return null; + } + + return choiceIndexes.ToArray(); + } + + /// + /// Called when the active prompt should be cancelled. + /// + protected override void OnPromptCancelled() + { + // Cancel the prompt task + this.promptCancellationTokenSource.Cancel(); + this.cancelTask.TrySetCanceled(); + } + + #endregion + + #region Abstract Methods + + /// + /// Called when the prompt should be displayed to the user. + /// + /// + /// Indicates the prompt style to use when showing the prompt. + /// + protected abstract void ShowPrompt(PromptStyle promptStyle); + + /// + /// Reads an input string asynchronously from the console. + /// + /// + /// A CancellationToken that can be used to cancel the read. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + protected abstract Task ReadInputStringAsync(CancellationToken cancellationToken); + + #endregion + + #region Private Methods + + private int GetSingleResult(int[] choiceArray) + { + return + choiceArray != null + ? choiceArray.DefaultIfEmpty(-1).First() + : -1; + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs new file mode 100644 index 000000000..80ae62b5f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs @@ -0,0 +1,138 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Contains the details of an colleciton input field shown + /// from an InputPromptHandler. This class is meant to be + /// serializable to the user's UI. + /// + public class CollectionFieldDetails : FieldDetails + { + #region Private Fields + + private bool isArray; + private bool isEntryComplete; + private string fieldName; + private int currentCollectionIndex; + private ArrayList collectionItems = new ArrayList(); + + #endregion + + #region Constructors + + /// + /// Creates an instance of the CollectionFieldDetails class. + /// + /// The field's name. + /// The field's label. + /// The field's value type. + /// If true, marks the field as mandatory. + /// The field's default value. + public CollectionFieldDetails( + string name, + string label, + Type fieldType, + bool isMandatory, + object defaultValue) + : base(name, label, fieldType, isMandatory, defaultValue) + { + this.fieldName = name; + + this.FieldType = typeof(object); + + if (fieldType.IsArray) + { + this.isArray = true; + this.FieldType = fieldType.GetElementType(); + } + + this.Name = + string.Format( + "{0}[{1}]", + this.fieldName, + this.currentCollectionIndex); + } + + #endregion + + #region Public Methods + + /// + /// Gets the next field to display if this is a complex + /// field, otherwise returns null. + /// + /// + /// A FieldDetails object if there's another field to + /// display or if this field is complete. + /// + public override FieldDetails GetNextField() + { + if (!this.isEntryComplete) + { + // Get the next collection field + this.currentCollectionIndex++; + this.Name = + string.Format( + "{0}[{1}]", + this.fieldName, + this.currentCollectionIndex); + + return this; + } + else + { + return null; + } + } + + /// + /// Sets the field's value. + /// + /// The field's value. + /// + /// True if a value has been supplied by the user, false if the user supplied no value. + /// + public override void SetValue(object fieldValue, bool hasValue) + { + if (hasValue) + { + // Add the item to the collection + this.collectionItems.Add(fieldValue); + } + else + { + this.isEntryComplete = true; + } + } + + /// + /// Gets the field's final value after the prompt is + /// complete. + /// + /// The field's final value. + protected override object OnGetValue() + { + object collection = this.collectionItems; + + // Should the result collection be an array? + if (this.isArray) + { + // Convert the ArrayList to an array + collection = + this.collectionItems.ToArray( + this.FieldType); + } + + return collection; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs new file mode 100644 index 000000000..e5ed4472c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs @@ -0,0 +1,133 @@ +// +// 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 Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides a standard implementation of ChoicePromptHandler + /// for use in the interactive console (REPL). + /// + public abstract class ConsoleChoicePromptHandler : ChoicePromptHandler + { + #region Private Fields + + /// + /// The IHostOutput instance to use for this prompt. + /// + protected IHostOutput hostOutput; + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ConsoleChoicePromptHandler class. + /// + /// + /// The IHostOutput implementation to use for writing to the + /// console. + /// + /// An ILogger implementation used for writing log messages. + public ConsoleChoicePromptHandler( + IHostOutput hostOutput, + ILogger logger) + : base(logger) + { + this.hostOutput = hostOutput; + } + + #endregion + + /// + /// Called when the prompt should be displayed to the user. + /// + /// + /// Indicates the prompt style to use when showing the prompt. + /// + protected override void ShowPrompt(PromptStyle promptStyle) + { + if (promptStyle == PromptStyle.Full) + { + if (this.Caption != null) + { + this.hostOutput.WriteOutput(this.Caption); + } + + if (this.Message != null) + { + this.hostOutput.WriteOutput(this.Message); + } + } + + foreach (var choice in this.Choices) + { + string hotKeyString = + choice.HotKeyIndex > -1 ? + choice.Label[choice.HotKeyIndex].ToString().ToUpper() : + string.Empty; + + this.hostOutput.WriteOutput( + string.Format( + "[{0}] {1} ", + hotKeyString, + choice.Label), + false); + } + + this.hostOutput.WriteOutput("[?] Help", false); + + var validDefaultChoices = + this.DefaultChoices.Where( + choice => choice > -1 && choice < this.Choices.Length); + + if (validDefaultChoices.Any()) + { + var choiceString = + string.Join( + ", ", + this.DefaultChoices + .Select(choice => this.Choices[choice].Label)); + + this.hostOutput.WriteOutput( + $" (default is \"{choiceString}\"): ", + false); + } + } + + + /// + /// Implements behavior to handle the user's response. + /// + /// The string representing the user's response. + /// + /// True if the prompt is complete, false if the prompt is + /// still waiting for a valid response. + /// + protected override int[] HandleResponse(string responseString) + { + if (responseString.Trim() == "?") + { + // Print help text + foreach (var choice in this.Choices) + { + this.hostOutput.WriteOutput( + string.Format( + "{0} - {1}", + (choice.HotKeyCharacter.HasValue ? + choice.HotKeyCharacter.Value.ToString() : + choice.Label), + choice.HelpMessage)); + } + + return null; + } + + return base.HandleResponse(responseString); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs new file mode 100644 index 000000000..6e804f86e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs @@ -0,0 +1,102 @@ +// +// 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.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides a standard implementation of InputPromptHandler + /// for use in the interactive console (REPL). + /// + public abstract class ConsoleInputPromptHandler : InputPromptHandler + { + #region Private Fields + + /// + /// The IHostOutput instance to use for this prompt. + /// + protected IHostOutput hostOutput; + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ConsoleInputPromptHandler class. + /// + /// + /// The IHostOutput implementation to use for writing to the + /// console. + /// + /// An ILogger implementation used for writing log messages. + public ConsoleInputPromptHandler( + IHostOutput hostOutput, + ILogger logger) + : base(logger) + { + this.hostOutput = hostOutput; + } + + #endregion + + #region Public Methods + + /// + /// Called when the prompt caption and message should be + /// displayed to the user. + /// + /// The caption string to be displayed. + /// The message string to be displayed. + protected override void ShowPromptMessage(string caption, string message) + { + if (!string.IsNullOrEmpty(caption)) + { + this.hostOutput.WriteOutput(caption, true); + } + + if (!string.IsNullOrEmpty(message)) + { + this.hostOutput.WriteOutput(message, true); + } + } + + /// + /// Called when a prompt should be displayed for a specific + /// input field. + /// + /// The details of the field to be displayed. + protected override void ShowFieldPrompt(FieldDetails fieldDetails) + { + // For a simple prompt there won't be any field name. + // In this case don't write anything + if (!string.IsNullOrEmpty(fieldDetails.Name)) + { + this.hostOutput.WriteOutput( + fieldDetails.Name + ": ", + false); + } + } + + /// + /// Called when an error should be displayed, such as when the + /// user types in a string with an incorrect format for the + /// current field. + /// + /// + /// The Exception containing the error to be displayed. + /// + protected override void ShowErrorMessage(Exception e) + { + this.hostOutput.WriteOutput( + e.Message, + true, + OutputType.Error); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs new file mode 100644 index 000000000..b9312ca7c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs @@ -0,0 +1,196 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides asynchronous implementations of the API's as well as + /// synchronous implementations that work around platform specific issues. + /// + internal static class ConsoleProxy + { + private static IConsoleOperations s_consoleProxy; + + static ConsoleProxy() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + s_consoleProxy = new WindowsConsoleOperations(); + return; + } + + s_consoleProxy = new UnixConsoleOperations(); + } + + /// + /// Obtains the next character or function key pressed by the user asynchronously. + /// Does not block when other console API's are called. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The CancellationToken to observe. + /// + /// An object that describes the constant and Unicode character, if any, + /// that correspond to the pressed console key. The object also + /// describes, in a bitwise combination of values, whether + /// one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously with the console key. + /// + public static ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) => + s_consoleProxy.ReadKey(intercept, cancellationToken); + + /// + /// Obtains the next character or function key pressed by the user asynchronously. + /// Does not block when other console API's are called. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The CancellationToken to observe. + /// + /// A task that will complete with a result of the key pressed by the user. + /// + public static Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) => + s_consoleProxy.ReadKeyAsync(intercept, cancellationToken); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The horizontal position of the console cursor. + public static int GetCursorLeft() => + s_consoleProxy.GetCursorLeft(); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The horizontal position of the console cursor. + public static int GetCursorLeft(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorLeft(cancellationToken); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// + public static Task GetCursorLeftAsync() => + s_consoleProxy.GetCursorLeftAsync(); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// + public static Task GetCursorLeftAsync(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorLeftAsync(cancellationToken); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The vertical position of the console cursor. + public static int GetCursorTop() => + s_consoleProxy.GetCursorTop(); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The vertical position of the console cursor. + public static int GetCursorTop(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorTop(cancellationToken); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// + public static Task GetCursorTopAsync() => + s_consoleProxy.GetCursorTopAsync(); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// + public static Task GetCursorTopAsync(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorTopAsync(cancellationToken); + + /// + /// On Unix platforms this method is sent to PSReadLine as a work around for issues + /// with the System.Console implementation for that platform. Functionally it is the + /// same as System.Console.ReadKey, with the exception that it will not lock the + /// standard input stream. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// true to not display the pressed key; otherwise, false. + /// + /// + /// The that can be used to cancel the request. + /// + /// + /// An object that describes the ConsoleKey constant and Unicode character, if any, + /// that correspond to the pressed console key. The ConsoleKeyInfo object also describes, + /// in a bitwise combination of ConsoleModifiers values, whether one or more Shift, Alt, + /// or Ctrl modifier keys was pressed simultaneously with the console key. + /// + internal static ConsoleKeyInfo UnixReadKey(bool intercept, CancellationToken cancellationToken) + { + try + { + return ((UnixConsoleOperations)s_consoleProxy).ReadKey(intercept, cancellationToken); + } + catch (OperationCanceledException) + { + return default(ConsoleKeyInfo); + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs new file mode 100644 index 000000000..466e10764 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs @@ -0,0 +1,616 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + using System; + using System.Management.Automation; + using System.Management.Automation.Language; + using System.Security; + + internal class ConsoleReadLine + { + #region Private Field + private PowerShellContextService powerShellContext; + + #endregion + + #region Constructors + + public ConsoleReadLine(PowerShellContextService powerShellContext) + { + this.powerShellContext = powerShellContext; + } + + #endregion + + #region Public Methods + + public Task ReadCommandLineAsync(CancellationToken cancellationToken) + { + return this.ReadLineAsync(true, cancellationToken); + } + + public Task ReadSimpleLineAsync(CancellationToken cancellationToken) + { + return this.ReadLineAsync(false, cancellationToken); + } + + public async Task ReadSecureLineAsync(CancellationToken cancellationToken) + { + SecureString secureString = new SecureString(); + + int initialPromptRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + int initialPromptCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); + int previousInputLength = 0; + + Console.TreatControlCAsInput = true; + + try + { + while (!cancellationToken.IsCancellationRequested) + { + ConsoleKeyInfo keyInfo = await ReadKeyAsync(cancellationToken); + + if ((int)keyInfo.Key == 3 || + keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + throw new PipelineStoppedException(); + } + if (keyInfo.Key == ConsoleKey.Enter) + { + // Break to return the completed string + break; + } + if (keyInfo.Key == ConsoleKey.Tab) + { + continue; + } + if (keyInfo.Key == ConsoleKey.Backspace) + { + if (secureString.Length > 0) + { + secureString.RemoveAt(secureString.Length - 1); + } + } + else if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) + { + secureString.AppendChar(keyInfo.KeyChar); + } + + // Re-render the secure string characters + int currentInputLength = secureString.Length; + int consoleWidth = Console.WindowWidth; + + if (currentInputLength > previousInputLength) + { + Console.Write('*'); + } + else if (previousInputLength > 0 && currentInputLength < previousInputLength) + { + int row = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + int col = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); + + // Back up the cursor before clearing the character + col--; + if (col < 0) + { + col = consoleWidth - 1; + row--; + } + + Console.SetCursorPosition(col, row); + Console.Write(' '); + Console.SetCursorPosition(col, row); + } + + previousInputLength = currentInputLength; + } + } + finally + { + Console.TreatControlCAsInput = false; + } + + return secureString; + } + + #endregion + + #region Private Methods + + private static async Task ReadKeyAsync(CancellationToken cancellationToken) + { + return await ConsoleProxy.ReadKeyAsync(intercept: true, cancellationToken); + } + + private async Task ReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await this.powerShellContext.InvokeReadLineAsync(isCommandLine, cancellationToken); + } + + /// + /// Invokes a custom ReadLine method that is similar to but more basic than PSReadLine. + /// This method should be used when PSReadLine is disabled, either by user settings or + /// unsupported PowerShell versions. + /// + /// + /// Indicates whether ReadLine should act like a command line. + /// + /// + /// The cancellation token that will be checked prior to completing the returned task. + /// + /// + /// A task object representing the asynchronus operation. The Result property on + /// the task object returns the user input string. + /// + internal async Task InvokeLegacyReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + string inputBeforeCompletion = null; + string inputAfterCompletion = null; + CommandCompletion currentCompletion = null; + + int historyIndex = -1; + Collection currentHistory = null; + + StringBuilder inputLine = new StringBuilder(); + + int initialCursorCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); + int initialCursorRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + + int initialWindowLeft = Console.WindowLeft; + int initialWindowTop = Console.WindowTop; + + int currentCursorIndex = 0; + + Console.TreatControlCAsInput = true; + + try + { + while (!cancellationToken.IsCancellationRequested) + { + ConsoleKeyInfo keyInfo = await ReadKeyAsync(cancellationToken); + + // Do final position calculation after the key has been pressed + // because the window could have been resized before then + int promptStartCol = initialCursorCol; + int promptStartRow = initialCursorRow; + int consoleWidth = Console.WindowWidth; + + if ((int)keyInfo.Key == 3 || + keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + throw new PipelineStoppedException(); + } + else if (keyInfo.Key == ConsoleKey.Tab && isCommandLine) + { + if (currentCompletion == null) + { + inputBeforeCompletion = inputLine.ToString(); + inputAfterCompletion = null; + + // TODO: This logic should be moved to AstOperations or similar! + + if (this.powerShellContext.IsDebuggerStopped) + { + PSCommand command = new PSCommand(); + command.AddCommand("TabExpansion2"); + command.AddParameter("InputScript", inputBeforeCompletion); + command.AddParameter("CursorColumn", currentCursorIndex); + command.AddParameter("Options", null); + + var results = + await this.powerShellContext.ExecuteCommandAsync(command, false, false); + + currentCompletion = results.FirstOrDefault(); + } + else + { + using (RunspaceHandle runspaceHandle = await this.powerShellContext.GetRunspaceHandleAsync()) + using (PowerShell powerShell = PowerShell.Create()) + { + powerShell.Runspace = runspaceHandle.Runspace; + currentCompletion = + CommandCompletion.CompleteInput( + inputBeforeCompletion, + currentCursorIndex, + null, + powerShell); + + if (currentCompletion.CompletionMatches.Count > 0) + { + int replacementEndIndex = + currentCompletion.ReplacementIndex + + currentCompletion.ReplacementLength; + + inputAfterCompletion = + inputLine.ToString( + replacementEndIndex, + inputLine.Length - replacementEndIndex); + } + else + { + currentCompletion = null; + } + } + } + } + + CompletionResult completion = + currentCompletion?.GetNextResult( + !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift)); + + if (completion != null) + { + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + $"{completion.CompletionText}{inputAfterCompletion}", + currentCursorIndex, + insertIndex: currentCompletion.ReplacementIndex, + replaceLength: inputLine.Length - currentCompletion.ReplacementIndex, + finalCursorIndex: currentCompletion.ReplacementIndex + completion.CompletionText.Length); + } + } + else if (keyInfo.Key == ConsoleKey.LeftArrow) + { + currentCompletion = null; + + if (currentCursorIndex > 0) + { + currentCursorIndex = + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + currentCursorIndex - 1); + } + } + else if (keyInfo.Key == ConsoleKey.Home) + { + currentCompletion = null; + + currentCursorIndex = + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + 0); + } + else if (keyInfo.Key == ConsoleKey.RightArrow) + { + currentCompletion = null; + + if (currentCursorIndex < inputLine.Length) + { + currentCursorIndex = + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + currentCursorIndex + 1); + } + } + else if (keyInfo.Key == ConsoleKey.End) + { + currentCompletion = null; + + currentCursorIndex = + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + inputLine.Length); + } + else if (keyInfo.Key == ConsoleKey.UpArrow && isCommandLine) + { + currentCompletion = null; + + // TODO: Ctrl+Up should allow navigation in multi-line input + + if (currentHistory == null) + { + historyIndex = -1; + + PSCommand command = new PSCommand(); + command.AddCommand("Get-History"); + + currentHistory = + await this.powerShellContext.ExecuteCommandAsync( + command, + false, + false) as Collection; + + if (currentHistory != null) + { + historyIndex = currentHistory.Count; + } + } + + if (currentHistory != null && currentHistory.Count > 0 && historyIndex > 0) + { + historyIndex--; + + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + (string)currentHistory[historyIndex].Properties["CommandLine"].Value, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + } + else if (keyInfo.Key == ConsoleKey.DownArrow && isCommandLine) + { + currentCompletion = null; + + // The down arrow shouldn't cause history to be loaded, + // it's only for navigating an active history array + + if (historyIndex > -1 && historyIndex < currentHistory.Count && + currentHistory != null && currentHistory.Count > 0) + { + historyIndex++; + + if (historyIndex < currentHistory.Count) + { + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + (string)currentHistory[historyIndex].Properties["CommandLine"].Value, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + else if (historyIndex == currentHistory.Count) + { + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + } + } + else if (keyInfo.Key == ConsoleKey.Escape) + { + currentCompletion = null; + historyIndex = currentHistory != null ? currentHistory.Count : -1; + + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + else if (keyInfo.Key == ConsoleKey.Backspace) + { + currentCompletion = null; + + if (currentCursorIndex > 0) + { + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: currentCursorIndex - 1, + replaceLength: 1, + finalCursorIndex: currentCursorIndex - 1); + } + } + else if (keyInfo.Key == ConsoleKey.Delete) + { + currentCompletion = null; + + if (currentCursorIndex < inputLine.Length) + { + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + replaceLength: 1, + finalCursorIndex: currentCursorIndex); + } + } + else if (keyInfo.Key == ConsoleKey.Enter) + { + string completedInput = inputLine.ToString(); + currentCompletion = null; + currentHistory = null; + + //if ((keyInfo.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift) + //{ + // // TODO: Start a new line! + // continue; + //} + + Parser.ParseInput( + completedInput, + out Token[] tokens, + out ParseError[] parseErrors); + + //if (parseErrors.Any(e => e.IncompleteInput)) + //{ + // // TODO: Start a new line! + // continue; + //} + + return completedInput; + } + else if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) + { + // Normal character input + currentCompletion = null; + + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + keyInfo.KeyChar.ToString(), + currentCursorIndex, + finalCursorIndex: currentCursorIndex + 1); + } + } + } + finally + { + Console.TreatControlCAsInput = false; + } + + return null; + } + + private int CalculateIndexFromCursor( + int promptStartCol, + int promptStartRow, + int consoleWidth) + { + return + ((ConsoleProxy.GetCursorTop() - promptStartRow) * consoleWidth) + + ConsoleProxy.GetCursorLeft() - promptStartCol; + } + + private void CalculateCursorFromIndex( + int promptStartCol, + int promptStartRow, + int consoleWidth, + int inputIndex, + out int cursorCol, + out int cursorRow) + { + cursorCol = promptStartCol + inputIndex; + cursorRow = promptStartRow + cursorCol / consoleWidth; + cursorCol = cursorCol % consoleWidth; + } + + private int InsertInput( + StringBuilder inputLine, + int promptStartCol, + int promptStartRow, + string insertedInput, + int cursorIndex, + int insertIndex = -1, + int replaceLength = 0, + int finalCursorIndex = -1) + { + int consoleWidth = Console.WindowWidth; + int previousInputLength = inputLine.Length; + + if (insertIndex == -1) + { + insertIndex = cursorIndex; + } + + // Move the cursor to the new insertion point + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + insertIndex); + + // Edit the input string based on the insertion + if (insertIndex < inputLine.Length) + { + if (replaceLength > 0) + { + inputLine.Remove(insertIndex, replaceLength); + } + + inputLine.Insert(insertIndex, insertedInput); + } + else + { + inputLine.Append(insertedInput); + } + + // Re-render affected section + Console.Write( + inputLine.ToString( + insertIndex, + inputLine.Length - insertIndex)); + + if (inputLine.Length < previousInputLength) + { + Console.Write( + new string( + ' ', + previousInputLength - inputLine.Length)); + } + + // Automatically set the final cursor position to the end + // of the new input string. This is needed if the previous + // input string is longer than the new one and needed to have + // its old contents overwritten. This will position the cursor + // back at the end of the new text + if (finalCursorIndex == -1 && inputLine.Length < previousInputLength) + { + finalCursorIndex = inputLine.Length; + } + + if (finalCursorIndex > -1) + { + // Move the cursor to the final position + return + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + finalCursorIndex); + } + else + { + return inputLine.Length; + } + } + + private int MoveCursorToIndex( + int promptStartCol, + int promptStartRow, + int consoleWidth, + int newCursorIndex) + { + this.CalculateCursorFromIndex( + promptStartCol, + promptStartRow, + consoleWidth, + newCursorIndex, + out int newCursorCol, + out int newCursorRow); + + Console.SetCursorPosition(newCursorCol, newCursorRow); + + return newCursorIndex; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs new file mode 100644 index 000000000..4b4452f2b --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs @@ -0,0 +1,122 @@ +// +// 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.Management.Automation; +using System.Security; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Contains the details of a PSCredential field shown + /// from an InputPromptHandler. This class is meant to + /// be serializable to the user's UI. + /// + public class CredentialFieldDetails : FieldDetails + { + private string userName; + private SecureString password; + + /// + /// Creates an instance of the CredentialFieldDetails class. + /// + /// The field's name. + /// The field's label. + /// The initial value of the userName field. + public CredentialFieldDetails( + string name, + string label, + string userName) + : this(name, label, typeof(PSCredential), true, null) + { + if (!string.IsNullOrEmpty(userName)) + { + // Call GetNextField to prepare the password field + this.userName = userName; + this.GetNextField(); + } + } + + /// + /// Creates an instance of the CredentialFieldDetails class. + /// + /// The field's name. + /// The field's label. + /// The field's value type. + /// If true, marks the field as mandatory. + /// The field's default value. + public CredentialFieldDetails( + string name, + string label, + Type fieldType, + bool isMandatory, + object defaultValue) + : base(name, label, fieldType, isMandatory, defaultValue) + { + this.Name = "User"; + this.FieldType = typeof(string); + } + + #region Public Methods + + /// + /// Gets the next field to display if this is a complex + /// field, otherwise returns null. + /// + /// + /// A FieldDetails object if there's another field to + /// display or if this field is complete. + /// + public override FieldDetails GetNextField() + { + if (this.password != null) + { + // No more fields to display + return null; + } + else if (this.userName != null) + { + this.Name = $"Password for user {this.userName}"; + this.FieldType = typeof(SecureString); + } + + return this; + } + + /// + /// Sets the field's value. + /// + /// The field's value. + /// + /// True if a value has been supplied by the user, false if the user supplied no value. + /// + public override void SetValue(object fieldValue, bool hasValue) + { + if (hasValue) + { + if (this.userName == null) + { + this.userName = (string)fieldValue; + } + else + { + this.password = (SecureString)fieldValue; + } + } + } + + /// + /// Gets the field's final value after the prompt is + /// complete. + /// + /// The field's final value. + protected override object OnGetValue() + { + return new PSCredential(this.userName, this.password); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs new file mode 100644 index 000000000..9fc80252e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs @@ -0,0 +1,234 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Collections; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Reflection; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Contains the details of an input field shown from an + /// InputPromptHandler. This class is meant to be + /// serializable to the user's UI. + /// + public class FieldDetails + { + #region Private Fields + + private object fieldValue; + + #endregion + + #region Properties + + /// + /// Gets or sets the name of the field. + /// + public string Name { get; set; } + + /// + /// Gets or sets the original name of the field before it was manipulated. + /// + public string OriginalName { get; set; } + + /// + /// Gets or sets the descriptive label for the field. + /// + public string Label { get; set; } + + /// + /// Gets or sets the field's value type. + /// + public Type FieldType { get; set; } + + /// + /// Gets or sets the field's help message. + /// + public string HelpMessage { get; set; } + + /// + /// Gets or sets a boolean that is true if the user + /// must enter a value for the field. + /// + public bool IsMandatory { get; set; } + + /// + /// Gets or sets the default value for the field. + /// + public object DefaultValue { get; set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the FieldDetails class. + /// + /// The field's name. + /// The field's label. + /// The field's value type. + /// If true, marks the field as mandatory. + /// The field's default value. + public FieldDetails( + string name, + string label, + Type fieldType, + bool isMandatory, + object defaultValue) + { + this.OriginalName = name; + this.Name = name; + this.Label = label; + this.FieldType = fieldType; + this.IsMandatory = isMandatory; + this.DefaultValue = defaultValue; + + if (fieldType.GetTypeInfo().IsGenericType) + { + throw new PSArgumentException( + "Generic types are not supported for input fields at this time."); + } + } + + #endregion + + #region Public Methods + + /// + /// Sets the field's value. + /// + /// The field's value. + /// + /// True if a value has been supplied by the user, false if the user supplied no value. + /// + public virtual void SetValue(object fieldValue, bool hasValue) + { + if (hasValue) + { + this.fieldValue = fieldValue; + } + } + + /// + /// Gets the field's final value after the prompt is + /// complete. + /// + /// The field's final value. + public object GetValue(ILogger logger) + { + object fieldValue = this.OnGetValue(); + + if (fieldValue == null) + { + if (!this.IsMandatory) + { + fieldValue = this.DefaultValue; + } + else + { + // This "shoudln't" happen, so log in case it does + logger.LogError( + $"Cannot retrieve value for field {this.Label}"); + } + } + + return fieldValue; + } + + /// + /// Gets the field's final value after the prompt is + /// complete. + /// + /// The field's final value. + protected virtual object OnGetValue() + { + return this.fieldValue; + } + + /// + /// Gets the next field if this field can accept multiple + /// values, like a collection or an object with multiple + /// properties. + /// + /// + /// A new FieldDetails instance if there is a next field + /// or null otherwise. + /// + public virtual FieldDetails GetNextField() + { + return null; + } + + #endregion + + #region Internal Methods + + internal static FieldDetails Create( + FieldDescription fieldDescription, + ILogger logger) + { + Type fieldType = + GetFieldTypeFromTypeName( + fieldDescription.ParameterAssemblyFullName, + logger); + + if (typeof(IList).GetTypeInfo().IsAssignableFrom(fieldType.GetTypeInfo())) + { + return new CollectionFieldDetails( + fieldDescription.Name, + fieldDescription.Label, + fieldType, + fieldDescription.IsMandatory, + fieldDescription.DefaultValue); + } + else if (typeof(PSCredential) == fieldType) + { + return new CredentialFieldDetails( + fieldDescription.Name, + fieldDescription.Label, + fieldType, + fieldDescription.IsMandatory, + fieldDescription.DefaultValue); + } + else + { + return new FieldDetails( + fieldDescription.Name, + fieldDescription.Label, + fieldType, + fieldDescription.IsMandatory, + fieldDescription.DefaultValue); + } + } + + private static Type GetFieldTypeFromTypeName( + string assemblyFullName, + ILogger logger) + { + Type fieldType = typeof(string); + + if (!string.IsNullOrEmpty(assemblyFullName)) + { + if (!LanguagePrimitives.TryConvertTo(assemblyFullName, out fieldType)) + { + logger.LogWarning( + string.Format( + "Could not resolve type of field: {0}", + assemblyFullName)); + } + } + + return fieldType; + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs new file mode 100644 index 000000000..b3fb58561 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs @@ -0,0 +1,140 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides platform specific console utilities. + /// + public interface IConsoleOperations + { + /// + /// Obtains the next character or function key pressed by the user asynchronously. + /// Does not block when other console API's are called. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The CancellationToken to observe. + /// + /// An object that describes the constant and Unicode character, if any, + /// that correspond to the pressed console key. The object also + /// describes, in a bitwise combination of values, whether + /// one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously with the console key. + /// + ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken); + + /// + /// Obtains the next character or function key pressed by the user asynchronously. + /// Does not block when other console API's are called. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The CancellationToken to observe. + /// + /// A task that will complete with a result of the key pressed by the user. + /// + Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The horizontal position of the console cursor. + int GetCursorLeft(); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The horizontal position of the console cursor. + int GetCursorLeft(CancellationToken cancellationToken); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// + Task GetCursorLeftAsync(); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// + Task GetCursorLeftAsync(CancellationToken cancellationToken); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The vertical position of the console cursor. + int GetCursorTop(); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The vertical position of the console cursor. + int GetCursorTop(CancellationToken cancellationToken); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// + Task GetCursorTopAsync(); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// + Task GetCursorTopAsync(CancellationToken cancellationToken); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs new file mode 100644 index 000000000..2b5c9bd23 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs @@ -0,0 +1,331 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Management.Automation; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides a base implementation for IPromptHandler classes + /// that present the user a set of fields for which values + /// should be entered. + /// + public abstract class InputPromptHandler : PromptHandler + { + #region Private Fields + + private int currentFieldIndex = -1; + private FieldDetails currentField; + private CancellationTokenSource promptCancellationTokenSource = + new CancellationTokenSource(); + private TaskCompletionSource> cancelTask = + new TaskCompletionSource>(); + + #endregion + + /// + /// + /// + /// An ILogger implementation used for writing log messages. + public InputPromptHandler(ILogger logger) : base(logger) + { + } + + #region Properties + + /// + /// Gets the array of fields for which the user must enter values. + /// + protected FieldDetails[] Fields { get; private set; } + + #endregion + + #region Public Methods + + /// + /// Prompts the user for a line of input without writing any message or caption. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + public Task PromptForInputAsync( + CancellationToken cancellationToken) + { + Task> innerTask = + this.PromptForInputAsync( + null, + null, + new FieldDetails[] { new FieldDetails("", "", typeof(string), false, "") }, + cancellationToken); + + return + innerTask.ContinueWith( + task => + { + if (task.IsFaulted) + { + throw task.Exception; + } + else if (task.IsCanceled) + { + throw new TaskCanceledException(task); + } + + // Return the value of the sole field + return (string)task.Result[""]; + }); + } + + /// + /// Prompts the user for a line (or lines) of input. + /// + /// + /// A title shown before the series of input fields. + /// + /// + /// A descritpive message shown before the series of input fields. + /// + /// + /// An array of FieldDetails items to be displayed which prompt the + /// user for input of a specific type. + /// + /// + /// A CancellationToken that can be used to cancel the prompt. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + public async Task> PromptForInputAsync( + string promptCaption, + string promptMessage, + FieldDetails[] fields, + CancellationToken cancellationToken) + { + // Cancel the prompt if the caller cancels the task + cancellationToken.Register(this.CancelPrompt, true); + + this.Fields = fields; + + this.ShowPromptMessage(promptCaption, promptMessage); + + Task> promptTask = + this.StartPromptLoopAsync(this.promptCancellationTokenSource.Token); + + Task finishedTask = + await Task.WhenAny( + cancelTask.Task, + promptTask); + + if (this.cancelTask.Task.IsCanceled) + { + throw new PipelineStoppedException(); + } + + return promptTask.Result; + } + + /// + /// Prompts the user for a SecureString without writing any message or caption. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + public Task PromptForSecureInputAsync( + CancellationToken cancellationToken) + { + Task> innerTask = + this.PromptForInputAsync( + null, + null, + new FieldDetails[] { new FieldDetails("", "", typeof(SecureString), false, "") }, + cancellationToken); + + return + innerTask.ContinueWith( + task => + { + if (task.IsFaulted) + { + throw task.Exception; + } + else if (task.IsCanceled) + { + throw new TaskCanceledException(task); + } + + // Return the value of the sole field + return (SecureString)task.Result?[""]; + }); + } + + /// + /// Called when the active prompt should be cancelled. + /// + protected override void OnPromptCancelled() + { + // Cancel the prompt task + this.promptCancellationTokenSource.Cancel(); + this.cancelTask.TrySetCanceled(); + } + + #endregion + + #region Abstract Methods + + /// + /// Called when the prompt caption and message should be + /// displayed to the user. + /// + /// The caption string to be displayed. + /// The message string to be displayed. + protected abstract void ShowPromptMessage(string caption, string message); + + /// + /// Called when a prompt should be displayed for a specific + /// input field. + /// + /// The details of the field to be displayed. + protected abstract void ShowFieldPrompt(FieldDetails fieldDetails); + + /// + /// Reads an input string asynchronously from the console. + /// + /// + /// A CancellationToken that can be used to cancel the read. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + protected abstract Task ReadInputStringAsync(CancellationToken cancellationToken); + + /// + /// Reads a SecureString asynchronously from the console. + /// + /// + /// A CancellationToken that can be used to cancel the read. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + protected abstract Task ReadSecureStringAsync(CancellationToken cancellationToken); + + /// + /// Called when an error should be displayed, such as when the + /// user types in a string with an incorrect format for the + /// current field. + /// + /// + /// The Exception containing the error to be displayed. + /// + protected abstract void ShowErrorMessage(Exception e); + + #endregion + + #region Private Methods + + private async Task> StartPromptLoopAsync( + CancellationToken cancellationToken) + { + this.GetNextField(); + + // Loop until there are no more prompts to process + while (this.currentField != null && !cancellationToken.IsCancellationRequested) + { + // Show current prompt + this.ShowFieldPrompt(this.currentField); + + bool enteredValue = false; + object responseValue = null; + string responseString = null; + + // Read input depending on field type + if (this.currentField.FieldType == typeof(SecureString)) + { + SecureString secureString = await this.ReadSecureStringAsync(cancellationToken); + responseValue = secureString; + enteredValue = secureString != null; + } + else + { + responseString = await this.ReadInputStringAsync(cancellationToken); + responseValue = responseString; + enteredValue = responseString != null && responseString.Length > 0; + + try + { + responseValue = + LanguagePrimitives.ConvertTo( + responseString, + this.currentField.FieldType, + CultureInfo.CurrentCulture); + } + catch (PSInvalidCastException e) + { + this.ShowErrorMessage(e.InnerException ?? e); + continue; + } + } + + // Set the field's value and get the next field + this.currentField.SetValue(responseValue, enteredValue); + this.GetNextField(); + } + + if (cancellationToken.IsCancellationRequested) + { + // Throw a TaskCanceledException to stop the pipeline + throw new TaskCanceledException(); + } + + // Return the field values + return this.GetFieldValues(); + } + + private FieldDetails GetNextField() + { + FieldDetails nextField = this.currentField?.GetNextField(); + + if (nextField == null) + { + this.currentFieldIndex++; + + // Have we shown all the prompts already? + if (this.currentFieldIndex < this.Fields.Length) + { + nextField = this.Fields[this.currentFieldIndex]; + } + } + + this.currentField = nextField; + return nextField; + } + + private Dictionary GetFieldValues() + { + Dictionary fieldValues = new Dictionary(); + + foreach (FieldDetails field in this.Fields) + { + fieldValues.Add(field.OriginalName, field.GetValue(this.Logger)); + } + + return fieldValues; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs new file mode 100644 index 000000000..0de8a91b5 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs @@ -0,0 +1,55 @@ +// +// 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.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Defines an abstract base class for prompt handler implementations. + /// + public abstract class PromptHandler + { + /// + /// Gets the ILogger implementation used for this instance. + /// + protected ILogger Logger { get; private set; } + + /// + /// + /// + /// An ILogger implementation used for writing log messages. + public PromptHandler(ILogger logger) + { + this.Logger = logger; + } + + /// + /// Called when the active prompt should be cancelled. + /// + public void CancelPrompt() + { + // Allow the implementation to clean itself up + this.OnPromptCancelled(); + this.PromptCancelled?.Invoke(this, new EventArgs()); + } + + /// + /// An event that gets raised if the prompt is cancelled, either + /// by the user or due to a timeout. + /// + public event EventHandler PromptCancelled; + + /// + /// Implementation classes may override this method to perform + /// cleanup when the CancelPrompt method gets called. + /// + protected virtual void OnPromptCancelled() + { + } + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs new file mode 100644 index 000000000..174c15a11 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +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 ReadInputStringAsync(CancellationToken cancellationToken) + { + string inputString = await this.consoleReadLine.ReadSimpleLineAsync(cancellationToken); + this.hostOutput.WriteOutput(string.Empty); + + return inputString; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs new file mode 100644 index 000000000..4d95bc92c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +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 ReadInputStringAsync(CancellationToken cancellationToken) + { + string inputString = await this.consoleReadLine.ReadSimpleLineAsync(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 ReadSecureStringAsync(CancellationToken cancellationToken) + { + SecureString secureString = await this.consoleReadLine.ReadSecureLineAsync(cancellationToken); + this.hostOutput.WriteOutput(string.Empty); + + return secureString; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs new file mode 100644 index 000000000..e51547a10 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs @@ -0,0 +1,298 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; +using UnixConsoleEcho; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + internal class UnixConsoleOperations : IConsoleOperations + { + private const int LongWaitForKeySleepTime = 300; + + private const int ShortWaitForKeyTimeout = 5000; + + private const int ShortWaitForKeySpinUntilSleepTime = 30; + + private static readonly ManualResetEventSlim s_waitHandle = new ManualResetEventSlim(); + + private static readonly SemaphoreSlim s_readKeyHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + private static readonly SemaphoreSlim s_stdInHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + private Func WaitForKeyAvailable; + + private Func> WaitForKeyAvailableAsync; + + internal UnixConsoleOperations() + { + // Switch between long and short wait periods depending on if the + // user has recently (last 5 seconds) pressed a key to avoid preventing + // the CPU from entering low power mode. + WaitForKeyAvailable = LongWaitForKey; + WaitForKeyAvailableAsync = LongWaitForKeyAsync; + } + + public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) + { + s_readKeyHandle.Wait(cancellationToken); + + // On Unix platforms System.Console.ReadKey has an internal lock on stdin. Because + // of this, if a ReadKey call is pending in one thread and in another thread + // Console.CursorLeft is called, both threads block until a key is pressed. + + // To work around this we wait for a key to be pressed before actually calling Console.ReadKey. + // However, any pressed keys during this time will be echoed to the console. To get around + // this we use the UnixConsoleEcho package to disable echo prior to waiting. + if (VersionUtils.IsPS6) + { + InputEcho.Disable(); + } + + try + { + // The WaitForKeyAvailable delegate switches between a long delay between waits and + // a short timeout depending on how recently a key has been pressed. This allows us + // to let the CPU enter low power mode without compromising responsiveness. + while (!WaitForKeyAvailable(cancellationToken)); + } + finally + { + if (VersionUtils.IsPS6) + { + InputEcho.Disable(); + } + s_readKeyHandle.Release(); + } + + // A key has been pressed, so aquire a lock on our internal stdin handle. This is done + // so any of our calls to cursor position API's do not release ReadKey. + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.ReadKey(intercept); + } + finally + { + s_stdInHandle.Release(); + } + } + + public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) + { + await s_readKeyHandle.WaitAsync(cancellationToken); + + // I tried to replace this library with a call to `stty -echo`, but unfortunately + // the library also sets up allowing backspace to trigger `Console.KeyAvailable`. + if (VersionUtils.IsPS6) + { + InputEcho.Disable(); + } + + try + { + while (!await WaitForKeyAvailableAsync(cancellationToken)); + } + finally + { + if (VersionUtils.IsPS6) + { + InputEcho.Enable(); + } + s_readKeyHandle.Release(); + } + + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.ReadKey(intercept); + } + finally + { + s_stdInHandle.Release(); + } + } + + public int GetCursorLeft() + { + return GetCursorLeft(CancellationToken.None); + } + + public int GetCursorLeft(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.CursorLeft; + } + finally + { + s_stdInHandle.Release(); + } + } + + public async Task GetCursorLeftAsync() + { + return await GetCursorLeftAsync(CancellationToken.None); + } + + public async Task GetCursorLeftAsync(CancellationToken cancellationToken) + { + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.CursorLeft; + } + finally + { + s_stdInHandle.Release(); + } + } + + public int GetCursorTop() + { + return GetCursorTop(CancellationToken.None); + } + + public int GetCursorTop(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.CursorTop; + } + finally + { + s_stdInHandle.Release(); + } + } + + public async Task GetCursorTopAsync() + { + return await GetCursorTopAsync(CancellationToken.None); + } + + public async Task GetCursorTopAsync(CancellationToken cancellationToken) + { + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.CursorTop; + } + finally + { + s_stdInHandle.Release(); + } + } + + private bool LongWaitForKey(CancellationToken cancellationToken) + { + // Wait for a key to be buffered (in other words, wait for Console.KeyAvailable to become + // true) with a long delay between checks. + while (!IsKeyAvailable(cancellationToken)) + { + s_waitHandle.Wait(LongWaitForKeySleepTime, cancellationToken); + } + + // As soon as a key is buffered, return true and switch the wait logic to be more + // responsive, but also more expensive. + WaitForKeyAvailable = ShortWaitForKey; + return true; + } + + private async Task LongWaitForKeyAsync(CancellationToken cancellationToken) + { + while (!await IsKeyAvailableAsync(cancellationToken)) + { + await Task.Delay(LongWaitForKeySleepTime, cancellationToken); + } + + WaitForKeyAvailableAsync = ShortWaitForKeyAsync; + return true; + } + + private bool ShortWaitForKey(CancellationToken cancellationToken) + { + // Check frequently for a new key to be buffered. + if (SpinUntilKeyAvailable(ShortWaitForKeyTimeout, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + return true; + } + + // If the user has not pressed a key before the end of the SpinUntil timeout then + // the user is idle and we can switch back to long delays between KeyAvailable checks. + cancellationToken.ThrowIfCancellationRequested(); + WaitForKeyAvailable = LongWaitForKey; + return false; + } + + private async Task ShortWaitForKeyAsync(CancellationToken cancellationToken) + { + if (await SpinUntilKeyAvailableAsync(ShortWaitForKeyTimeout, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + return true; + } + + cancellationToken.ThrowIfCancellationRequested(); + WaitForKeyAvailableAsync = LongWaitForKeyAsync; + return false; + } + + private bool SpinUntilKeyAvailable(int millisecondsTimeout, CancellationToken cancellationToken) + { + return SpinWait.SpinUntil( + () => + { + s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); + return IsKeyAvailable(cancellationToken); + }, + millisecondsTimeout); + } + + private async Task SpinUntilKeyAvailableAsync(int millisecondsTimeout, CancellationToken cancellationToken) + { + return await Task.Factory.StartNew( + () => SpinWait.SpinUntil( + () => + { + // The wait handle is never set, it's just used to enable cancelling the wait. + s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); + return IsKeyAvailable(cancellationToken); + }, + millisecondsTimeout)); + } + + private bool IsKeyAvailable(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.KeyAvailable; + } + finally + { + s_stdInHandle.Release(); + } + } + + private async Task IsKeyAvailableAsync(CancellationToken cancellationToken) + { + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.KeyAvailable; + } + finally + { + s_stdInHandle.Release(); + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs new file mode 100644 index 000000000..493e66930 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + internal class WindowsConsoleOperations : IConsoleOperations + { + private ConsoleKeyInfo? _bufferedKey; + + private SemaphoreSlim _readKeyHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + public int GetCursorLeft() => System.Console.CursorLeft; + + public int GetCursorLeft(CancellationToken cancellationToken) => System.Console.CursorLeft; + + public Task GetCursorLeftAsync() => Task.FromResult(System.Console.CursorLeft); + + public Task GetCursorLeftAsync(CancellationToken cancellationToken) => Task.FromResult(System.Console.CursorLeft); + + public int GetCursorTop() => System.Console.CursorTop; + + public int GetCursorTop(CancellationToken cancellationToken) => System.Console.CursorTop; + + public Task GetCursorTopAsync() => Task.FromResult(System.Console.CursorTop); + + public Task GetCursorTopAsync(CancellationToken cancellationToken) => Task.FromResult(System.Console.CursorTop); + + public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) + { + await _readKeyHandle.WaitAsync(cancellationToken); + try + { + return + _bufferedKey.HasValue + ? _bufferedKey.Value + : await Task.Factory.StartNew( + () => (_bufferedKey = System.Console.ReadKey(intercept)).Value); + } + finally + { + _readKeyHandle.Release(); + + // Throw if we're cancelled so the buffered key isn't cleared. + cancellationToken.ThrowIfCancellationRequested(); + _bufferedKey = null; + } + } + + public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) + { + _readKeyHandle.Wait(cancellationToken); + try + { + return + _bufferedKey.HasValue + ? _bufferedKey.Value + : (_bufferedKey = System.Console.ReadKey(intercept)).Value; + } + finally + { + _readKeyHandle.Release(); + + // Throw if we're cancelled so the buffered key isn't cleared. + cancellationToken.ThrowIfCancellationRequested(); + _bufferedKey = null; + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs new file mode 100644 index 000000000..8a7b80cef --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides details about a command that has been registered + /// with the editor. + /// + public class EditorCommand + { + #region Properties + + /// + /// Gets the name which uniquely identifies the command. + /// + public string Name { get; private set; } + + /// + /// Gets the display name for the command. + /// + public string DisplayName { get; private set; } + + /// + /// Gets the boolean which determines whether this command's + /// output should be suppressed. + /// + public bool SuppressOutput { get; private set; } + + /// + /// Gets the ScriptBlock which can be used to execute the command. + /// + public ScriptBlock ScriptBlock { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new EditorCommand instance that invokes a cmdlet or + /// function by name. + /// + /// The unique identifier name for the command. + /// The display name for the command. + /// If true, causes output to be suppressed for this command. + /// The name of the cmdlet or function which will be invoked by this command. + public EditorCommand( + string commandName, + string displayName, + bool suppressOutput, + string cmdletName) + : this( + commandName, + displayName, + suppressOutput, + ScriptBlock.Create( + string.Format( + "param($context) {0} $context", + cmdletName))) + { + } + + /// + /// Creates a new EditorCommand instance that invokes a ScriptBlock. + /// + /// The unique identifier name for the command. + /// The display name for the command. + /// If true, causes output to be suppressed for this command. + /// The ScriptBlock which will be invoked by this command. + public EditorCommand( + string commandName, + string displayName, + bool suppressOutput, + ScriptBlock scriptBlock) + { + this.Name = commandName; + this.DisplayName = displayName; + this.SuppressOutput = suppressOutput; + this.ScriptBlock = scriptBlock; + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs new file mode 100644 index 000000000..8020cb844 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs @@ -0,0 +1,33 @@ +using System; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides an attribute that can be used to target PowerShell + /// commands for import as editor commands. + /// + [AttributeUsage(AttributeTargets.Class)] + public class EditorCommandAttribute : Attribute + { + + #region Properties + + /// + /// Gets or sets the name which uniquely identifies the command. + /// + public string Name { get; set; } + + /// + /// Gets or sets the display name for the command. + /// + public string DisplayName { get; set; } + + /// + /// Gets or sets a value indicating whether this command's output + /// should be suppressed. + /// + public bool SuppressOutput { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs new file mode 100644 index 000000000..c669acd19 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs @@ -0,0 +1,117 @@ +// +// 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.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides context for the host editor at the time of creation. + /// + public class EditorContext + { + #region Private Fields + + private IEditorOperations editorOperations; + + #endregion + + #region Properties + + /// + /// Gets the FileContext for the active file. + /// + public FileContext CurrentFile { get; private set; } + + /// + /// Gets the BufferRange representing the current selection in the file. + /// + public BufferRange SelectedRange { get; private set; } + + /// + /// Gets the FilePosition representing the current cursor position. + /// + public FilePosition CursorPosition { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the EditorContext class. + /// + /// An IEditorOperations implementation which performs operations in the editor. + /// The ScriptFile that is in the active editor buffer. + /// The position of the user's cursor in the active editor buffer. + /// The range of the user's selection in the active editor buffer. + /// Determines the language of the file.false If it is not specified, then it defaults to "Unknown" + public EditorContext( + IEditorOperations editorOperations, + ScriptFile currentFile, + BufferPosition cursorPosition, + BufferRange selectedRange, + string language = "Unknown") + { + this.editorOperations = editorOperations; + this.CurrentFile = new FileContext(currentFile, this, editorOperations, language); + this.SelectedRange = selectedRange; + this.CursorPosition = new FilePosition(currentFile, cursorPosition); + } + + #endregion + + #region Public Methods + + /// + /// Sets a selection in the host editor's active buffer. + /// + /// The 1-based starting line of the selection. + /// The 1-based starting column of the selection. + /// The 1-based ending line of the selection. + /// The 1-based ending column of the selection. + public void SetSelection( + int startLine, + int startColumn, + int endLine, + int endColumn) + { + this.SetSelection( + new BufferRange( + startLine, startColumn, + endLine, endColumn)); + } + + /// + /// Sets a selection in the host editor's active buffer. + /// + /// The starting position of the selection. + /// The ending position of the selection. + public void SetSelection( + BufferPosition startPosition, + BufferPosition endPosition) + { + this.SetSelection( + new BufferRange( + startPosition, + endPosition)); + } + + /// + /// Sets a selection in the host editor's active buffer. + /// + /// The range of the selection. + public void SetSelection(BufferRange selectionRange) + { + this.editorOperations + .SetSelectionAsync(selectionRange) + .Wait(); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs new file mode 100644 index 000000000..28a0b8a32 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs @@ -0,0 +1,111 @@ +// +// 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.Components; +using System; +using System.Reflection; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides the entry point of the extensibility API, inserted into + /// the PowerShell session as the "$psEditor" variable. + /// + public class EditorObject + { + #region Private Fields + + private ExtensionService extensionService; + private IEditorOperations editorOperations; + + #endregion + + #region Properties + + /// + /// Gets the version of PowerShell Editor Services. + /// + public Version EditorServicesVersion + { + get { return this.GetType().GetTypeInfo().Assembly.GetName().Version; } + } + + /// + /// Gets the workspace interface for the editor API. + /// + public EditorWorkspace Workspace { get; private set; } + + /// + /// Gets the window interface for the editor API. + /// + public EditorWindow Window { get; private set; } + + /// + /// Gets the component registry for the session. + /// + /// + public IComponentRegistry Components { get; private set; } + + #endregion + + /// + /// Creates a new instance of the EditorObject class. + /// + /// An ExtensionService which handles command registration. + /// An IEditorOperations implementation which handles operations in the host editor. + /// An IComponentRegistry instance which provides components in the session. + public EditorObject( + ExtensionService extensionService, + IEditorOperations editorOperations, + IComponentRegistry componentRegistry) + { + this.extensionService = extensionService; + this.editorOperations = editorOperations; + this.Components = componentRegistry; + + // Create API area objects + this.Workspace = new EditorWorkspace(this.editorOperations); + this.Window = new EditorWindow(this.editorOperations); + } + + /// + /// Registers a new command in the editor. + /// + /// The EditorCommand to be registered. + /// True if the command is newly registered, false if the command already exists. + public bool RegisterCommand(EditorCommand editorCommand) + { + return this.extensionService.RegisterCommand(editorCommand); + } + + /// + /// Unregisters an existing EditorCommand based on its registered name. + /// + /// The name of the command to be unregistered. + public void UnregisterCommand(string commandName) + { + this.extensionService.UnregisterCommand(commandName); + } + + /// + /// Returns all registered EditorCommands. + /// + /// An Array of all registered EditorCommands. + public EditorCommand[] GetCommands() + { + return this.extensionService.GetCommands(); + } + /// + /// Gets the EditorContext which contains the state of the editor + /// at the time this method is invoked. + /// + /// A instance of the EditorContext class. + public EditorContext GetEditorContext() + { + return this.editorOperations.GetEditorContextAsync().Result; + } + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs new file mode 100644 index 000000000..8d241559a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs @@ -0,0 +1,83 @@ +// +// 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.Extensions +{ + /// + /// Provides a PowerShell-facing API which allows scripts to + /// interact with the editor's window. + /// + public class EditorWindow + { + #region Private Fields + + private IEditorOperations editorOperations; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the EditorWindow class. + /// + /// An IEditorOperations implementation which handles operations in the host editor. + internal EditorWindow(IEditorOperations editorOperations) + { + this.editorOperations = editorOperations; + } + + #endregion + + #region Public Methods + + /// + /// Shows an informational message to the user. + /// + /// The message to be shown. + public void ShowInformationMessage(string message) + { + this.editorOperations.ShowInformationMessageAsync(message).Wait(); + } + + /// + /// Shows an error message to the user. + /// + /// The message to be shown. + public void ShowErrorMessage(string message) + { + this.editorOperations.ShowErrorMessageAsync(message).Wait(); + } + + /// + /// Shows a warning message to the user. + /// + /// The message to be shown. + public void ShowWarningMessage(string message) + { + this.editorOperations.ShowWarningMessageAsync(message).Wait(); + } + + /// + /// Sets the status bar message in the editor UI (if applicable). + /// + /// The message to be shown. + public void SetStatusBarMessage(string message) + { + this.editorOperations.SetStatusBarMessageAsync(message, null).Wait(); + } + + /// + /// Sets the status bar message in the editor UI (if applicable). + /// + /// The message to be shown. + /// A timeout in milliseconds for how long the message should remain visible. + public void SetStatusBarMessage(string message, int timeout) + { + this.editorOperations.SetStatusBarMessageAsync(message, timeout).Wait(); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs new file mode 100644 index 000000000..ad679f371 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs @@ -0,0 +1,76 @@ +// +// 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.Extensions +{ + /// + /// Provides a PowerShell-facing API which allows scripts to + /// interact with the editor's workspace. + /// + public class EditorWorkspace + { + #region Private Fields + + private IEditorOperations editorOperations; + + #endregion + + #region Properties + + /// + /// Gets the current workspace path if there is one or null otherwise. + /// + public string Path + { + get { return this.editorOperations.GetWorkspacePath(); } + } + + #endregion + + #region Constructors + + internal EditorWorkspace(IEditorOperations editorOperations) + { + this.editorOperations = editorOperations; + } + + #endregion + + #region Public Methods + + /// + /// Creates a new file in the editor + /// + public void NewFile() + { + this.editorOperations.NewFileAsync().Wait(); + } + + /// + /// Opens a file in the workspace. If the file is already open + /// its buffer will be made active. + /// + /// The path to the file to be opened. + public void OpenFile(string filePath) + { + this.editorOperations.OpenFileAsync(filePath).Wait(); + } + + /// + /// Opens a file in the workspace. If the file is already open + /// its buffer will be made active. + /// You can specify whether the file opens as a preview or as a durable editor. + /// + /// The path to the file to be opened. + /// Determines wether the file is opened as a preview or as a durable editor. + public void OpenFile(string filePath, bool preview) + { + this.editorOperations.OpenFileAsync(filePath, preview).Wait(); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs new file mode 100644 index 000000000..89f0f7411 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs @@ -0,0 +1,217 @@ +// +// 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.Components; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides a high-level service which enables PowerShell scripts + /// and modules to extend the behavior of the host editor. + /// + public class ExtensionService + { + #region Fields + + private Dictionary editorCommands = + new Dictionary(); + + #endregion + + #region Properties + + /// + /// Gets the IEditorOperations implementation used to invoke operations + /// in the host editor. + /// + public IEditorOperations EditorOperations { get; private set; } + + /// + /// Gets the EditorObject which exists in the PowerShell session as the + /// '$psEditor' variable. + /// + public EditorObject EditorObject { get; private set; } + + /// + /// Gets the PowerShellContext in which extension code will be executed. + /// + public PowerShellContextService PowerShellContext { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ExtensionService which uses the provided + /// PowerShellContext for loading and executing extension code. + /// + /// A PowerShellContext used to execute extension code. + public ExtensionService(PowerShellContextService powerShellContext) + { + this.PowerShellContext = powerShellContext; + } + + #endregion + + #region Public Methods + + /// + /// Initializes this ExtensionService using the provided IEditorOperations + /// implementation for future interaction with the host editor. + /// + /// An IEditorOperations implementation. + /// An IComponentRegistry instance which provides components in the session. + /// A Task that can be awaited for completion. + public async Task InitializeAsync( + IEditorOperations editorOperations, + IComponentRegistry componentRegistry) + { + this.EditorObject = + new EditorObject( + this, + editorOperations, + componentRegistry); + + // Register the editor object in the runspace + PSCommand variableCommand = new PSCommand(); + using (RunspaceHandle handle = await this.PowerShellContext.GetRunspaceHandleAsync()) + { + handle.Runspace.SessionStateProxy.PSVariable.Set( + "psEditor", + this.EditorObject); + } + } + + /// + /// Invokes the specified editor command against the provided EditorContext. + /// + /// The unique name of the command to be invoked. + /// The context in which the command is being invoked. + /// A Task that can be awaited for completion. + public async Task InvokeCommandAsync(string commandName, EditorContext editorContext) + { + EditorCommand editorCommand; + + if (this.editorCommands.TryGetValue(commandName, out editorCommand)) + { + PSCommand executeCommand = new PSCommand(); + executeCommand.AddCommand("Invoke-Command"); + executeCommand.AddParameter("ScriptBlock", editorCommand.ScriptBlock); + executeCommand.AddParameter("ArgumentList", new object[] { editorContext }); + + await this.PowerShellContext.ExecuteCommandAsync( + executeCommand, + !editorCommand.SuppressOutput, + true); + } + else + { + throw new KeyNotFoundException( + string.Format( + "Editor command not found: '{0}'", + commandName)); + } + } + + /// + /// Registers a new EditorCommand with the ExtensionService and + /// causes its details to be sent to the host editor. + /// + /// The details about the editor command to be registered. + /// True if the command is newly registered, false if the command already exists. + public bool RegisterCommand(EditorCommand editorCommand) + { + bool commandExists = + this.editorCommands.ContainsKey( + editorCommand.Name); + + // Add or replace the editor command + this.editorCommands[editorCommand.Name] = editorCommand; + + if (!commandExists) + { + this.OnCommandAdded(editorCommand); + } + else + { + this.OnCommandUpdated(editorCommand); + } + + return !commandExists; + } + + /// + /// Unregisters an existing EditorCommand based on its registered name. + /// + /// The name of the command to be unregistered. + public void UnregisterCommand(string commandName) + { + EditorCommand existingCommand = null; + if (this.editorCommands.TryGetValue(commandName, out existingCommand)) + { + this.editorCommands.Remove(commandName); + this.OnCommandRemoved(existingCommand); + } + else + { + throw new KeyNotFoundException( + string.Format( + "Command '{0}' is not registered", + commandName)); + } + } + + /// + /// Returns all registered EditorCommands. + /// + /// An Array of all registered EditorCommands. + public EditorCommand[] GetCommands() + { + EditorCommand[] commands = new EditorCommand[this.editorCommands.Count]; + this.editorCommands.Values.CopyTo(commands,0); + return commands; + } + #endregion + + #region Events + + /// + /// Raised when a new editor command is added. + /// + public event EventHandler CommandAdded; + + private void OnCommandAdded(EditorCommand command) + { + this.CommandAdded?.Invoke(this, command); + } + + /// + /// Raised when an existing editor command is updated. + /// + public event EventHandler CommandUpdated; + + private void OnCommandUpdated(EditorCommand command) + { + this.CommandUpdated?.Invoke(this, command); + } + + /// + /// Raised when an existing editor command is removed. + /// + public event EventHandler CommandRemoved; + + private void OnCommandRemoved(EditorCommand command) + { + this.CommandRemoved?.Invoke(this, command); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs new file mode 100644 index 000000000..5f6d98b7c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs @@ -0,0 +1,280 @@ +// +// 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.IO; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides context for a file that is open in the editor. + /// + public class FileContext + { + #region Private Fields + + internal ScriptFile scriptFile; + private EditorContext editorContext; + private IEditorOperations editorOperations; + + #endregion + + #region Properties + + /// + /// Gets the parsed abstract syntax tree for the file. + /// + public Ast Ast + { + get { return this.scriptFile.ScriptAst; } + } + + /// + /// Gets a BufferRange which represents the entire content + /// range of the file. + /// + public BufferRange FileRange + { + get { return this.scriptFile.FileRange; } + } + + /// + /// Gets the language of the file. + /// + public string Language { get; private set; } + + /// + /// Gets the filesystem path of the file. + /// + public string Path + { + get { return this.scriptFile.FilePath; } + } + + /// + /// Gets the parsed token list for the file. + /// + public Token[] Tokens + { + get { return this.scriptFile.ScriptTokens; } + } + + /// + /// Gets the workspace-relative path of the file. + /// + public string WorkspacePath + { + get + { + return + this.editorOperations.GetWorkspaceRelativePath( + this.scriptFile.FilePath); + } + } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the FileContext class. + /// + /// The ScriptFile to which this file refers. + /// The EditorContext to which this file relates. + /// An IEditorOperations implementation which performs operations in the editor. + /// Determines the language of the file.false If it is not specified, then it defaults to "Unknown" + public FileContext( + ScriptFile scriptFile, + EditorContext editorContext, + IEditorOperations editorOperations, + string language = "Unknown") + { + if (string.IsNullOrWhiteSpace(language)) + { + language = "Unknown"; + } + + this.scriptFile = scriptFile; + this.editorContext = editorContext; + this.editorOperations = editorOperations; + this.Language = language; + } + + #endregion + + #region Text Accessors + + /// + /// Gets the complete file content as a string. + /// + /// A string containing the complete file content. + public string GetText() + { + return this.scriptFile.Contents; + } + + /// + /// Gets the file content in the specified range as a string. + /// + /// The buffer range for which content will be extracted. + /// A string with the specified range of content. + public string GetText(BufferRange bufferRange) + { + return + string.Join( + Environment.NewLine, + this.GetTextLines(bufferRange)); + } + + /// + /// Gets the complete file content as an array of strings. + /// + /// An array of strings, each representing a line in the file. + public string[] GetTextLines() + { + return this.scriptFile.FileLines.ToArray(); + } + + /// + /// Gets the file content in the specified range as an array of strings. + /// + /// The buffer range for which content will be extracted. + /// An array of strings, each representing a line in the file within the specified range. + public string[] GetTextLines(BufferRange bufferRange) + { + return this.scriptFile.GetLinesInRange(bufferRange); + } + + #endregion + + #region Text Manipulation + + /// + /// Inserts a text string at the current cursor position represented by + /// the parent EditorContext's CursorPosition property. + /// + /// The text string to insert. + public void InsertText(string textToInsert) + { + // Is there a selection? + if (this.editorContext.SelectedRange.HasRange) + { + this.InsertText( + textToInsert, + this.editorContext.SelectedRange); + } + else + { + this.InsertText( + textToInsert, + this.editorContext.CursorPosition); + } + } + + /// + /// Inserts a text string at the specified buffer position. + /// + /// The text string to insert. + /// The position at which the text will be inserted. + public void InsertText(string textToInsert, BufferPosition insertPosition) + { + this.InsertText( + textToInsert, + new BufferRange(insertPosition, insertPosition)); + } + + /// + /// Inserts a text string at the specified line and column numbers. + /// + /// The text string to insert. + /// The 1-based line number at which the text will be inserted. + /// The 1-based column number at which the text will be inserted. + public void InsertText(string textToInsert, int insertLine, int insertColumn) + { + this.InsertText( + textToInsert, + new BufferPosition(insertLine, insertColumn)); + } + + /// + /// Inserts a text string to replace the specified range, represented + /// by starting and ending line and column numbers. Can be used to + /// insert, replace, or delete text depending on the specified range + /// and text to insert. + /// + /// The text string to insert. + /// The 1-based starting line number where text will be replaced. + /// The 1-based starting column number where text will be replaced. + /// The 1-based ending line number where text will be replaced. + /// The 1-based ending column number where text will be replaced. + public void InsertText( + string textToInsert, + int startLine, + int startColumn, + int endLine, + int endColumn) + { + this.InsertText( + textToInsert, + new BufferRange( + startLine, + startColumn, + endLine, + endColumn)); + } + + /// + /// Inserts a text string to replace the specified range. Can be + /// used to insert, replace, or delete text depending on the specified + /// range and text to insert. + /// + /// The text string to insert. + /// The buffer range which will be replaced by the string. + public void InsertText(string textToInsert, BufferRange insertRange) + { + this.editorOperations + .InsertTextAsync(this.scriptFile.ClientFilePath, textToInsert, insertRange) + .Wait(); + } + + #endregion + + #region File Manipulation + + /// + /// Saves this file. + /// + public void Save() + { + this.editorOperations.SaveFileAsync(this.scriptFile.FilePath); + } + + /// + /// Save this file under a new path and open a new editor window on that file. + /// + /// + /// the path where the file should be saved, + /// including the file name with extension as the leaf + /// + public void SaveAs(string newFilePath) + { + // Do some validation here so that we can provide a helpful error if the path won't work + string absolutePath = System.IO.Path.IsPathRooted(newFilePath) ? + newFilePath : + System.IO.Path.GetFullPath(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(this.scriptFile.FilePath), newFilePath)); + + if (File.Exists(absolutePath)) + { + throw new IOException(String.Format("The file '{0}' already exists", absolutePath)); + } + + this.editorOperations.SaveFileAsync(this.scriptFile.FilePath, newFilePath); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs new file mode 100644 index 000000000..7ef6bdf0e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs @@ -0,0 +1,128 @@ +// +// 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; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides an interface that must be implemented by an editor + /// host to perform operations invoked by extensions written in + /// PowerShell. + /// + public interface IEditorOperations + { + /// + /// Gets the EditorContext for the editor's current state. + /// + /// A new EditorContext object. + Task GetEditorContextAsync(); + + /// + /// Gets the path to the editor's active workspace. + /// + /// The workspace path or null if there isn't one. + string GetWorkspacePath(); + + /// + /// Resolves the given file path relative to the current workspace path. + /// + /// The file path to be resolved. + /// The resolved file path. + string GetWorkspaceRelativePath(string filePath); + + /// + /// Causes a new untitled file to be created in the editor. + /// + /// A task that can be awaited for completion. + Task NewFileAsync(); + + /// + /// Causes a file to be opened in the editor. If the file is + /// already open, the editor must switch to the file. + /// + /// The path of the file to be opened. + /// A Task that can be tracked for completion. + Task OpenFileAsync(string filePath); + + /// + /// Causes a file to be opened in the editor. If the file is + /// already open, the editor must switch to the file. + /// You can specify whether the file opens as a preview or as a durable editor. + /// + /// The path of the file to be opened. + /// Determines wether the file is opened as a preview or as a durable editor. + /// A Task that can be tracked for completion. + Task OpenFileAsync(string filePath, bool preview); + + /// + /// Causes a file to be closed in the editor. + /// + /// The path of the file to be closed. + /// A Task that can be tracked for completion. + Task CloseFileAsync(string filePath); + + /// + /// Causes a file to be saved in the editor. + /// + /// The path of the file to be saved. + /// A Task that can be tracked for completion. + Task SaveFileAsync(string filePath); + + /// + /// Causes a file to be saved as a new file in a new editor window. + /// + /// the path of the current file being saved + /// the path of the new file where the current window content will be saved + /// + Task SaveFileAsync(string oldFilePath, string newFilePath); + + /// + /// Inserts text into the specified range for the file at the specified path. + /// + /// The path of the file which will have text inserted. + /// The text to insert into the file. + /// The range in the file to be replaced. + /// A Task that can be tracked for completion. + Task InsertTextAsync(string filePath, string insertText, BufferRange insertRange); + + /// + /// Causes the selection to be changed in the editor's active file buffer. + /// + /// The range over which the selection will be made. + /// A Task that can be tracked for completion. + Task SetSelectionAsync(BufferRange selectionRange); + + /// + /// Shows an informational message to the user. + /// + /// The message to be shown. + /// A Task that can be tracked for completion. + Task ShowInformationMessageAsync(string message); + + /// + /// Shows an error message to the user. + /// + /// The message to be shown. + /// A Task that can be tracked for completion. + Task ShowErrorMessageAsync(string message); + + /// + /// Shows a warning message to the user. + /// + /// The message to be shown. + /// A Task that can be tracked for completion. + Task ShowWarningMessageAsync(string message); + + /// + /// Sets the status bar message in the editor UI (if applicable). + /// + /// The message to be shown. + /// If non-null, a timeout in milliseconds for how long the message should remain visible. + /// A Task that can be tracked for completion. + Task SetStatusBarMessageAsync(string message, int? timeout); + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs new file mode 100644 index 000000000..cfe5f93dd --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs @@ -0,0 +1,2506 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Management.Automation.Host; +using System.Management.Automation.Remoting; +using System.Management.Automation.Runspaces; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices +{ + using System.Management.Automation; + using Microsoft.PowerShell.EditorServices.Engine; + + /// + /// Manages the lifetime and usage of a PowerShell session. + /// Handles nested PowerShell prompts and also manages execution of + /// commands whether inside or outside of the debugger. + /// + public class PowerShellContextService : IDisposable, IHostSupportsInteractiveSession + { + private static readonly Action s_runspaceApartmentStateSetter; + + static PowerShellContextService() + { + // PowerShell ApartmentState APIs aren't available in PSStandard, so we need to use reflection + if (!VersionUtils.IsNetCore) + { + MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod(); + Delegate setter = Delegate.CreateDelegate(typeof(Action), firstArgument: null, method: setterInfo); + s_runspaceApartmentStateSetter = (Action)setter; + } + } + + #region Fields + + private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + private bool isPSReadLineEnabled; + private ILogger logger; + private PowerShell powerShell; + private bool ownsInitialRunspace; + private RunspaceDetails initialRunspace; + private SessionDetails mostRecentSessionDetails; + + private ProfilePaths profilePaths; + + private IVersionSpecificOperations versionSpecificOperations; + + private Stack runspaceStack = new Stack(); + + private int isCommandLoopRestarterSet; + + #endregion + + #region Properties + + private IPromptContext PromptContext { get; set; } + + private PromptNest PromptNest { get; set; } + + private InvocationEventQueue InvocationEventQueue { get; set; } + + private EngineIntrinsics EngineIntrinsics { get; set; } + + private PSHost ExternalHost { get; set; } + + /// + /// Gets a boolean that indicates whether the debugger is currently stopped, + /// either at a breakpoint or because the user broke execution. + /// + public bool IsDebuggerStopped => + this.versionSpecificOperations.IsDebuggerStopped( + PromptNest, + CurrentRunspace.Runspace); + + /// + /// Gets the current state of the session. + /// + public PowerShellContextState SessionState + { + get; + private set; + } + + /// + /// Gets the PowerShell version details for the initial local runspace. + /// + public PowerShellVersionDetails LocalPowerShellVersion + { + get; + private set; + } + + /// + /// Gets or sets an IHostOutput implementation for use in + /// writing output to the console. + /// + private IHostOutput ConsoleWriter { get; set; } + + internal IHostInput ConsoleReader { get; private set; } + + /// + /// Gets details pertaining to the current runspace. + /// + public RunspaceDetails CurrentRunspace + { + get; + private set; + } + + /// + /// Gets a value indicating whether the current runspace + /// is ready for a command + /// + public bool IsAvailable => this.SessionState == PowerShellContextState.Ready; + + /// + /// Gets the working directory path the PowerShell context was inititially set when the debugger launches. + /// This path is used to determine whether a script in the call stack is an "external" script. + /// + public string InitialWorkingDirectory { get; private set; } + + #endregion + + #region Constructors + + /// + /// + /// + /// An ILogger implementation used for writing log messages. + /// + /// Indicates whether PSReadLine should be used if possible + /// + public PowerShellContextService(ILogger logger, bool isPSReadLineEnabled) + { + + this.logger = logger; + this.isPSReadLineEnabled = isPSReadLineEnabled; + } + + /// + /// + /// + /// + /// + /// + /// The EditorServicesPSHostUserInterface to use for this instance. + /// + /// An ILogger implementation to use for this instance. + /// + public static Runspace CreateRunspace( + HostDetails hostDetails, + PowerShellContextService powerShellContext, + EditorServicesPSHostUserInterface hostUserInterface, + ILogger logger) + { + var psHost = new EditorServicesPSHost(powerShellContext, hostDetails, hostUserInterface, logger); + powerShellContext.ConsoleWriter = hostUserInterface; + powerShellContext.ConsoleReader = hostUserInterface; + return CreateRunspace(psHost); + } + + /// + /// + /// + /// + /// + public static Runspace CreateRunspace(PSHost psHost) + { + InitialSessionState initialSessionState; + if (Environment.GetEnvironmentVariable("PSES_TEST_USE_CREATE_DEFAULT") == "1") { + initialSessionState = InitialSessionState.CreateDefault(); + } else { + initialSessionState = InitialSessionState.CreateDefault2(); + } + + Runspace runspace = RunspaceFactory.CreateRunspace(psHost, initialSessionState); + + // Windows PowerShell must be hosted in STA mode + // This must be set on the runspace *before* it is opened + if (s_runspaceApartmentStateSetter != null) + { + s_runspaceApartmentStateSetter(runspace, ApartmentState.STA); + } + + runspace.ThreadOptions = PSThreadOptions.ReuseThread; + runspace.Open(); + + return runspace; + } + + /// + /// Initializes a new instance of the PowerShellContext class using + /// an existing runspace for the session. + /// + /// An object containing the profile paths for the session. + /// The initial runspace to use for this instance. + /// If true, the PowerShellContext owns this runspace. + public void Initialize( + ProfilePaths profilePaths, + Runspace initialRunspace, + bool ownsInitialRunspace) + { + this.Initialize(profilePaths, initialRunspace, ownsInitialRunspace, null); + } + + /// + /// Initializes a new instance of the PowerShellContext class using + /// an existing runspace for the session. + /// + /// 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 IHostOutput implementation. Optional. + public void Initialize( + ProfilePaths profilePaths, + Runspace initialRunspace, + bool ownsInitialRunspace, + IHostOutput consoleHost) + { + Validate.IsNotNull("initialRunspace", initialRunspace); + + this.ownsInitialRunspace = ownsInitialRunspace; + this.SessionState = PowerShellContextState.NotStarted; + this.ConsoleWriter = consoleHost; + this.ConsoleReader = consoleHost as IHostInput; + + // Get the PowerShell runtime version + this.LocalPowerShellVersion = + PowerShellVersionDetails.GetVersionDetails( + initialRunspace, + this.logger); + + this.powerShell = PowerShell.Create(); + this.powerShell.Runspace = initialRunspace; + + this.initialRunspace = + new RunspaceDetails( + initialRunspace, + this.GetSessionDetailsInRunspace(initialRunspace), + this.LocalPowerShellVersion, + RunspaceLocation.Local, + RunspaceContext.Original, + null); + this.CurrentRunspace = this.initialRunspace; + + // Write out the PowerShell version for tracking purposes + this.logger.LogInformation( + string.Format( + "PowerShell runtime version: {0}, edition: {1}", + this.LocalPowerShellVersion.Version, + this.LocalPowerShellVersion.Edition)); + + Version powerShellVersion = this.LocalPowerShellVersion.Version; + if (powerShellVersion >= new Version(5, 0)) + { + this.versionSpecificOperations = new PowerShell5Operations(); + } + else + { + throw new NotSupportedException( + "This computer has an unsupported version of PowerShell installed: " + + powerShellVersion.ToString()); + } + + if (this.LocalPowerShellVersion.Edition != "Linux") + { + // TODO: Should this be configurable? + this.SetExecutionPolicy(ExecutionPolicy.RemoteSigned); + } + + // Set up the runspace + this.ConfigureRunspace(this.CurrentRunspace); + + // Add runspace capabilities + this.ConfigureRunspaceCapabilities(this.CurrentRunspace); + + // Set the $profile variable in the runspace + this.profilePaths = profilePaths; + if (this.profilePaths != null) + { + this.SetProfileVariableInCurrentRunspace(profilePaths); + } + + // Now that initialization is complete we can watch for InvocationStateChanged + this.SessionState = PowerShellContextState.Ready; + + // EngineIntrinsics is used in some instances to interact with the initial + // runspace without having to wait for PSReadLine to check for events. + this.EngineIntrinsics = + initialRunspace + .SessionStateProxy + .PSVariable + .GetValue("ExecutionContext") + as EngineIntrinsics; + + // The external host is used to properly exit from a nested prompt that + // was entered by the user. + this.ExternalHost = + initialRunspace + .SessionStateProxy + .PSVariable + .GetValue("Host") + as PSHost; + + // Now that the runspace is ready, enqueue it for first use + this.PromptNest = new PromptNest( + this, + this.powerShell, + this.ConsoleReader, + this.versionSpecificOperations); + this.InvocationEventQueue = InvocationEventQueue.Create(this, this.PromptNest); + + if (powerShellVersion.Major >= 5 && + this.isPSReadLineEnabled && + PSReadLinePromptContext.TryGetPSReadLineProxy(logger, initialRunspace, out PSReadLineProxy proxy)) + { + this.PromptContext = new PSReadLinePromptContext( + this, + this.PromptNest, + this.InvocationEventQueue, + proxy); + } + else + { + this.PromptContext = new LegacyReadLineContext(this); + } + } + + /// + /// Imports the PowerShellEditorServices.Commands module into + /// the runspace. This method will be moved somewhere else soon. + /// + /// + /// + public Task ImportCommandsModuleAsync(string moduleBasePath) + { + PSCommand importCommand = new PSCommand(); + importCommand + .AddCommand("Import-Module") + .AddArgument( + Path.Combine( + moduleBasePath, + "PowerShellEditorServices.Commands.psd1")); + + return this.ExecuteCommandAsync(importCommand, false, false); + } + + private static bool CheckIfRunspaceNeedsEventHandlers(RunspaceDetails runspaceDetails) + { + // The only types of runspaces that need to be configured are: + // - Locally created runspaces + // - Local process entered with Enter-PSHostProcess + // - Remote session entered with Enter-PSSession + return + (runspaceDetails.Location == RunspaceLocation.Local && + (runspaceDetails.Context == RunspaceContext.Original || + runspaceDetails.Context == RunspaceContext.EnteredProcess)) || + (runspaceDetails.Location == RunspaceLocation.Remote && runspaceDetails.Context == RunspaceContext.Original); + } + + private void ConfigureRunspace(RunspaceDetails runspaceDetails) + { + runspaceDetails.Runspace.StateChanged += this.HandleRunspaceStateChanged; + if (runspaceDetails.Runspace.Debugger != null) + { + runspaceDetails.Runspace.Debugger.BreakpointUpdated += OnBreakpointUpdated; + runspaceDetails.Runspace.Debugger.DebuggerStop += OnDebuggerStop; + } + + this.versionSpecificOperations.ConfigureDebugger(runspaceDetails.Runspace); + } + + private void CleanupRunspace(RunspaceDetails runspaceDetails) + { + runspaceDetails.Runspace.StateChanged -= this.HandleRunspaceStateChanged; + if (runspaceDetails.Runspace.Debugger != null) + { + runspaceDetails.Runspace.Debugger.BreakpointUpdated -= OnBreakpointUpdated; + runspaceDetails.Runspace.Debugger.DebuggerStop -= OnDebuggerStop; + } + } + + #endregion + + #region Public Methods + + /// + /// Gets a RunspaceHandle for the session's runspace. This + /// handle is used to gain temporary ownership of the runspace + /// so that commands can be executed against it directly. + /// + /// A RunspaceHandle instance that gives access to the session's runspace. + public Task GetRunspaceHandleAsync() + { + return this.GetRunspaceHandleImplAsync(CancellationToken.None, isReadLine: false); + } + + /// + /// Gets a RunspaceHandle for the session's runspace. This + /// handle is used to gain temporary ownership of the runspace + /// so that commands can be executed against it directly. + /// + /// A CancellationToken that can be used to cancel the request. + /// A RunspaceHandle instance that gives access to the session's runspace. + public Task GetRunspaceHandleAsync(CancellationToken cancellationToken) + { + return this.GetRunspaceHandleImplAsync(cancellationToken, isReadLine: false); + } + + /// + /// Executes a PSCommand against the session's runspace and returns + /// a collection of results of the expected type. + /// + /// The expected result type. + /// The PSCommand to be executed. + /// + /// If true, causes any output written during command execution to be written to the host. + /// + /// + /// If true, causes any errors encountered during command execution to be written to the host. + /// + /// + /// An awaitable Task which will provide results once the command + /// execution completes. + /// + public async Task> ExecuteCommandAsync( + PSCommand psCommand, + bool sendOutputToHost = false, + bool sendErrorToHost = true) + { + return await ExecuteCommandAsync(psCommand, null, sendOutputToHost, sendErrorToHost); + } + + /// + /// Executes a PSCommand against the session's runspace and returns + /// a collection of results of the expected type. + /// + /// The expected result type. + /// The PSCommand to be executed. + /// Error messages from PowerShell will be written to the StringBuilder. + /// + /// If true, causes any output written during command execution to be written to the host. + /// + /// + /// If true, causes any errors encountered during command execution to be written to the host. + /// + /// + /// If true, adds the command to the user's command history. + /// + /// + /// An awaitable Task which will provide results once the command + /// execution completes. + /// + public Task> ExecuteCommandAsync( + PSCommand psCommand, + StringBuilder errorMessages, + bool sendOutputToHost = false, + bool sendErrorToHost = true, + bool addToHistory = false) + { + return + this.ExecuteCommandAsync( + psCommand, + errorMessages, + new ExecutionOptions + { + WriteOutputToHost = sendOutputToHost, + WriteErrorsToHost = sendErrorToHost, + AddToHistory = addToHistory + }); + } + + /// + /// Executes a PSCommand against the session's runspace and returns + /// a collection of results of the expected type. + /// + /// The expected result type. + /// The PSCommand to be executed. + /// Error messages from PowerShell will be written to the StringBuilder. + /// Specifies options to be used when executing this command. + /// + /// An awaitable Task which will provide results once the command + /// execution completes. + /// + public async Task> ExecuteCommandAsync( + PSCommand psCommand, + StringBuilder errorMessages, + ExecutionOptions executionOptions) + { + // Add history to PSReadLine before cancelling, otherwise it will be restored as the + // cancelled prompt when it's called again. + if (executionOptions.AddToHistory) + { + this.PromptContext.AddToHistory(psCommand.Commands[0].CommandText); + } + + bool hadErrors = false; + RunspaceHandle runspaceHandle = null; + ExecutionTarget executionTarget = ExecutionTarget.PowerShell; + IEnumerable executionResult = Enumerable.Empty(); + var shouldCancelReadLine = + executionOptions.InterruptCommandPrompt || + executionOptions.WriteOutputToHost; + + // If the debugger is active and the caller isn't on the pipeline + // thread, send the command over to that thread to be executed. + // Determine if execution should take place in a different thread + // using the following criteria: + // 1. The current frame in the prompt nest has a thread controller + // (meaning it is a nested prompt or is in the debugger) + // 2. We aren't already on the thread in question + // 3. The command is not a candidate for background invocation + // via PowerShell eventing + // 4. The command cannot be for a PSReadLine pipeline while we + // are currently in a out of process runspace + var threadController = PromptNest.GetThreadController(); + if (!(threadController == null || + !threadController.IsPipelineThread || + threadController.IsCurrentThread() || + this.ShouldExecuteWithEventing(executionOptions) || + (PromptNest.IsRemote && executionOptions.IsReadLine))) + { + this.logger.LogTrace("Passing command execution to pipeline thread."); + + if (shouldCancelReadLine && PromptNest.IsReadLineBusy()) + { + // If a ReadLine pipeline is running in the debugger then we'll hang here + // if we don't cancel it. Typically we can rely on OnExecutionStatusChanged but + // the pipeline request won't even start without clearing the current task. + this.ConsoleReader?.StopCommandLoop(); + } + + // Send the pipeline execution request to the pipeline thread + return await threadController.RequestPipelineExecutionAsync( + new PipelineExecutionRequest( + this, + psCommand, + errorMessages, + executionOptions)); + } + else + { + try + { + // Instruct PowerShell to send output and errors to the host + if (executionOptions.WriteOutputToHost) + { + psCommand.Commands[0].MergeMyResults( + PipelineResultTypes.Error, + PipelineResultTypes.Output); + + psCommand.Commands.Add( + this.GetOutputCommand( + endOfStatement: false)); + } + + executionTarget = GetExecutionTarget(executionOptions); + + // If a ReadLine pipeline is running we can still execute commands that + // don't write output (e.g. command completion) + if (executionTarget == ExecutionTarget.InvocationEvent) + { + return (await this.InvocationEventQueue.ExecuteCommandOnIdleAsync( + psCommand, + errorMessages, + executionOptions)); + } + + // Prompt is stopped and started based on the execution status, so naturally + // we don't want PSReadLine pipelines to factor in. + if (!executionOptions.IsReadLine) + { + this.OnExecutionStatusChanged( + ExecutionStatus.Running, + executionOptions, + false); + } + + runspaceHandle = await this.GetRunspaceHandleAsync(executionOptions.IsReadLine); + if (executionOptions.WriteInputToHost) + { + this.WriteOutput(psCommand.Commands[0].CommandText, true); + } + + if (executionTarget == ExecutionTarget.Debugger) + { + // Manually change the session state for debugger commands because + // we don't have an invocation state event to attach to. + if (!executionOptions.IsReadLine) + { + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Running, + PowerShellExecutionResult.NotFinished, + null)); + } + try + { + return this.ExecuteCommandInDebugger( + psCommand, + executionOptions.WriteOutputToHost); + } + catch (Exception e) + { + logger.LogError( + "Exception occurred while executing debugger command:\r\n\r\n" + e.ToString()); + } + finally + { + if (!executionOptions.IsReadLine) + { + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Ready, + PowerShellExecutionResult.Stopped, + null)); + } + } + } + + var invocationSettings = new PSInvocationSettings() + { + AddToHistory = executionOptions.AddToHistory + }; + + this.logger.LogTrace( + string.Format( + "Attempting to execute command(s):\r\n\r\n{0}", + GetStringForPSCommand(psCommand))); + + + PowerShell shell = this.PromptNest.GetPowerShell(executionOptions.IsReadLine); + shell.Commands = psCommand; + + // Don't change our SessionState for ReadLine. + if (!executionOptions.IsReadLine) + { + shell.InvocationStateChanged += powerShell_InvocationStateChanged; + } + + shell.Runspace = executionOptions.ShouldExecuteInOriginalRunspace + ? this.initialRunspace.Runspace + : this.CurrentRunspace.Runspace; + try + { + // Nested PowerShell instances can't be invoked asynchronously. This occurs + // in nested prompts and pipeline requests from eventing. + if (shell.IsNested) + { + return shell.Invoke(null, invocationSettings); + } + + return await Task.Factory.StartNew>( + () => shell.Invoke(null, invocationSettings), + CancellationToken.None, // Might need a cancellation token + TaskCreationOptions.None, + TaskScheduler.Default); + } + finally + { + if (!executionOptions.IsReadLine) + { + shell.InvocationStateChanged -= powerShell_InvocationStateChanged; + } + + if (shell.HadErrors) + { + var strBld = new StringBuilder(1024); + strBld.AppendFormat("Execution of the following command(s) completed with errors:\r\n\r\n{0}\r\n", + GetStringForPSCommand(psCommand)); + + int i = 1; + foreach (var error in shell.Streams.Error) + { + if (i > 1) strBld.Append("\r\n\r\n"); + strBld.Append($"Error #{i++}:\r\n"); + strBld.Append(error.ToString() + "\r\n"); + strBld.Append("ScriptStackTrace:\r\n"); + strBld.Append((error.ScriptStackTrace ?? "") + "\r\n"); + strBld.Append($"Exception:\r\n {error.Exception?.ToString() ?? ""}"); + Exception innerEx = error.Exception?.InnerException; + while (innerEx != null) + { + strBld.Append($"InnerException:\r\n {innerEx.ToString()}"); + innerEx = innerEx.InnerException; + } + } + + // We've reported these errors, clear them so they don't keep showing up. + shell.Streams.Error.Clear(); + + var errorMessage = strBld.ToString(); + + errorMessages?.Append(errorMessage); + this.logger.LogError(errorMessage); + + hadErrors = true; + } + else + { + this.logger.LogTrace( + "Execution completed successfully."); + } + } + } + catch (PSRemotingDataStructureException e) + { + this.logger.LogError( + "Pipeline stopped while executing command:\r\n\r\n" + e.ToString()); + + errorMessages?.Append(e.Message); + } + catch (PipelineStoppedException e) + { + this.logger.LogError( + "Pipeline stopped while executing command:\r\n\r\n" + e.ToString()); + + errorMessages?.Append(e.Message); + } + catch (RuntimeException e) + { + this.logger.LogWarning( + "Runtime exception occurred while executing command:\r\n\r\n" + e.ToString()); + + hadErrors = true; + errorMessages?.Append(e.Message); + + if (executionOptions.WriteErrorsToHost) + { + // Write the error to the host + this.WriteExceptionToHost(e); + } + } + catch (Exception) + { + this.OnExecutionStatusChanged( + ExecutionStatus.Failed, + executionOptions, + true); + + throw; + } + finally + { + // If the RunspaceAvailability is None, it means that the runspace we're in is dead. + // If this is the case, we should abort the execution which will clean up the runspace + // (and clean up the debugger) and then pop it off the stack. + // An example of when this happens is when the "attach" debug config is used and the + // process you're attached to dies randomly. + if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.None) + { + this.AbortExecution(shouldAbortDebugSession: true); + this.PopRunspace(); + } + + // Get the new prompt before releasing the runspace handle + if (executionOptions.WriteOutputToHost) + { + SessionDetails sessionDetails = null; + + // Get the SessionDetails and then write the prompt + if (executionTarget == ExecutionTarget.Debugger) + { + sessionDetails = this.GetSessionDetailsInDebugger(); + } + else if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.Available) + { + // This state can happen if the user types a command that causes the + // debugger to exit before we reach this point. No RunspaceHandle + // will exist already so we need to create one and then use it + if (runspaceHandle == null) + { + runspaceHandle = await this.GetRunspaceHandleAsync(); + } + + sessionDetails = this.GetSessionDetailsInRunspace(runspaceHandle.Runspace); + } + else + { + sessionDetails = this.GetSessionDetailsInNestedPipeline(); + } + + // Check if the runspace has changed + this.UpdateRunspaceDetailsIfSessionChanged(sessionDetails); + } + + // Dispose of the execution context + if (runspaceHandle != null) + { + runspaceHandle.Dispose(); + } + + this.OnExecutionStatusChanged( + ExecutionStatus.Completed, + executionOptions, + hadErrors); + } + } + + return executionResult; + } + + /// + /// Executes a PSCommand in the session's runspace without + /// expecting to receive any result. + /// + /// The PSCommand to be executed. + /// + /// An awaitable Task that the caller can use to know when + /// execution completes. + /// + public Task ExecuteCommandAsync(PSCommand psCommand) + { + return this.ExecuteCommandAsync(psCommand); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// A Task that can be awaited for the script completion. + public Task> ExecuteScriptStringAsync( + string scriptString) + { + return this.ExecuteScriptStringAsync(scriptString, false, true); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// Error messages from PowerShell will be written to the StringBuilder. + /// A Task that can be awaited for the script completion. + public Task> ExecuteScriptStringAsync( + string scriptString, + StringBuilder errorMessages) + { + return this.ExecuteScriptStringAsync(scriptString, errorMessages, false, true, false); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// If true, causes the script string to be written to the host. + /// If true, causes the script output to be written to the host. + /// A Task that can be awaited for the script completion. + public Task> ExecuteScriptStringAsync( + string scriptString, + bool writeInputToHost, + bool writeOutputToHost) + { + return this.ExecuteScriptStringAsync(scriptString, null, writeInputToHost, writeOutputToHost, false); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// If true, causes the script string to be written to the host. + /// If true, causes the script output to be written to the host. + /// If true, adds the command to the user's command history. + /// A Task that can be awaited for the script completion. + public Task> ExecuteScriptStringAsync( + string scriptString, + bool writeInputToHost, + bool writeOutputToHost, + bool addToHistory) + { + return this.ExecuteScriptStringAsync(scriptString, null, writeInputToHost, writeOutputToHost, addToHistory); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// Error messages from PowerShell will be written to the StringBuilder. + /// If true, causes the script string to be written to the host. + /// If true, causes the script output to be written to the host. + /// If true, adds the command to the user's command history. + /// A Task that can be awaited for the script completion. + public async Task> ExecuteScriptStringAsync( + string scriptString, + StringBuilder errorMessages, + bool writeInputToHost, + bool writeOutputToHost, + bool addToHistory) + { + return await this.ExecuteCommandAsync( + new PSCommand().AddScript(scriptString.Trim()), + errorMessages, + new ExecutionOptions() + { + WriteOutputToHost = writeOutputToHost, + AddToHistory = addToHistory, + WriteInputToHost = writeInputToHost + }); + } + + /// + /// Executes a script file at the specified path. + /// + /// The script execute. + /// Arguments to pass to the script. + /// Writes the executed script path and arguments to the host. + /// A Task that can be awaited for completion. + public async Task ExecuteScriptWithArgsAsync(string script, string arguments = null, bool writeInputToHost = false) + { + PSCommand command = new PSCommand(); + + if (arguments != null) + { + // Need to determine If the script string is a path to a script file. + string scriptAbsPath = string.Empty; + try + { + // Assume we can only debug scripts from the FileSystem provider + string workingDir = (await ExecuteCommandAsync( + new PSCommand() + .AddCommand("Microsoft.PowerShell.Management\\Get-Location") + .AddParameter("PSProvider", "FileSystem"), + false, + false)) + .FirstOrDefault() + .ProviderPath; + + workingDir = workingDir.TrimEnd(Path.DirectorySeparatorChar); + scriptAbsPath = workingDir + Path.DirectorySeparatorChar + script; + } + catch (System.Management.Automation.DriveNotFoundException e) + { + this.logger.LogError( + "Could not determine current filesystem location:\r\n\r\n" + e.ToString()); + } + + var strBld = new StringBuilder(); + + // The script parameter can refer to either a "script path" or a "command name". If it is a + // script path, we can determine that by seeing if the path exists. If so, we always single + // quote that path in case it includes special PowerShell characters like ', &, (, ), [, ] and + // . Any embedded single quotes are escaped. + // If the provided path is already quoted, then File.Exists will not find it. + // This keeps us from quoting an already quoted path. + // Related to issue #123. + if (File.Exists(script) || File.Exists(scriptAbsPath)) + { + // Dot-source the launched script path and single quote the path in case it includes + strBld.Append(". ").Append(QuoteEscapeString(script)); + } + else + { + strBld.Append(script); + } + + // Add arguments + strBld.Append(' ').Append(arguments); + + var launchedScript = strBld.ToString(); + this.logger.LogTrace($"Launch script is: {launchedScript}"); + + command.AddScript(launchedScript, false); + } + else + { + // AddCommand can handle script paths including those with special chars e.g.: + // ".\foo & [bar]\foo.ps1" and it can handle arbitrary commands, like "Invoke-Pester" + command.AddCommand(script, false); + } + + if (writeInputToHost) + { + this.WriteOutput( + script + Environment.NewLine, + true); + } + + await this.ExecuteCommandAsync( + command, + null, + sendOutputToHost: true, + addToHistory: true); + } + + /// + /// Forces the to trigger PowerShell event handling, + /// reliquishing control of the pipeline thread during event processing. + /// + /// + /// This method is called automatically by and + /// . Consider using them instead of this method directly when + /// possible. + /// + internal void ForcePSEventHandling() + { + PromptContext.ForcePSEventHandling(); + } + + /// + /// Marshals a to run on the pipeline thread. A new + /// will be created for the invocation. + /// + /// + /// The to invoke on the pipeline thread. The nested + /// instance for the created + /// will be passed as an argument. + /// + /// + /// An awaitable that the caller can use to know when execution completes. + /// + /// + /// This method is called automatically by . Consider using + /// that method instead of calling this directly when possible. + /// + internal async Task InvokeOnPipelineThreadAsync(Action invocationAction) + { + if (this.PromptNest.IsReadLineBusy()) + { + await this.InvocationEventQueue.InvokeOnPipelineThreadAsync(invocationAction); + return; + } + + // If this is invoked when ReadLine isn't busy then there shouldn't be any running + // pipelines. Right now this method is only used by command completion which doesn't + // actually require running on the pipeline thread, as long as nothing else is running. + invocationAction.Invoke(this.PromptNest.GetPowerShell()); + } + + internal async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await PromptContext.InvokeReadLineAsync( + isCommandLine, + cancellationToken); + } + + internal static TResult ExecuteScriptAndGetItem(string scriptToExecute, Runspace runspace, TResult defaultValue = default(TResult)) + { + using (PowerShell pwsh = PowerShell.Create()) + { + pwsh.Runspace = runspace; + IEnumerable results = pwsh.AddScript(scriptToExecute).Invoke(); + return results.DefaultIfEmpty(defaultValue).First(); + } + } + + /// + /// Loads PowerShell profiles for the host from the specified + /// profile locations. Only the profile paths which exist are + /// loaded. + /// + /// A Task that can be awaited for completion. + public async Task LoadHostProfilesAsync() + { + if (this.profilePaths != null) + { + // Load any of the profile paths that exist + foreach (var profilePath in this.profilePaths.GetLoadableProfilePaths()) + { + PSCommand command = new PSCommand(); + command.AddCommand(profilePath, false); + await this.ExecuteCommandAsync(command, true, true); + } + + // Gather the session details (particularly the prompt) after + // loading the user's profiles. + await this.GetSessionDetailsInRunspaceAsync(); + } + } + + /// + /// Causes the most recent execution to be aborted no matter what state + /// it is currently in. + /// + public void AbortExecution() + { + this.AbortExecution(shouldAbortDebugSession: false); + } + + /// + /// Causes the most recent execution to be aborted no matter what state + /// it is currently in. + /// + /// + /// A value indicating whether a debug session should be aborted if one + /// is currently active. + /// + public void AbortExecution(bool shouldAbortDebugSession) + { + if (this.SessionState != PowerShellContextState.Aborting && + this.SessionState != PowerShellContextState.Disposed) + { + this.logger.LogTrace("Execution abort requested..."); + + if (shouldAbortDebugSession) + { + this.ExitAllNestedPrompts(); + } + + if (this.PromptNest.IsInDebugger) + { + if (shouldAbortDebugSession) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + this.ResumeDebugger(DebuggerResumeAction.Stop); + } + else + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + } + else + { + this.PromptNest.GetPowerShell(isReadLine: false).BeginStop(null, null); + } + + this.SessionState = PowerShellContextState.Aborting; + + this.OnExecutionStatusChanged( + ExecutionStatus.Aborted, + null, + false); + } + else + { + this.logger.LogTrace( + string.Format( + $"Execution abort requested when already aborted (SessionState = {this.SessionState})")); + } + } + + /// + /// Exit all consecutive nested prompts that the user has entered. + /// + internal void ExitAllNestedPrompts() + { + while (this.PromptNest.IsNestedPrompt) + { + this.PromptNest.WaitForCurrentFrameExit(frame => this.ExitNestedPrompt()); + this.versionSpecificOperations.ExitNestedPrompt(ExternalHost); + } + } + + /// + /// Exit all consecutive nested prompts that the user has entered. + /// + /// + /// A task object that represents all nested prompts being exited + /// + internal async Task ExitAllNestedPromptsAsync() + { + while (this.PromptNest.IsNestedPrompt) + { + await this.PromptNest.WaitForCurrentFrameExitAsync(frame => this.ExitNestedPrompt()); + this.versionSpecificOperations.ExitNestedPrompt(ExternalHost); + } + } + + /// + /// Causes the debugger to break execution wherever it currently is. + /// This method is internal because the real Break API is provided + /// by the DebugService. + /// + internal void BreakExecution() + { + this.logger.LogTrace("Debugger break requested..."); + + // Pause the debugger + this.versionSpecificOperations.PauseDebugger( + this.CurrentRunspace.Runspace); + } + + internal void ResumeDebugger(DebuggerResumeAction resumeAction) + { + ResumeDebugger(resumeAction, shouldWaitForExit: true); + } + + private void ResumeDebugger(DebuggerResumeAction resumeAction, bool shouldWaitForExit) + { + resumeRequestHandle.Wait(); + try + { + if (this.PromptNest.IsNestedPrompt) + { + this.ExitAllNestedPrompts(); + } + + if (this.PromptNest.IsInDebugger) + { + // Set the result so that the execution thread resumes. + // The execution thread will clean up the task. + if (shouldWaitForExit) + { + this.PromptNest.WaitForCurrentFrameExit( + frame => + { + frame.ThreadController.StartThreadExit(resumeAction); + this.ConsoleReader?.StopCommandLoop(); + if (this.SessionState != PowerShellContextState.Ready) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + }); + } + else + { + this.PromptNest.GetThreadController().StartThreadExit(resumeAction); + this.ConsoleReader?.StopCommandLoop(); + if (this.SessionState != PowerShellContextState.Ready) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + } + } + else + { + this.logger.LogError( + $"Tried to resume debugger with action {resumeAction} but there was no debuggerStoppedTask."); + } + } + finally + { + resumeRequestHandle.Release(); + } + } + + /// + /// Disposes the runspace and any other resources being used + /// by this PowerShellContext. + /// + public void Dispose() + { + this.PromptNest.Dispose(); + this.SessionState = PowerShellContextState.Disposed; + + // Clean up the active runspace + this.CleanupRunspace(this.CurrentRunspace); + + // Push the active runspace so it will be included in the loop + this.runspaceStack.Push(this.CurrentRunspace); + + while (this.runspaceStack.Count > 0) + { + RunspaceDetails poppedRunspace = this.runspaceStack.Pop(); + + // Close the popped runspace if it isn't the initial runspace + // or if it is the initial runspace and we own that runspace + if (this.initialRunspace != poppedRunspace || this.ownsInitialRunspace) + { + this.CloseRunspace(poppedRunspace); + } + + this.OnRunspaceChanged( + this, + new RunspaceChangedEventArgs( + RunspaceChangeAction.Shutdown, + poppedRunspace, + null)); + } + + this.initialRunspace = null; + } + + private async Task GetRunspaceHandleAsync(bool isReadLine) + { + return await this.GetRunspaceHandleImplAsync(CancellationToken.None, isReadLine); + } + + private async Task GetRunspaceHandleImplAsync(CancellationToken cancellationToken, bool isReadLine) + { + return await this.PromptNest.GetRunspaceHandleAsync(cancellationToken, isReadLine); + } + + private ExecutionTarget GetExecutionTarget(ExecutionOptions options = null) + { + if (options == null) + { + options = new ExecutionOptions(); + } + + var noBackgroundInvocation = + options.InterruptCommandPrompt || + options.WriteOutputToHost || + options.IsReadLine || + PromptNest.IsRemote; + + // Take over the pipeline if PSReadLine is running, we aren't trying to run PSReadLine, and + // we aren't in a remote session. + if (!noBackgroundInvocation && PromptNest.IsReadLineBusy() && PromptNest.IsMainThreadBusy()) + { + return ExecutionTarget.InvocationEvent; + } + + // We can't take the pipeline from PSReadLine if it's in a remote session, so we need to + // invoke locally in that case. + if (IsDebuggerStopped && PromptNest.IsInDebugger && !(options.IsReadLine && PromptNest.IsRemote)) + { + return ExecutionTarget.Debugger; + } + + return ExecutionTarget.PowerShell; + } + + private bool ShouldExecuteWithEventing(ExecutionOptions executionOptions) + { + return + this.PromptNest.IsReadLineBusy() && + this.PromptNest.IsMainThreadBusy() && + !(executionOptions.IsReadLine || + executionOptions.InterruptCommandPrompt || + executionOptions.WriteOutputToHost || + IsCurrentRunspaceOutOfProcess()); + } + + private void CloseRunspace(RunspaceDetails runspaceDetails) + { + string exitCommand = null; + + switch (runspaceDetails.Context) + { + case RunspaceContext.Original: + if (runspaceDetails.Location == RunspaceLocation.Local) + { + runspaceDetails.Runspace.Close(); + runspaceDetails.Runspace.Dispose(); + } + else + { + exitCommand = "Exit-PSSession"; + } + + break; + + case RunspaceContext.EnteredProcess: + exitCommand = "Exit-PSHostProcess"; + break; + + case RunspaceContext.DebuggedRunspace: + // An attached runspace will be detached when the + // running pipeline is aborted + break; + } + + if (exitCommand != null) + { + Exception exitException = null; + + try + { + using (PowerShell ps = PowerShell.Create()) + { + ps.Runspace = runspaceDetails.Runspace; + ps.AddCommand(exitCommand); + ps.Invoke(); + } + } + catch (RemoteException e) + { + exitException = e; + } + catch (RuntimeException e) + { + exitException = e; + } + + if (exitException != null) + { + this.logger.LogError( + $"Caught {exitException.GetType().Name} while exiting {runspaceDetails.Location} runspace:\r\n{exitException.ToString()}"); + } + } + } + + internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) + { + Validate.IsNotNull("runspaceHandle", runspaceHandle); + + if (PromptNest.IsMainThreadBusy() || (runspaceHandle.IsReadLine && PromptNest.IsReadLineBusy())) + { + var unusedTask = PromptNest + .ReleaseRunspaceHandleAsync(runspaceHandle) + .ConfigureAwait(false); + } + else + { + // Write the situation to the log since this shouldn't happen + this.logger.LogError( + "ReleaseRunspaceHandle was called when the main thread was not busy."); + } + } + + /// + /// Determines if the current runspace is out of process. + /// + /// + /// A value indicating whether the current runspace is out of process. + /// + internal bool IsCurrentRunspaceOutOfProcess() + { + return + CurrentRunspace.Context == RunspaceContext.EnteredProcess || + CurrentRunspace.Context == RunspaceContext.DebuggedRunspace || + CurrentRunspace.Location == RunspaceLocation.Remote; + } + + /// + /// Called by the external PSHost when $Host.EnterNestedPrompt is called. + /// + internal void EnterNestedPrompt() + { + if (this.IsCurrentRunspaceOutOfProcess()) + { + throw new NotSupportedException(); + } + + this.PromptNest.PushPromptContext(PromptNestFrameType.NestedPrompt); + var localThreadController = this.PromptNest.GetThreadController(); + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Ready, + PowerShellExecutionResult.Stopped, + null)); + + // Reset command loop mainly for PSReadLine + this.ConsoleReader?.StopCommandLoop(); + this.ConsoleReader?.StartCommandLoop(); + + var localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); + var localDebuggerStoppedTask = localThreadController.Exit(); + + // Wait for off-thread pipeline requests and/or ExitNestedPrompt + while (true) + { + int taskIndex = Task.WaitAny( + localPipelineExecutionTask, + localDebuggerStoppedTask); + + if (taskIndex == 0) + { + var localExecutionTask = localPipelineExecutionTask.GetAwaiter().GetResult(); + localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); + localExecutionTask.ExecuteAsync().GetAwaiter().GetResult(); + continue; + } + + this.ConsoleReader?.StopCommandLoop(); + this.PromptNest.PopPromptContext(); + break; + } + } + + /// + /// Called by the external PSHost when $Host.ExitNestedPrompt is called. + /// + internal void ExitNestedPrompt() + { + if (this.PromptNest.NestedPromptLevel == 1 || !this.PromptNest.IsNestedPrompt) + { + this.logger.LogError( + "ExitNestedPrompt was called outside of a nested prompt."); + return; + } + + // Stop the command input loop so PSReadLine isn't invoked between ExitNestedPrompt + // being invoked and EnterNestedPrompt getting the message to exit. + this.ConsoleReader?.StopCommandLoop(); + this.PromptNest.GetThreadController().StartThreadExit(DebuggerResumeAction.Stop); + } + + /// + /// Sets the current working directory of the powershell context. The path should be + /// unescaped before calling this method. + /// + /// + public async Task SetWorkingDirectoryAsync(string path) + { + await this.SetWorkingDirectoryAsync(path, true); + } + + /// + /// Sets the current working directory of the powershell context. + /// + /// + /// Specify false to have the path escaped, otherwise specify true if the path has already been escaped. + public async Task SetWorkingDirectoryAsync(string path, bool isPathAlreadyEscaped) + { + this.InitialWorkingDirectory = path; + + if (!isPathAlreadyEscaped) + { + path = WildcardEscapePath(path); + } + + await ExecuteCommandAsync( + new PSCommand().AddCommand("Set-Location").AddParameter("Path", path), + null, + sendOutputToHost: false, + sendErrorToHost: false, + addToHistory: false); + } + + /// + /// Fully escape a given path for use in PowerShell script. + /// Note: this will not work with PowerShell.AddParameter() + /// + /// The path to escape. + /// An escaped version of the path that can be embedded in PowerShell script. + internal static string FullyPowerShellEscapePath(string path) + { + string wildcardEscapedPath = WildcardEscapePath(path); + return QuoteEscapeString(wildcardEscapedPath); + } + + /// + /// Wrap a string in quotes to make it safe to use in scripts. + /// + /// The glob-escaped path to wrap in quotes. + /// The given path wrapped in quotes appropriately. + internal static string QuoteEscapeString(string escapedPath) + { + var sb = new StringBuilder(escapedPath.Length + 2); // Length of string plus two quotes + sb.Append('\''); + if (!escapedPath.Contains('\'')) + { + sb.Append(escapedPath); + } + else + { + foreach (char c in escapedPath) + { + if (c == '\'') + { + sb.Append("''"); + continue; + } + + sb.Append(c); + } + } + sb.Append('\''); + return sb.ToString(); + } + + /// + /// Return the given path with all PowerShell globbing characters escaped, + /// plus optionally the whitespace. + /// + /// The path to process. + /// Specify True to escape spaces in the path, otherwise False. + /// The path with [ and ] escaped. + internal static string WildcardEscapePath(string path, bool escapeSpaces = false) + { + var sb = new StringBuilder(); + for (int i = 0; i < path.Length; i++) + { + char curr = path[i]; + switch (curr) + { + // Escape '[', ']', '?' and '*' with '`' + case '[': + case ']': + case '*': + case '?': + case '`': + sb.Append('`').Append(curr); + break; + + default: + // Escape whitespace if required + if (escapeSpaces && char.IsWhiteSpace(curr)) + { + sb.Append('`').Append(curr); + break; + } + sb.Append(curr); + break; + } + } + + return sb.ToString(); + } + + /// + /// Returns the passed in path with the [ and ] characters escaped. Escaping spaces is optional. + /// + /// The path to process. + /// Specify True to escape spaces in the path, otherwise False. + /// The path with [ and ] escaped. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This API is not meant for public usage and should not be used.")] + public static string EscapePath(string path, bool escapeSpaces) + { + return WildcardEscapePath(path, escapeSpaces); + } + + internal static string UnescapeWildcardEscapedPath(string wildcardEscapedPath) + { + // Prevent relying on my implementation if we can help it + if (!wildcardEscapedPath.Contains('`')) + { + return wildcardEscapedPath; + } + + var sb = new StringBuilder(wildcardEscapedPath.Length); + for (int i = 0; i < wildcardEscapedPath.Length; i++) + { + // If we see a backtick perform a lookahead + char curr = wildcardEscapedPath[i]; + if (curr == '`' && i + 1 < wildcardEscapedPath.Length) + { + // If the next char is an escapable one, don't add this backtick to the new string + char next = wildcardEscapedPath[i + 1]; + switch (next) + { + case '[': + case ']': + case '?': + case '*': + continue; + + default: + if (char.IsWhiteSpace(next)) + { + continue; + } + break; + } + } + + sb.Append(curr); + } + + return sb.ToString(); + } + + /// + /// Unescapes any escaped [, ] or space characters. Typically use this before calling a + /// .NET API that doesn't understand PowerShell escaped chars. + /// + /// The path to unescape. + /// The path with the ` character before [, ] and spaces removed. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This API is not meant for public usage and should not be used.")] + public static string UnescapePath(string path) + { + return UnescapeWildcardEscapedPath(path); + } + + #endregion + + #region Events + + /// + /// Raised when the state of the session has changed. + /// + public event EventHandler SessionStateChanged; + + private void OnSessionStateChanged(object sender, SessionStateChangedEventArgs e) + { + if (this.SessionState != PowerShellContextState.Disposed) + { + this.logger.LogTrace( + string.Format( + "Session state changed --\r\n\r\n Old state: {0}\r\n New state: {1}\r\n Result: {2}", + this.SessionState.ToString(), + e.NewSessionState.ToString(), + e.ExecutionResult)); + + this.SessionState = e.NewSessionState; + this.SessionStateChanged?.Invoke(sender, e); + } + else + { + this.logger.LogWarning( + $"Received session state change to {e.NewSessionState} when already disposed"); + } + } + + /// + /// Raised when the runspace changes by entering a remote session or one in a different process. + /// + public event EventHandler RunspaceChanged; + + private void OnRunspaceChanged(object sender, RunspaceChangedEventArgs e) + { + this.RunspaceChanged?.Invoke(sender, e); + } + + /// + /// Raised when the status of an executed command changes. + /// + public event EventHandler ExecutionStatusChanged; + + private void OnExecutionStatusChanged( + ExecutionStatus executionStatus, + ExecutionOptions executionOptions, + bool hadErrors) + { + this.ExecutionStatusChanged?.Invoke( + this, + new ExecutionStatusChangedEventArgs( + executionStatus, + executionOptions, + hadErrors)); + } + + #endregion + + #region Private Methods + + private IEnumerable ExecuteCommandInDebugger(PSCommand psCommand, bool sendOutputToHost) + { + this.logger.LogTrace( + string.Format( + "Attempting to execute command(s) in the debugger:\r\n\r\n{0}", + GetStringForPSCommand(psCommand))); + + IEnumerable output = + this.versionSpecificOperations.ExecuteCommandInDebugger( + this, + this.CurrentRunspace.Runspace, + psCommand, + sendOutputToHost, + out DebuggerResumeAction? debuggerResumeAction); + + if (debuggerResumeAction.HasValue) + { + // Resume the debugger with the specificed action + this.ResumeDebugger( + debuggerResumeAction.Value, + shouldWaitForExit: false); + } + + return output; + } + + internal void WriteOutput(string outputString, bool includeNewLine) + { + this.WriteOutput( + outputString, + includeNewLine, + OutputType.Normal); + } + + internal void WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType) + { + if (this.ConsoleWriter != null) + { + this.ConsoleWriter.WriteOutput( + outputString, + includeNewLine, + outputType); + } + } + + private void WriteExceptionToHost(Exception e) + { + const string ExceptionFormat = + "{0}\r\n{1}\r\n + CategoryInfo : {2}\r\n + FullyQualifiedErrorId : {3}"; + + IContainsErrorRecord containsErrorRecord = e as IContainsErrorRecord; + + if (containsErrorRecord == null || + containsErrorRecord.ErrorRecord == null) + { + this.WriteError(e.Message, null, 0, 0); + return; + } + + ErrorRecord errorRecord = containsErrorRecord.ErrorRecord; + if (errorRecord.InvocationInfo == null) + { + this.WriteError(errorRecord.ToString(), String.Empty, 0, 0); + return; + } + + string errorRecordString = errorRecord.ToString(); + if ((errorRecord.InvocationInfo.PositionMessage != null) && + errorRecordString.IndexOf(errorRecord.InvocationInfo.PositionMessage, StringComparison.Ordinal) != -1) + { + this.WriteError(errorRecordString); + return; + } + + string message = + string.Format( + CultureInfo.InvariantCulture, + ExceptionFormat, + errorRecord.ToString(), + errorRecord.InvocationInfo.PositionMessage, + errorRecord.CategoryInfo, + errorRecord.FullyQualifiedErrorId); + + this.WriteError(message); + } + + private void WriteError( + string errorMessage, + string filePath, + int lineNumber, + int columnNumber) + { + const string ErrorLocationFormat = "At {0}:{1} char:{2}"; + + this.WriteError( + errorMessage + + Environment.NewLine + + string.Format( + ErrorLocationFormat, + String.IsNullOrEmpty(filePath) ? "line" : filePath, + lineNumber, + columnNumber)); + } + + private void WriteError(string errorMessage) + { + if (this.ConsoleWriter != null) + { + this.ConsoleWriter.WriteOutput( + errorMessage, + true, + OutputType.Error, + ConsoleColor.Red, + ConsoleColor.Black); + } + } + + void powerShell_InvocationStateChanged(object sender, PSInvocationStateChangedEventArgs e) + { + SessionStateChangedEventArgs eventArgs = TranslateInvocationStateInfo(e.InvocationStateInfo); + this.OnSessionStateChanged(this, eventArgs); + } + + private static SessionStateChangedEventArgs TranslateInvocationStateInfo(PSInvocationStateInfo invocationState) + { + PowerShellContextState newState = PowerShellContextState.Unknown; + PowerShellExecutionResult executionResult = PowerShellExecutionResult.NotFinished; + + switch (invocationState.State) + { + case PSInvocationState.NotStarted: + newState = PowerShellContextState.NotStarted; + break; + + case PSInvocationState.Failed: + newState = PowerShellContextState.Ready; + executionResult = PowerShellExecutionResult.Failed; + break; + + case PSInvocationState.Disconnected: + // TODO: Any extra work to do in this case? + // TODO: Is this a unique state that can be re-connected? + newState = PowerShellContextState.Disposed; + executionResult = PowerShellExecutionResult.Stopped; + break; + + case PSInvocationState.Running: + newState = PowerShellContextState.Running; + break; + + case PSInvocationState.Completed: + newState = PowerShellContextState.Ready; + executionResult = PowerShellExecutionResult.Completed; + break; + + case PSInvocationState.Stopping: + newState = PowerShellContextState.Aborting; + break; + + case PSInvocationState.Stopped: + newState = PowerShellContextState.Ready; + executionResult = PowerShellExecutionResult.Aborted; + break; + + default: + newState = PowerShellContextState.Unknown; + break; + } + + return + new SessionStateChangedEventArgs( + newState, + executionResult, + invocationState.Reason); + } + + private Command GetOutputCommand(bool endOfStatement) + { + Command outputCommand = + new Command( + command: this.PromptNest.IsInDebugger ? "Out-String" : "Out-Default", + isScript: false, + useLocalScope: true); + + if (this.PromptNest.IsInDebugger) + { + // Out-String needs the -Stream parameter added + outputCommand.Parameters.Add("Stream"); + } + + return outputCommand; + } + + private static string GetStringForPSCommand(PSCommand psCommand) + { + StringBuilder stringBuilder = new StringBuilder(); + + foreach (var command in psCommand.Commands) + { + stringBuilder.Append(" "); + stringBuilder.Append(command.CommandText); + foreach (var param in command.Parameters) + { + if (param.Name != null) + { + stringBuilder.Append($" -{param.Name} {param.Value}"); + } + else + { + stringBuilder.Append($" {param.Value}"); + } + } + + stringBuilder.AppendLine(); + } + + return stringBuilder.ToString(); + } + + private void SetExecutionPolicy(ExecutionPolicy desiredExecutionPolicy) + { + var currentPolicy = ExecutionPolicy.Undefined; + + // Get the current execution policy so that we don't set it higher than it already is + this.powerShell.Commands.AddCommand("Get-ExecutionPolicy"); + + var result = this.powerShell.Invoke(); + if (result.Count > 0) + { + currentPolicy = result.FirstOrDefault(); + } + + if (desiredExecutionPolicy < currentPolicy || + desiredExecutionPolicy == ExecutionPolicy.Bypass || + currentPolicy == ExecutionPolicy.Undefined) + { + this.logger.LogTrace( + string.Format( + "Setting execution policy:\r\n Current = ExecutionPolicy.{0}\r\n Desired = ExecutionPolicy.{1}", + currentPolicy, + desiredExecutionPolicy)); + + this.powerShell.Commands.Clear(); + this.powerShell + .AddCommand("Set-ExecutionPolicy") + .AddParameter("ExecutionPolicy", desiredExecutionPolicy) + .AddParameter("Scope", ExecutionPolicyScope.Process) + .AddParameter("Force"); + + try + { + this.powerShell.Invoke(); + } + catch (CmdletInvocationException e) + { + this.logger.LogException( + $"An error occurred while calling Set-ExecutionPolicy, the desired policy of {desiredExecutionPolicy} may not be set.", + e); + } + + this.powerShell.Commands.Clear(); + } + else + { + this.logger.LogTrace( + string.Format( + "Current execution policy: ExecutionPolicy.{0}", + currentPolicy)); + + } + } + + private SessionDetails GetSessionDetails(Func invokeAction) + { + try + { + this.mostRecentSessionDetails = + new SessionDetails( + invokeAction( + SessionDetails.GetDetailsCommand())); + + return this.mostRecentSessionDetails; + } + catch (RuntimeException e) + { + this.logger.LogTrace( + "Runtime exception occurred while gathering runspace info:\r\n\r\n" + e.ToString()); + } + catch (ArgumentNullException) + { + this.logger.LogError( + "Could not retrieve session details but no exception was thrown."); + } + + // TODO: Return a harmless object if necessary + this.mostRecentSessionDetails = null; + return this.mostRecentSessionDetails; + } + + private async Task GetSessionDetailsInRunspaceAsync() + { + using (RunspaceHandle runspaceHandle = await this.GetRunspaceHandleAsync()) + { + return this.GetSessionDetailsInRunspace(runspaceHandle.Runspace); + } + } + + private SessionDetails GetSessionDetailsInRunspace(Runspace runspace) + { + SessionDetails sessionDetails = + this.GetSessionDetails( + command => + { + using (PowerShell powerShell = PowerShell.Create()) + { + powerShell.Runspace = runspace; + powerShell.Commands = command; + + return + powerShell + .Invoke() + .FirstOrDefault(); + } + }); + + return sessionDetails; + } + + private SessionDetails GetSessionDetailsInDebugger() + { + return this.GetSessionDetails( + command => + { + // Use LastOrDefault to get the last item returned. This + // is necessary because advanced prompt functions (like those + // in posh-git) may return multiple objects in the result. + return + this.ExecuteCommandInDebugger(command, false) + .LastOrDefault(); + }); + } + + private SessionDetails GetSessionDetailsInNestedPipeline() + { + // We don't need to check what thread we're on here. If it's a local + // nested pipeline then we will already be on the correct thread, and + // non-debugger nested pipelines aren't supported in remote runspaces. + return this.GetSessionDetails( + command => + { + using (var localPwsh = PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + localPwsh.Commands = command; + return localPwsh.Invoke().FirstOrDefault(); + } + }); + } + + private void SetProfileVariableInCurrentRunspace(ProfilePaths profilePaths) + { + // Create the $profile variable + PSObject profile = new PSObject(profilePaths.CurrentUserCurrentHost); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.AllUsersAllHosts), + profilePaths.AllUsersAllHosts)); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.AllUsersCurrentHost), + profilePaths.AllUsersCurrentHost)); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.CurrentUserAllHosts), + profilePaths.CurrentUserAllHosts)); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.CurrentUserCurrentHost), + profilePaths.CurrentUserCurrentHost)); + + this.logger.LogTrace( + string.Format( + "Setting $profile variable in runspace. Current user host profile path: {0}", + profilePaths.CurrentUserCurrentHost)); + + // Set the variable in the runspace + this.powerShell.Commands.Clear(); + this.powerShell + .AddCommand("Set-Variable") + .AddParameter("Name", "profile") + .AddParameter("Value", profile) + .AddParameter("Option", "None"); + this.powerShell.Invoke(); + this.powerShell.Commands.Clear(); + } + + private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs args) + { + switch (args.RunspaceStateInfo.State) + { + case RunspaceState.Opening: + case RunspaceState.Opened: + // These cases don't matter, just return + return; + + case RunspaceState.Closing: + case RunspaceState.Closed: + case RunspaceState.Broken: + // If the runspace closes or fails, pop the runspace + ((IHostSupportsInteractiveSession)this).PopRunspace(); + break; + } + } + + #endregion + + #region Events + + // NOTE: This event is 'internal' because the DebugService provides + // the publicly consumable event. + internal event EventHandler DebuggerStop; + + /// + /// Raised when the debugger is resumed after it was previously stopped. + /// + public event EventHandler DebuggerResumed; + + private void StartCommandLoopOnRunspaceAvailable() + { + if (Interlocked.CompareExchange(ref this.isCommandLoopRestarterSet, 1, 1) == 1) + { + return; + } + + EventHandler handler = null; + handler = (runspace, eventArgs) => + { + if (eventArgs.RunspaceAvailability != RunspaceAvailability.Available || + this.versionSpecificOperations.IsDebuggerStopped(this.PromptNest, (Runspace)runspace)) + { + return; + } + + ((Runspace)runspace).AvailabilityChanged -= handler; + Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 0); + this.ConsoleReader?.StartCommandLoop(); + }; + + this.CurrentRunspace.Runspace.AvailabilityChanged += handler; + Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 1); + } + + private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) + { + if (CurrentRunspace.Context == RunspaceContext.Original) + { + StartCommandLoopOnRunspaceAvailable(); + } + + this.logger.LogTrace("Debugger stopped execution."); + + PromptNest.PushPromptContext( + IsCurrentRunspaceOutOfProcess() + ? PromptNestFrameType.Debug | PromptNestFrameType.Remote + : PromptNestFrameType.Debug); + + ThreadController localThreadController = PromptNest.GetThreadController(); + + // Update the session state + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Ready, + PowerShellExecutionResult.Stopped, + null)); + + // Get the session details and push the current + // runspace if the session has changed + SessionDetails sessionDetails = null; + try + { + sessionDetails = this.GetSessionDetailsInDebugger(); + } + catch (InvalidOperationException) + { + this.logger.LogTrace( + "Attempting to get session details failed, most likely due to a running pipeline that is attempting to stop."); + } + + if (!localThreadController.FrameExitTask.Task.IsCompleted) + { + // Push the current runspace if the session has changed + this.UpdateRunspaceDetailsIfSessionChanged(sessionDetails, isDebuggerStop: true); + + // Raise the event for the debugger service + this.DebuggerStop?.Invoke(sender, e); + } + + this.logger.LogTrace("Starting pipeline thread message loop..."); + + Task localPipelineExecutionTask = + localThreadController.TakeExecutionRequestAsync(); + Task localDebuggerStoppedTask = + localThreadController.Exit(); + while (true) + { + int taskIndex = + Task.WaitAny( + localDebuggerStoppedTask, + localPipelineExecutionTask); + + if (taskIndex == 0) + { + // Write a new output line before continuing + this.WriteOutput("", true); + + e.ResumeAction = localDebuggerStoppedTask.GetAwaiter().GetResult(); + this.logger.LogTrace("Received debugger resume action " + e.ResumeAction.ToString()); + + // Notify listeners that the debugger has resumed + this.DebuggerResumed?.Invoke(this, e.ResumeAction); + + // Pop the current RunspaceDetails if we were attached + // to a runspace and the resume action is Stop + if (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace && + e.ResumeAction == DebuggerResumeAction.Stop) + { + this.PopRunspace(); + } + else if (e.ResumeAction != DebuggerResumeAction.Stop) + { + // Update the session state + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Running, + PowerShellExecutionResult.NotFinished, + null)); + } + + break; + } + else if (taskIndex == 1) + { + this.logger.LogTrace("Received pipeline thread execution request."); + + IPipelineExecutionRequest executionRequest = localPipelineExecutionTask.Result; + localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); + executionRequest.ExecuteAsync().GetAwaiter().GetResult(); + + this.logger.LogTrace("Pipeline thread execution completed."); + + if (!this.versionSpecificOperations.IsDebuggerStopped( + this.PromptNest, + this.CurrentRunspace.Runspace)) + { + if (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace) + { + // Notify listeners that the debugger has resumed + this.DebuggerResumed?.Invoke(this, DebuggerResumeAction.Stop); + + // We're detached from the runspace now, send a runspace update. + this.PopRunspace(); + } + + // If the executed command caused the debugger to exit, break + // from the pipeline loop + break; + } + } + else + { + // TODO: How to handle this? + } + } + + PromptNest.PopPromptContext(); + } + + // NOTE: This event is 'internal' because the DebugService provides + // the publicly consumable event. + internal event EventHandler BreakpointUpdated; + + private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) + { + this.BreakpointUpdated?.Invoke(sender, e); + } + + #endregion + + #region Nested Classes + + private void ConfigureRunspaceCapabilities(RunspaceDetails runspaceDetails) + { + // TODO: Bring this back + //DscBreakpointCapability.CheckForCapability(this.CurrentRunspace, this, this.logger); + } + + private void PushRunspace(RunspaceDetails newRunspaceDetails) + { + this.logger.LogTrace( + $"Pushing {this.CurrentRunspace.Location} ({this.CurrentRunspace.Context}), new runspace is {newRunspaceDetails.Location} ({newRunspaceDetails.Context}), connection: {newRunspaceDetails.ConnectionString}"); + + RunspaceDetails previousRunspace = this.CurrentRunspace; + + if (newRunspaceDetails.Context == RunspaceContext.DebuggedRunspace) + { + this.WriteOutput( + $"Entering debugged runspace on {newRunspaceDetails.Location.ToString().ToLower()} machine {newRunspaceDetails.SessionDetails.ComputerName}", + true); + } + + // Switch out event handlers if necessary + if (CheckIfRunspaceNeedsEventHandlers(newRunspaceDetails)) + { + this.CleanupRunspace(previousRunspace); + this.ConfigureRunspace(newRunspaceDetails); + } + + this.runspaceStack.Push(previousRunspace); + this.CurrentRunspace = newRunspaceDetails; + + // Check for runspace capabilities + this.ConfigureRunspaceCapabilities(newRunspaceDetails); + + this.OnRunspaceChanged( + this, + new RunspaceChangedEventArgs( + RunspaceChangeAction.Enter, + previousRunspace, + this.CurrentRunspace)); + } + + private void UpdateRunspaceDetailsIfSessionChanged(SessionDetails sessionDetails, bool isDebuggerStop = false) + { + RunspaceDetails newRunspaceDetails = null; + + // If we've exited an entered process or debugged runspace, pop what we've + // got before we evaluate where we're at + if ( + (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace && + this.CurrentRunspace.SessionDetails.InstanceId != sessionDetails.InstanceId) || + (this.CurrentRunspace.Context == RunspaceContext.EnteredProcess && + this.CurrentRunspace.SessionDetails.ProcessId != sessionDetails.ProcessId)) + { + this.PopRunspace(); + } + + // Are we in a new session that the PushRunspace command won't + // notify us about? + // + // Possible cases: + // - Debugged runspace in a local or remote session + // - Entered process in a remote session + // + // We don't need additional logic to check for the cases that + // PowerShell would have notified us about because the CurrentRunspace + // will already be updated by PowerShell by the time we reach + // these checks. + + if (this.CurrentRunspace.SessionDetails.InstanceId != sessionDetails.InstanceId && isDebuggerStop) + { + // Are we on a local or remote computer? + bool differentComputer = + !string.Equals( + sessionDetails.ComputerName, + this.initialRunspace.SessionDetails.ComputerName, + StringComparison.CurrentCultureIgnoreCase); + + // We started debugging a runspace + newRunspaceDetails = + RunspaceDetails.CreateFromDebugger( + this.CurrentRunspace, + differentComputer ? RunspaceLocation.Remote : RunspaceLocation.Local, + RunspaceContext.DebuggedRunspace, + sessionDetails); + } + else if (this.CurrentRunspace.SessionDetails.ProcessId != sessionDetails.ProcessId) + { + // We entered a different PowerShell host process + newRunspaceDetails = + RunspaceDetails.CreateFromContext( + this.CurrentRunspace, + RunspaceContext.EnteredProcess, + sessionDetails); + } + + if (newRunspaceDetails != null) + { + this.PushRunspace(newRunspaceDetails); + } + } + + private void PopRunspace() + { + if (this.SessionState != PowerShellContextState.Disposed) + { + if (this.runspaceStack.Count > 0) + { + RunspaceDetails previousRunspace = this.CurrentRunspace; + this.CurrentRunspace = this.runspaceStack.Pop(); + + this.logger.LogTrace( + $"Popping {previousRunspace.Location} ({previousRunspace.Context}), new runspace is {this.CurrentRunspace.Location} ({this.CurrentRunspace.Context}), connection: {this.CurrentRunspace.ConnectionString}"); + + if (previousRunspace.Context == RunspaceContext.DebuggedRunspace) + { + this.WriteOutput( + $"Leaving debugged runspace on {previousRunspace.Location.ToString().ToLower()} machine {previousRunspace.SessionDetails.ComputerName}", + true); + } + + // Switch out event handlers if necessary + if (CheckIfRunspaceNeedsEventHandlers(previousRunspace)) + { + this.CleanupRunspace(previousRunspace); + this.ConfigureRunspace(this.CurrentRunspace); + } + + this.OnRunspaceChanged( + this, + new RunspaceChangedEventArgs( + RunspaceChangeAction.Exit, + previousRunspace, + this.CurrentRunspace)); + } + else + { + this.logger.LogError( + "Caller attempted to pop a runspace when no runspaces are on the stack."); + } + } + } + + #endregion + + #region IHostSupportsInteractiveSession Implementation + + bool IHostSupportsInteractiveSession.IsRunspacePushed + { + get + { + return this.runspaceStack.Count > 0; + } + } + + Runspace IHostSupportsInteractiveSession.Runspace + { + get + { + return this.CurrentRunspace.Runspace; + } + } + + void IHostSupportsInteractiveSession.PushRunspace(Runspace runspace) + { + // Get the session details for the new runspace + SessionDetails sessionDetails = this.GetSessionDetailsInRunspace(runspace); + + this.PushRunspace( + RunspaceDetails.CreateFromRunspace( + runspace, + sessionDetails, + this.logger)); + } + + void IHostSupportsInteractiveSession.PopRunspace() + { + this.PopRunspace(); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs new file mode 100644 index 000000000..03ce2abe9 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs @@ -0,0 +1,166 @@ +//// +//// 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.Tasks; + +//namespace Microsoft.PowerShell.EditorServices.Session.Capabilities +//{ +// using Microsoft.Extensions.Logging; +// using Microsoft.PowerShell.EditorServices.Utility; +// using System; +// using System.Collections.Generic; +// using System.Collections.ObjectModel; +// using System.Management.Automation; + +// internal class DscBreakpointCapability : IRunspaceCapability +// { +// private string[] dscResourceRootPaths = new string[0]; + +// private Dictionary breakpointsPerFile = +// new Dictionary(); + +// public async Task> SetLineBreakpointsAsync( +// PowerShellContextService powerShellContext, +// string scriptPath, +// BreakpointDetails[] breakpoints) +// { +// List resultBreakpointDetails = +// new List(); + +// // We always get the latest array of breakpoint line numbers +// // so store that for future use +// if (breakpoints.Length > 0) +// { +// // Set the breakpoints for this scriptPath +// this.breakpointsPerFile[scriptPath] = +// breakpoints.Select(b => b.LineNumber).ToArray(); +// } +// else +// { +// // No more breakpoints for this scriptPath, remove it +// this.breakpointsPerFile.Remove(scriptPath); +// } + +// string hashtableString = +// string.Join( +// ", ", +// this.breakpointsPerFile +// .Select(file => $"@{{Path=\"{file.Key}\";Line=@({string.Join(",", file.Value)})}}")); + +// // Run Enable-DscDebug as a script because running it as a PSCommand +// // causes an error which states that the Breakpoint parameter has not +// // been passed. +// await powerShellContext.ExecuteScriptStringAsync( +// hashtableString.Length > 0 +// ? $"Enable-DscDebug -Breakpoint {hashtableString}" +// : "Disable-DscDebug", +// false, +// false); + +// // Verify all the breakpoints and return them +// foreach (var breakpoint in breakpoints) +// { +// breakpoint.Verified = true; +// } + +// return breakpoints.ToList(); +// } + +// public bool IsDscResourcePath(string scriptPath) +// { +// return dscResourceRootPaths.Any( +// dscResourceRootPath => +// scriptPath.StartsWith( +// dscResourceRootPath, +// StringComparison.CurrentCultureIgnoreCase)); +// } + +// public static DscBreakpointCapability CheckForCapability( +// RunspaceDetails runspaceDetails, +// PowerShellContextService powerShellContext, +// ILogger logger) +// { +// DscBreakpointCapability capability = null; + +// // DSC support is enabled only for Windows PowerShell. +// if ((runspaceDetails.PowerShellVersion.Version.Major < 6) && +// (runspaceDetails.Context != RunspaceContext.DebuggedRunspace)) +// { +// using (PowerShell powerShell = PowerShell.Create()) +// { +// powerShell.Runspace = runspaceDetails.Runspace; + +// // Attempt to import the updated DSC module +// powerShell.AddCommand("Import-Module"); +// powerShell.AddArgument(@"C:\Program Files\DesiredStateConfiguration\1.0.0.0\Modules\PSDesiredStateConfiguration\PSDesiredStateConfiguration.psd1"); +// powerShell.AddParameter("PassThru"); +// powerShell.AddParameter("ErrorAction", "Ignore"); + +// PSObject moduleInfo = null; + +// try +// { +// moduleInfo = powerShell.Invoke().FirstOrDefault(); +// } +// catch (RuntimeException e) +// { +// logger.LogException("Could not load the DSC module!", e); +// } + +// if (moduleInfo != null) +// { +// logger.LogTrace("Side-by-side DSC module found, gathering DSC resource paths..."); + +// // The module was loaded, add the breakpoint capability +// capability = new DscBreakpointCapability(); +// runspaceDetails.AddCapability(capability); + +// powerShell.Commands.Clear(); +// powerShell.AddScript("Write-Host \"Gathering DSC resource paths, this may take a while...\""); +// powerShell.Invoke(); + +// // Get the list of DSC resource paths +// powerShell.Commands.Clear(); +// powerShell.AddCommand("Get-DscResource"); +// powerShell.AddCommand("Select-Object"); +// powerShell.AddParameter("ExpandProperty", "ParentPath"); + +// Collection resourcePaths = null; + +// try +// { +// resourcePaths = powerShell.Invoke(); +// } +// catch (CmdletInvocationException e) +// { +// logger.LogException("Get-DscResource failed!", e); +// } + +// if (resourcePaths != null) +// { +// capability.dscResourceRootPaths = +// resourcePaths +// .Select(o => (string)o.BaseObject) +// .ToArray(); + +// logger.LogTrace($"DSC resources found: {resourcePaths.Count}"); +// } +// else +// { +// logger.LogTrace($"No DSC resources found."); +// } +// } +// else +// { +// logger.LogTrace($"Side-by-side DSC module was not found."); +// } +// } +// } + +// return capability; +// } +// } +//} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs new file mode 100644 index 000000000..a1071606f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Defines options for the execution of a command. + /// + public class ExecutionOptions + { + private bool? _shouldExecuteInOriginalRunspace; + + #region Properties + + /// + /// Gets or sets a boolean that determines whether command output + /// should be written to the host. + /// + public bool WriteOutputToHost { get; set; } + + /// + /// Gets or sets a boolean that determines whether command errors + /// should be written to the host. + /// + public bool WriteErrorsToHost { get; set; } + + /// + /// Gets or sets a boolean that determines whether the executed + /// command should be added to the command history. + /// + public bool AddToHistory { get; set; } + + /// + /// Gets or sets a boolean that determines whether the execution + /// of the command should interrupt the command prompt. Should + /// only be set if WriteOutputToHost is false but the command + /// should still interrupt the command prompt. + /// + public bool InterruptCommandPrompt { get; set; } + + /// + /// Gets or sets a value indicating whether the text of the command + /// should be written to the host as if it was ran interactively. + /// + public bool WriteInputToHost { get; set; } + + /// + /// Gets or sets a value indicating whether the command to + /// be executed is a console input prompt, such as the + /// PSConsoleHostReadLine function. + /// + internal bool IsReadLine { get; set; } + + /// + /// Gets or sets a value indicating whether the command should + /// be invoked in the original runspace. In the majority of cases + /// this should remain unset. + /// + internal bool ShouldExecuteInOriginalRunspace + { + get + { + return _shouldExecuteInOriginalRunspace ?? IsReadLine; + } + set + { + _shouldExecuteInOriginalRunspace = value; + } + } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ExecutionOptions class with + /// default settings configured. + /// + public ExecutionOptions() + { + this.WriteOutputToHost = true; + this.WriteErrorsToHost = true; + this.WriteInputToHost = false; + this.AddToHistory = false; + this.InterruptCommandPrompt = false; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs new file mode 100644 index 000000000..233d0499e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Enumerates the possible execution results that can occur after + /// executing a command or script. + /// + public enum ExecutionStatus + { + /// + /// Indicates that execution has not yet started. + /// + Pending, + + /// + /// Indicates that the command is executing. + /// + Running, + + /// + /// Indicates that execution has failed. + /// + Failed, + + /// + /// Indicates that execution was aborted by the user. + /// + Aborted, + + /// + /// Indicates that execution completed successfully. + /// + Completed + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs new file mode 100644 index 000000000..cd2dcaaf2 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Contains details about an executed + /// + public class ExecutionStatusChangedEventArgs + { + #region Properties + + /// + /// Gets the options used when the command was executed. + /// + public ExecutionOptions ExecutionOptions { get; private set; } + + /// + /// Gets the command execution's current status. + /// + public ExecutionStatus ExecutionStatus { get; private set; } + + /// + /// If true, the command execution had errors. + /// + public bool HadErrors { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ExecutionStatusChangedEventArgs class. + /// + /// The command execution's current status. + /// The options used when the command was executed. + /// If execution has completed, indicates whether there were errors. + public ExecutionStatusChangedEventArgs( + ExecutionStatus executionStatus, + ExecutionOptions executionOptions, + bool hadErrors) + { + this.ExecutionStatus = executionStatus; + this.ExecutionOptions = executionOptions; + this.HadErrors = hadErrors || (executionStatus == ExecutionStatus.Failed); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs new file mode 100644 index 000000000..70ec3cb6f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Represents the different API's available for executing commands. + /// + internal enum ExecutionTarget + { + /// + /// Indicates that the command should be invoked through the PowerShell debugger. + /// + Debugger, + + /// + /// Indicates that the command should be invoked via an instance of the PowerShell class. + /// + PowerShell, + + /// + /// Indicates that the command should be invoked through the PowerShell engine's event manager. + /// + InvocationEvent + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs new file mode 100644 index 000000000..52a94daae --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs @@ -0,0 +1,373 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine; +using Microsoft.PowerShell.EditorServices.Session; +using System; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides an implementation of the PSHost class for the + /// ConsoleService and routes its calls to an IConsoleHost + /// implementation. + /// + public class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession + { + #region Private Fields + + private ILogger Logger; + private HostDetails hostDetails; + private Guid instanceId = Guid.NewGuid(); + private EditorServicesPSHostUserInterface hostUserInterface; + private IHostSupportsInteractiveSession hostSupportsInteractiveSession; + private PowerShellContextService powerShellContext; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ConsoleServicePSHost class + /// with the given IConsoleHost implementation. + /// + /// + /// An implementation of IHostSupportsInteractiveSession for runspace management. + /// + /// + /// Provides details about the host application. + /// + /// + /// The EditorServicesPSHostUserInterface implementation to use for this host. + /// + /// An ILogger implementation to use for this host. + public EditorServicesPSHost( + PowerShellContextService powerShellContext, + HostDetails hostDetails, + EditorServicesPSHostUserInterface hostUserInterface, + ILogger logger) + { + this.Logger = logger; + this.hostDetails = hostDetails; + this.hostUserInterface = hostUserInterface; + this.hostSupportsInteractiveSession = powerShellContext; + this.powerShellContext = powerShellContext; + } + + #endregion + + #region PSHost Implementation + + /// + /// + /// + public override Guid InstanceId + { + get { return this.instanceId; } + } + + /// + /// + /// + public override string Name + { + get { return this.hostDetails.Name; } + } + + internal class ConsoleColorProxy + { + private EditorServicesPSHostUserInterface _hostUserInterface; + + internal ConsoleColorProxy(EditorServicesPSHostUserInterface hostUserInterface) + { + if (hostUserInterface == null) throw new ArgumentNullException("hostUserInterface"); + _hostUserInterface = hostUserInterface; + } + + /// + /// The ForegroundColor for Error + /// + public ConsoleColor ErrorForegroundColor + { + get + { return _hostUserInterface.ErrorForegroundColor; } + set + { _hostUserInterface.ErrorForegroundColor = value; } + } + + /// + /// The BackgroundColor for Error + /// + public ConsoleColor ErrorBackgroundColor + { + get + { return _hostUserInterface.ErrorBackgroundColor; } + set + { _hostUserInterface.ErrorBackgroundColor = value; } + } + + /// + /// The ForegroundColor for Warning + /// + public ConsoleColor WarningForegroundColor + { + get + { return _hostUserInterface.WarningForegroundColor; } + set + { _hostUserInterface.WarningForegroundColor = value; } + } + + /// + /// The BackgroundColor for Warning + /// + public ConsoleColor WarningBackgroundColor + { + get + { return _hostUserInterface.WarningBackgroundColor; } + set + { _hostUserInterface.WarningBackgroundColor = value; } + } + + /// + /// The ForegroundColor for Debug + /// + public ConsoleColor DebugForegroundColor + { + get + { return _hostUserInterface.DebugForegroundColor; } + set + { _hostUserInterface.DebugForegroundColor = value; } + } + + /// + /// The BackgroundColor for Debug + /// + public ConsoleColor DebugBackgroundColor + { + get + { return _hostUserInterface.DebugBackgroundColor; } + set + { _hostUserInterface.DebugBackgroundColor = value; } + } + + /// + /// The ForegroundColor for Verbose + /// + public ConsoleColor VerboseForegroundColor + { + get + { return _hostUserInterface.VerboseForegroundColor; } + set + { _hostUserInterface.VerboseForegroundColor = value; } + } + + /// + /// The BackgroundColor for Verbose + /// + public ConsoleColor VerboseBackgroundColor + { + get + { return _hostUserInterface.VerboseBackgroundColor; } + set + { _hostUserInterface.VerboseBackgroundColor = value; } + } + + /// + /// The ForegroundColor for Progress + /// + public ConsoleColor ProgressForegroundColor + { + get + { return _hostUserInterface.ProgressForegroundColor; } + set + { _hostUserInterface.ProgressForegroundColor = value; } + } + + /// + /// The BackgroundColor for Progress + /// + public ConsoleColor ProgressBackgroundColor + { + get + { return _hostUserInterface.ProgressBackgroundColor; } + set + { _hostUserInterface.ProgressBackgroundColor = value; } + } + } + + /// + /// Return the actual console host object so that the user can get at + /// the unproxied methods. + /// + public override PSObject PrivateData + { + get + { + if (hostUserInterface == null) return null; + return _consoleColorProxy ?? (_consoleColorProxy = PSObject.AsPSObject(new ConsoleColorProxy(hostUserInterface))); + } + } + private PSObject _consoleColorProxy; + + /// + /// + /// + public override Version Version + { + get { return this.hostDetails.Version; } + } + + // TODO: Pull these from IConsoleHost + + /// + /// + /// + public override System.Globalization.CultureInfo CurrentCulture + { + get { return System.Globalization.CultureInfo.CurrentCulture; } + } + + /// + /// + /// + public override System.Globalization.CultureInfo CurrentUICulture + { + get { return System.Globalization.CultureInfo.CurrentUICulture; } + } + + /// + /// + /// + public override PSHostUserInterface UI + { + get { return this.hostUserInterface; } + } + + /// + /// + /// + public override void EnterNestedPrompt() + { + this.powerShellContext.EnterNestedPrompt(); + } + + /// + /// + /// + public override void ExitNestedPrompt() + { + this.powerShellContext.ExitNestedPrompt(); + } + + /// + /// + /// + public override void NotifyBeginApplication() + { + Logger.LogTrace("NotifyBeginApplication() called."); + this.hostUserInterface.IsNativeApplicationRunning = true; + } + + /// + /// + /// + public override void NotifyEndApplication() + { + Logger.LogTrace("NotifyEndApplication() called."); + this.hostUserInterface.IsNativeApplicationRunning = false; + } + + /// + /// + /// + /// + public override void SetShouldExit(int exitCode) + { + if (this.IsRunspacePushed) + { + this.PopRunspace(); + } + } + + #endregion + + #region IHostSupportsInteractiveSession Implementation + + /// + /// + /// + /// + public bool IsRunspacePushed + { + get + { + if (this.hostSupportsInteractiveSession != null) + { + return this.hostSupportsInteractiveSession.IsRunspacePushed; + } + else + { + throw new NotImplementedException(); + } + } + } + + /// + /// + /// + /// + public Runspace Runspace + { + get + { + if (this.hostSupportsInteractiveSession != null) + { + return this.hostSupportsInteractiveSession.Runspace; + } + else + { + throw new NotImplementedException(); + } + } + } + + /// + /// + /// + /// + public void PushRunspace(Runspace runspace) + { + if (this.hostSupportsInteractiveSession != null) + { + this.hostSupportsInteractiveSession.PushRunspace(runspace); + } + else + { + throw new NotImplementedException(); + } + } + + /// + /// + /// + public void PopRunspace() + { + if (this.hostSupportsInteractiveSession != null) + { + this.hostSupportsInteractiveSession.PopRunspace(); + } + else + { + throw new NotImplementedException(); + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs new file mode 100644 index 000000000..e8139beed --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs @@ -0,0 +1,1068 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Linq; +using System.Security; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Console; +using System.Threading; +using Microsoft.PowerShell.EditorServices.Session; +using System.Globalization; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides an implementation of the PSHostUserInterface class + /// for the ConsoleService and routes its calls to an IConsoleHost + /// implementation. + /// + public abstract class EditorServicesPSHostUserInterface : + PSHostUserInterface, + IHostInput, + IHostOutput, + IHostUISupportsMultipleChoiceSelection + { + #region Private Fields + + private readonly ConcurrentDictionary currentProgressMessages = + new ConcurrentDictionary(); + private PromptHandler activePromptHandler; + private PSHostRawUserInterface rawUserInterface; + private CancellationTokenSource commandLoopCancellationToken; + + /// + /// The PowerShellContext to use for executing commands. + /// + protected PowerShellContextService 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 + +#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; } + + /// + /// Gets a value indicating whether writing progress is supported. + /// + internal protected virtual bool SupportsWriteProgress => false; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ConsoleServicePSHostUserInterface + /// class with the given IConsoleHost implementation. + /// + /// 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( + PowerShellContextService powerShellContext, + PSHostRawUserInterface rawUserInterface, + ILogger logger) + { + 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 + + /// + /// Starts the host's interactive command loop. + /// + public void StartCommandLoop() + { + if (!this.IsCommandLoopRunning) + { + this.IsCommandLoopRunning = true; + this.ShowCommandPrompt(); + } + } + + /// + /// Stops the host's interactive command loop. + /// + public void 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.StartReplLoopAsync(this.commandLoopCancellationToken.Token); + }); + } + else + { + Logger.LogTrace("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 ReadCommandLineAsync(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) + { + FieldDetails[] fields = + fieldDescriptions + .Select(f => { return FieldDetails.Create(f, this.Logger); }) + .ToArray(); + + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + Task> promptTask = + this.CreateInputPromptHandler() + .PromptForInputAsync( + 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) + { + // Convert all values to PSObjects + foreach (var keyValuePair in promptTask.Result) + { + psObjectDict.Add( + keyValuePair.Key, + keyValuePair.Value != null + ? PSObject.AsPSObject(keyValuePair.Value) + : null); + } + } + + // Return the result + return psObjectDict; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public override int PromptForChoice( + string promptCaption, + string promptMessage, + Collection choiceDescriptions, + int defaultChoice) + { + ChoiceDetails[] choices = + choiceDescriptions + .Select(ChoiceDetails.Create) + .ToArray(); + + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + Task promptTask = + this.CreateChoicePromptHandler() + .PromptForChoiceAsync( + 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, + string userName, + string targetName, + PSCredentialTypes allowedCredentialTypes, + PSCredentialUIOptions options) + { + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + + Task> promptTask = + this.CreateInputPromptHandler() + .PromptForInputAsync( + promptCaption, + promptMessage, + new FieldDetails[] { new CredentialFieldDetails("Credential", "Credential", userName) }, + cancellationToken.Token); + + Task unpackTask = + promptTask.ContinueWith( + task => + { + 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"]; + }); + + // 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, + string userName, + string targetName) + { + return this.PromptForCredential( + caption, + message, + userName, + targetName, + PSCredentialTypes.Default, + PSCredentialUIOptions.Default); + } + + /// + /// + /// + /// + public override PSHostRawUserInterface RawUI + { + get { return this.rawUserInterface; } + } + + /// + /// + /// + /// + public override string ReadLine() + { + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + + Task promptTask = + this.CreateInputPromptHandler() + .PromptForInputAsync(cancellationToken.Token); + + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "ReadLine", + cancellationToken); + + return promptTask.Result; + } + + /// + /// + /// + /// + public override SecureString ReadLineAsSecureString() + { + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + + Task promptTask = + this.CreateInputPromptHandler() + .PromptForSecureInputAsync(cancellationToken.Token); + + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "ReadLineAsSecureString", + cancellationToken); + + return promptTask.Result; + } + + /// + /// + /// + /// + /// + /// + public override void Write( + ConsoleColor foregroundColor, + ConsoleColor backgroundColor, + string value) + { + this.WriteOutput( + value, + false, + OutputType.Normal, + foregroundColor, + backgroundColor); + } + + /// + /// + /// + /// + public override void Write(string value) + { + this.WriteOutput( + value, + false, + OutputType.Normal, + this.rawUserInterface.ForegroundColor, + this.rawUserInterface.BackgroundColor); + } + + /// + /// + /// + /// + public override void WriteLine(string value) + { + this.WriteOutput( + value, + true, + OutputType.Normal, + this.rawUserInterface.ForegroundColor, + this.rawUserInterface.BackgroundColor); + } + + /// + /// + /// + /// + public override void WriteDebugLine(string message) + { + this.WriteOutput( + DebugMessagePrefix + message, + true, + OutputType.Debug, + foregroundColor: this.DebugForegroundColor, + backgroundColor: this.DebugBackgroundColor); + } + + /// + /// + /// + /// + public override void WriteVerboseLine(string message) + { + this.WriteOutput( + VerboseMessagePrefix + message, + true, + OutputType.Verbose, + foregroundColor: this.VerboseForegroundColor, + backgroundColor: this.VerboseBackgroundColor); + } + + /// + /// + /// + /// + public override void WriteWarningLine(string message) + { + this.WriteOutput( + WarningMessagePrefix + message, + true, + OutputType.Warning, + foregroundColor: this.WarningForegroundColor, + backgroundColor: this.WarningBackgroundColor); + } + + /// + /// + /// + /// + public override void WriteErrorLine(string value) + { + this.WriteOutput( + value, + true, + OutputType.Error, + foregroundColor: this.ErrorForegroundColor, + backgroundColor: this.ErrorBackgroundColor); + } + + /// + /// Invoked by to display a progress record. + /// + /// + /// Unique identifier of the source of the record. An int64 is used because typically, + /// the 'this' pointer of the command from whence the record is originating is used, and + /// that may be from a remote Runspace on a 64-bit machine. + /// + /// + /// The record being reported to the host. + /// + public sealed override void WriteProgress( + long sourceId, + ProgressRecord record) + { + // Maintain old behavior if this isn't overridden. + if (!this.SupportsWriteProgress) + { + this.UpdateProgress(sourceId, ProgressDetails.Create(record)); + return; + } + + // Keep a list of progress records we write so we can automatically + // clean them up after the pipeline ends. + if (record.RecordType == ProgressRecordType.Completed) + { + this.currentProgressMessages.TryRemove(new ProgressKey(sourceId, record), out _); + } + else + { + // Adding with a value of null here because we don't actually need a dictionary. We're + // only using ConcurrentDictionary<,> becuase there is no ConcurrentHashSet<>. + this.currentProgressMessages.TryAdd(new ProgressKey(sourceId, record), null); + } + + this.WriteProgressImpl(sourceId, record); + } + + /// + /// Invoked by to display a progress record. + /// + /// + /// Unique identifier of the source of the record. An int64 is used because typically, + /// the 'this' pointer of the command from whence the record is originating is used, and + /// that may be from a remote Runspace on a 64-bit machine. + /// + /// + /// The record being reported to the host. + /// + protected virtual void WriteProgressImpl(long sourceId, ProgressRecord record) + { + } + + internal void ClearProgress() + { + const string nonEmptyString = "noop"; + if (!this.SupportsWriteProgress) + { + return; + } + + foreach (ProgressKey key in this.currentProgressMessages.Keys) + { + // This constructor throws if the activity description is empty even + // with completed records. + var record = new ProgressRecord( + key.ActivityId, + activity: nonEmptyString, + statusDescription: nonEmptyString); + + record.RecordType = ProgressRecordType.Completed; + this.WriteProgressImpl(key.SourceId, record); + } + + this.currentProgressMessages.Clear(); + } + + #endregion + + #region IHostUISupportsMultipleChoiceSelection Implementation + + /// + /// + /// + /// + /// + /// + /// + /// + public Collection PromptForChoice( + string promptCaption, + string promptMessage, + Collection choiceDescriptions, + IEnumerable defaultChoices) + { + ChoiceDetails[] choices = + choiceDescriptions + .Select(ChoiceDetails.Create) + .ToArray(); + + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + Task promptTask = + this.CreateChoicePromptHandler() + .PromptForChoiceAsync( + 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 Coordinates lastPromptLocation; + + private async Task WritePromptStringToHostAsync(CancellationToken cancellationToken) + { + try + { + if (this.lastPromptLocation != null && + this.lastPromptLocation.X == await ConsoleProxy.GetCursorLeftAsync(cancellationToken) && + this.lastPromptLocation.Y == await ConsoleProxy.GetCursorTopAsync(cancellationToken)) + { + return; + } + } + // When output is redirected (like when running tests) attempting to get + // the cursor position will throw. + catch (System.IO.IOException) + { + } + + PSCommand promptCommand = new PSCommand().AddScript("prompt"); + + cancellationToken.ThrowIfCancellationRequested(); + string promptString = + (await this.powerShellContext.ExecuteCommandAsync(promptCommand, false, false)) + .Select(pso => pso.BaseObject) + .OfType() + .FirstOrDefault() ?? "PS> "; + + // Add the [DBG] prefix if we're stopped in the debugger and the prompt doesn't already have [DBG] in it + if (this.powerShellContext.IsDebuggerStopped && !promptString.Contains("[DBG]")) + { + 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); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Write the prompt string + this.WriteOutput(promptString, false); + this.lastPromptLocation = new Coordinates( + await ConsoleProxy.GetCursorLeftAsync(cancellationToken), + await ConsoleProxy.GetCursorTopAsync(cancellationToken)); + } + + private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs) + { + // 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); + } + } + + internal static ConsoleColor BackgroundColor { get; set; } + + internal ConsoleColor ErrorForegroundColor { get; set; } = ConsoleColor.Red; + internal ConsoleColor ErrorBackgroundColor { get; set; } = BackgroundColor; + + internal ConsoleColor WarningForegroundColor { get; set; } = ConsoleColor.Yellow; + internal ConsoleColor WarningBackgroundColor { get; set; } = BackgroundColor; + + internal ConsoleColor DebugForegroundColor { get; set; } = ConsoleColor.Yellow; + internal ConsoleColor DebugBackgroundColor { get; set; } = BackgroundColor; + + internal ConsoleColor VerboseForegroundColor { get; set; } = ConsoleColor.Yellow; + internal ConsoleColor VerboseBackgroundColor { get; set; } = BackgroundColor; + + internal ConsoleColor ProgressForegroundColor { get; set; } = ConsoleColor.Yellow; + internal ConsoleColor ProgressBackgroundColor { get; set; } = ConsoleColor.DarkCyan; + + private async Task StartReplLoopAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + string commandString = null; + int originalCursorTop = 0; + + try + { + await this.WritePromptStringToHostAsync(cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + + try + { + originalCursorTop = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + commandString = await this.ReadCommandLineAsync(cancellationToken); + } + catch (PipelineStoppedException) + { + this.WriteOutput( + "^C", + true, + OutputType.Normal, + foregroundColor: ConsoleColor.Red); + } + // Do nothing here, the while loop condition will exit. + catch (TaskCanceledException) + { } + catch (OperationCanceledException) + { } + 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.LogException("Caught exception while reading command line", e); + } + finally + { + if (!cancellationToken.IsCancellationRequested && + originalCursorTop == await ConsoleProxy.GetCursorTopAsync(cancellationToken)) + { + this.WriteLine(); + } + } + + if (!string.IsNullOrWhiteSpace(commandString)) + { + var unusedTask = + this.powerShellContext + .ExecuteScriptStringAsync( + commandString, + writeInputToHost: false, + writeOutputToHost: true, + addToHistory: true) + .ConfigureAwait(continueOnCapturedContext: false); + + break; + } + } + } + + private InputPromptHandler CreateInputPromptHandler() + { + if (this.activePromptHandler != null) + { + Logger.LogError( + "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.LogError( + "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, + CancellationTokenSource cancellationToken) + { + try + { + // This will synchronously block on the prompt task + // method which gets run on another thread. + promptTask.Wait(); + + if (promptTask.Status == TaskStatus.WaitingForActivation) + { + // The Wait() call has timed out, cancel the prompt + cancellationToken.Cancel(); + + this.WriteOutput("\r\nPrompt has been cancelled due to a timeout.\r\n"); + throw new PipelineStoppedException(); + } + } + catch (AggregateException e) + { + // Find the right InnerException + Exception innerException = e.InnerException; + while (innerException is AggregateException) + { + innerException = innerException.InnerException; + } + + // Was the task cancelled? + if (innerException is TaskCanceledException) + { + // Stop the pipeline if the prompt was cancelled + throw new PipelineStoppedException(); + } + else if (innerException is PipelineStoppedException) + { + // The prompt is being cancelled, rethrow the exception + throw innerException; + } + else + { + // Rethrow the exception + throw new Exception( + string.Format( + "{0} failed, check inner exception for details", + promptFunctionName), + innerException); + } + } + } + + private void PowerShellContext_DebuggerStop(object sender, System.Management.Automation.DebuggerStopEventArgs e) + { + if (!this.IsCommandLoopRunning) + { + StartCommandLoop(); + return; + } + + // 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 (eventArgs.ExecutionStatus == ExecutionStatus.Aborted) + { + this.ClearProgress(); + + // 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) + { + this.ClearProgress(); + + // Execution has completed, start the input prompt + this.ShowCommandPrompt(); + StartCommandLoop(); + } + else + { + // A new command was started, cancel the input prompt + StopCommandLoop(); + this.CancelCommandPrompt(); + } + } + else if ( + eventArgs.ExecutionOptions.WriteErrorsToHost && + (eventArgs.ExecutionStatus == ExecutionStatus.Failed || + eventArgs.HadErrors)) + { + this.ClearProgress(); + this.WriteOutput(string.Empty, true); + var unusedTask = this.WritePromptStringToHostAsync(CancellationToken.None); + } + } + + #endregion + + private readonly struct ProgressKey : IEquatable + { + internal readonly long SourceId; + + internal readonly int ActivityId; + + internal readonly int ParentActivityId; + + internal ProgressKey(long sourceId, ProgressRecord record) + { + SourceId = sourceId; + ActivityId = record.ActivityId; + ParentActivityId = record.ParentActivityId; + } + + public bool Equals(ProgressKey other) + { + return SourceId == other.SourceId + && ActivityId == other.ActivityId + && ParentActivityId == other.ParentActivityId; + } + + public override int GetHashCode() + { + // Algorithm from https://stackoverflow.com/questions/1646807/quick-and-simple-hash-code-combinations + unchecked + { + int hash = 17; + hash = hash * 31 + SourceId.GetHashCode(); + hash = hash * 31 + ActivityId.GetHashCode(); + hash = hash * 31 + ParentActivityId.GetHashCode(); + return hash; + } + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs new file mode 100644 index 000000000..28c79839d --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/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.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs new file mode 100644 index 000000000..4f36bc54f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs @@ -0,0 +1,175 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides a simplified interface for writing output to a + /// PowerShell host implementation. + /// + public interface IHostOutput + { + /// + /// 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. + /// + void WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor); + } + + /// + /// Provides helpful extension methods for the IHostOutput interface. + /// + public static class IHostOutputExtensions + { + /// + /// Writes normal output with a newline to the user interface. + /// + /// + /// The IHostOutput implementation to use for WriteOutput calls. + /// + /// + /// The output string to be written. + /// + public static void WriteOutput( + this IHostOutput hostOutput, + string outputString) + { + hostOutput.WriteOutput(outputString, true); + } + + /// + /// Writes normal output to the user interface. + /// + /// + /// The IHostOutput implementation to use for WriteOutput calls. + /// + /// + /// The output string to be written. + /// + /// + /// If true, a newline should be appended to the output's contents. + /// + public static void WriteOutput( + this IHostOutput hostOutput, + string outputString, + bool includeNewLine) + { + hostOutput.WriteOutput( + outputString, + includeNewLine, + OutputType.Normal); + } + + /// + /// Writes output of a particular type to the user interface + /// with a newline ending. + /// + /// + /// The IHostOutput implementation to use for WriteOutput calls. + /// + /// + /// The output string to be written. + /// + /// + /// Specifies the type of output to be written. + /// + public static void WriteOutput( + this IHostOutput hostOutput, + string outputString, + OutputType outputType) + { + hostOutput.WriteOutput( + outputString, + true, + OutputType.Normal); + } + + /// + /// Writes output of a particular type to the user interface. + /// + /// + /// The IHostOutput implementation to use for WriteOutput calls. + /// + /// + /// 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. + /// + public static void WriteOutput( + this IHostOutput hostOutput, + string outputString, + bool includeNewLine, + OutputType outputType) + { + hostOutput.WriteOutput( + outputString, + includeNewLine, + outputType, + ConsoleColor.Gray, + (ConsoleColor)(-1)); // -1 indicates the console's raw background color + } + + /// + /// Writes output of a particular type to the user interface using + /// a particular foreground color. + /// + /// + /// The IHostOutput implementation to use for WriteOutput calls. + /// + /// + /// 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. + /// + public static void WriteOutput( + this IHostOutput hostOutput, + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor) + { + hostOutput.WriteOutput( + outputString, + includeNewLine, + outputType, + foregroundColor, + (ConsoleColor)(-1)); // -1 indicates the console's raw background color + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs new file mode 100644 index 000000000..3514469e9 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs @@ -0,0 +1,225 @@ +// +// 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.Management.Automation.Host; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides an simple implementation of the PSHostRawUserInterface class. + /// + public class SimplePSHostRawUserInterface : PSHostRawUserInterface + { + #region Private Fields + + private const int DefaultConsoleHeight = 100; + private const int DefaultConsoleWidth = 120; + + private ILogger Logger; + + private Size currentBufferSize = new Size(DefaultConsoleWidth, DefaultConsoleHeight); + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the SimplePSHostRawUserInterface + /// class with the given IConsoleHost implementation. + /// + /// The ILogger implementation to use for this instance. + public SimplePSHostRawUserInterface(ILogger logger) + { + this.Logger = logger; + this.ForegroundColor = ConsoleColor.White; + this.BackgroundColor = ConsoleColor.Black; + } + + #endregion + + #region PSHostRawUserInterface Implementation + + /// + /// Gets or sets the background color of the console. + /// + public override ConsoleColor BackgroundColor + { + get; + set; + } + + /// + /// Gets or sets the foreground color of the console. + /// + public override ConsoleColor ForegroundColor + { + get; + set; + } + + /// + /// Gets or sets the size of the console buffer. + /// + public override Size BufferSize + { + get + { + return this.currentBufferSize; + } + set + { + this.currentBufferSize = value; + } + } + + /// + /// Gets or sets the cursor's position in the console buffer. + /// + public override Coordinates CursorPosition + { + get; + set; + } + + /// + /// Gets or sets the size of the cursor in the console buffer. + /// + public override int CursorSize + { + get; + set; + } + + /// + /// Gets or sets the position of the console's window. + /// + public override Coordinates WindowPosition + { + get; + set; + } + + /// + /// Gets or sets the size of the console's window. + /// + public override Size WindowSize + { + get; + set; + } + + /// + /// Gets or sets the console window's title. + /// + public override string WindowTitle + { + get; + set; + } + + /// + /// Gets a boolean that determines whether a keypress is available. + /// + public override bool KeyAvailable + { + get { return false; } + } + + /// + /// Gets the maximum physical size of the console window. + /// + public override Size MaxPhysicalWindowSize + { + get { return new Size(DefaultConsoleWidth, DefaultConsoleHeight); } + } + + /// + /// Gets the maximum size of the console window. + /// + public override Size MaxWindowSize + { + get { return new Size(DefaultConsoleWidth, DefaultConsoleHeight); } + } + + /// + /// Reads the current key pressed in the console. + /// + /// Options for reading the current keypress. + /// A KeyInfo struct with details about the current keypress. + public override KeyInfo ReadKey(ReadKeyOptions options) + { + Logger.LogWarning( + "PSHostRawUserInterface.ReadKey was called"); + + throw new System.NotImplementedException(); + } + + /// + /// Flushes the current input buffer. + /// + public override void FlushInputBuffer() + { + Logger.LogWarning( + "PSHostRawUserInterface.FlushInputBuffer was called"); + } + + /// + /// Gets the contents of the console buffer in a rectangular area. + /// + /// The rectangle inside which buffer contents will be accessed. + /// A BufferCell array with the requested buffer contents. + public override BufferCell[,] GetBufferContents(Rectangle rectangle) + { + return new BufferCell[0,0]; + } + + /// + /// Scrolls the contents of the console buffer. + /// + /// The source rectangle to scroll. + /// The destination coordinates by which to scroll. + /// The rectangle inside which the scrolling will be clipped. + /// The cell with which the buffer will be filled. + public override void ScrollBufferContents( + Rectangle source, + Coordinates destination, + Rectangle clip, + BufferCell fill) + { + Logger.LogWarning( + "PSHostRawUserInterface.ScrollBufferContents was called"); + } + + /// + /// Sets the contents of the buffer inside the specified rectangle. + /// + /// The rectangle inside which buffer contents will be filled. + /// The BufferCell which will be used to fill the requested space. + public override void SetBufferContents( + Rectangle rectangle, + BufferCell fill) + { + Logger.LogWarning( + "PSHostRawUserInterface.SetBufferContents was called"); + } + + /// + /// Sets the contents of the buffer at the given coordinate. + /// + /// The coordinate at which the buffer will be changed. + /// The new contents for the buffer at the given coordinate. + public override void SetBufferContents( + Coordinates origin, + BufferCell[,] contents) + { + Logger.LogWarning( + "PSHostRawUserInterface.SetBufferContents was called"); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs new file mode 100644 index 000000000..c9e05c7e3 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs @@ -0,0 +1,330 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Console; +using System; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides an implementation of the PSHostRawUserInterface class + /// for the ConsoleService and routes its calls to an IConsoleHost + /// implementation. + /// + internal class TerminalPSHostRawUserInterface : PSHostRawUserInterface + { + #region Private Fields + + private readonly PSHostRawUserInterface internalRawUI; + private ILogger Logger; + private KeyInfo? lastKeyDown; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the TerminalPSHostRawUserInterface + /// class with the given IConsoleHost implementation. + /// + /// The ILogger implementation to use for this instance. + /// The InternalHost instance from the origin runspace. + public TerminalPSHostRawUserInterface(ILogger logger, PSHost internalHost) + { + this.Logger = logger; + this.internalRawUI = internalHost.UI.RawUI; + } + + #endregion + + #region PSHostRawUserInterface Implementation + + /// + /// Gets or sets the background color of the console. + /// + public override ConsoleColor BackgroundColor + { + get { return System.Console.BackgroundColor; } + set { System.Console.BackgroundColor = value; } + } + + /// + /// Gets or sets the foreground color of the console. + /// + public override ConsoleColor ForegroundColor + { + get { return System.Console.ForegroundColor; } + set { System.Console.ForegroundColor = value; } + } + + /// + /// Gets or sets the size of the console buffer. + /// + public override Size BufferSize + { + get => this.internalRawUI.BufferSize; + set => this.internalRawUI.BufferSize = value; + } + + /// + /// Gets or sets the cursor's position in the console buffer. + /// + public override Coordinates CursorPosition + { + get + { + return new Coordinates( + ConsoleProxy.GetCursorLeft(), + ConsoleProxy.GetCursorTop()); + } + + set => this.internalRawUI.CursorPosition = value; + } + + /// + /// Gets or sets the size of the cursor in the console buffer. + /// + public override int CursorSize + { + get => this.internalRawUI.CursorSize; + set => this.internalRawUI.CursorSize = value; + } + + /// + /// Gets or sets the position of the console's window. + /// + public override Coordinates WindowPosition + { + get => this.internalRawUI.WindowPosition; + set => this.internalRawUI.WindowPosition = value; + } + + /// + /// Gets or sets the size of the console's window. + /// + public override Size WindowSize + { + get => this.internalRawUI.WindowSize; + set => this.internalRawUI.WindowSize = value; + } + + /// + /// Gets or sets the console window's title. + /// + public override string WindowTitle + { + get => this.internalRawUI.WindowTitle; + set => this.internalRawUI.WindowTitle = value; + } + + /// + /// Gets a boolean that determines whether a keypress is available. + /// + public override bool KeyAvailable => this.internalRawUI.KeyAvailable; + + /// + /// Gets the maximum physical size of the console window. + /// + public override Size MaxPhysicalWindowSize => this.internalRawUI.MaxPhysicalWindowSize; + + /// + /// Gets the maximum size of the console window. + /// + public override Size MaxWindowSize => this.internalRawUI.MaxWindowSize; + + /// + /// Reads the current key pressed in the console. + /// + /// Options for reading the current keypress. + /// A KeyInfo struct with details about the current keypress. + public override KeyInfo ReadKey(ReadKeyOptions options) + { + + bool includeUp = (options & ReadKeyOptions.IncludeKeyUp) != 0; + + // Key Up was requested and we have a cached key down we can return. + if (includeUp && this.lastKeyDown != null) + { + KeyInfo info = this.lastKeyDown.Value; + this.lastKeyDown = null; + return new KeyInfo( + info.VirtualKeyCode, + info.Character, + info.ControlKeyState, + keyDown: false); + } + + bool intercept = (options & ReadKeyOptions.NoEcho) != 0; + bool includeDown = (options & ReadKeyOptions.IncludeKeyDown) != 0; + if (!(includeDown || includeUp)) + { + throw new PSArgumentException( + "Cannot read key options. To read options, set one or both of the following: IncludeKeyDown, IncludeKeyUp.", + nameof(options)); + } + + // Allow ControlC as input so we can emulate pipeline stop requests. We can't actually + // determine if a stop is requested without using non-public API's. + bool oldValue = System.Console.TreatControlCAsInput; + try + { + System.Console.TreatControlCAsInput = true; + ConsoleKeyInfo key = ConsoleProxy.ReadKey(intercept, default(CancellationToken)); + + if (IsCtrlC(key)) + { + // Caller wants CtrlC as input so return it. + if ((options & ReadKeyOptions.AllowCtrlC) != 0) + { + return ProcessKey(key, includeDown); + } + + // Caller doesn't want CtrlC so throw a PipelineStoppedException to emulate + // a real stop. This will not show an exception to a script based caller and it + // will avoid having to return something like default(KeyInfo). + throw new PipelineStoppedException(); + } + + return ProcessKey(key, includeDown); + } + finally + { + System.Console.TreatControlCAsInput = oldValue; + } + } + + /// + /// Flushes the current input buffer. + /// + public override void FlushInputBuffer() + { + Logger.LogWarning( + "PSHostRawUserInterface.FlushInputBuffer was called"); + } + + /// + /// Gets the contents of the console buffer in a rectangular area. + /// + /// The rectangle inside which buffer contents will be accessed. + /// A BufferCell array with the requested buffer contents. + public override BufferCell[,] GetBufferContents(Rectangle rectangle) + { + return this.internalRawUI.GetBufferContents(rectangle); + } + + /// + /// Scrolls the contents of the console buffer. + /// + /// The source rectangle to scroll. + /// The destination coordinates by which to scroll. + /// The rectangle inside which the scrolling will be clipped. + /// The cell with which the buffer will be filled. + public override void ScrollBufferContents( + Rectangle source, + Coordinates destination, + Rectangle clip, + BufferCell fill) + { + this.internalRawUI.ScrollBufferContents(source, destination, clip, fill); + } + + /// + /// Sets the contents of the buffer inside the specified rectangle. + /// + /// The rectangle inside which buffer contents will be filled. + /// The BufferCell which will be used to fill the requested space. + public override void SetBufferContents( + Rectangle rectangle, + BufferCell fill) + { + // If the rectangle is all -1s then it means clear the visible buffer + if (rectangle.Top == -1 && + rectangle.Bottom == -1 && + rectangle.Left == -1 && + rectangle.Right == -1) + { + System.Console.Clear(); + return; + } + + this.internalRawUI.SetBufferContents(rectangle, fill); + } + + /// + /// Sets the contents of the buffer at the given coordinate. + /// + /// The coordinate at which the buffer will be changed. + /// The new contents for the buffer at the given coordinate. + public override void SetBufferContents( + Coordinates origin, + BufferCell[,] contents) + { + this.internalRawUI.SetBufferContents(origin, contents); + } + + #endregion + + /// + /// Determines if a key press represents the input Ctrl + C. + /// + /// The key to test. + /// + /// if the key represents the input Ctrl + C, + /// otherwise . + /// + private static bool IsCtrlC(ConsoleKeyInfo keyInfo) + { + // In the VSCode terminal Ctrl C is processed as virtual key code "3", which + // is not a named value in the ConsoleKey enum. + if ((int)keyInfo.Key == 3) + { + return true; + } + + return keyInfo.Key == ConsoleKey.C && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0; + } + + /// + /// Converts objects to objects and caches + /// key down events for the next key up request. + /// + /// The key to convert. + /// + /// A value indicating whether the result should be a key down event. + /// + /// The converted value. + private KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) + { + // Translate ConsoleModifiers to ControlKeyStates + ControlKeyStates states = default; + if ((key.Modifiers & ConsoleModifiers.Alt) != 0) + { + states |= ControlKeyStates.LeftAltPressed; + } + + if ((key.Modifiers & ConsoleModifiers.Control) != 0) + { + states |= ControlKeyStates.LeftCtrlPressed; + } + + if ((key.Modifiers & ConsoleModifiers.Shift) != 0) + { + states |= ControlKeyStates.ShiftPressed; + } + + var result = new KeyInfo((int)key.Key, key.KeyChar, states, isDown); + if (isDown) + { + this.lastKeyDown = result; + } + + return result; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs new file mode 100644 index 000000000..1ba2a7fc1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs @@ -0,0 +1,180 @@ +// +// 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.Management.Automation; + using System.Management.Automation.Host; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Logging; + + /// + /// Provides an EditorServicesPSHostUserInterface implementation + /// that integrates with the user's terminal UI. + /// + public class TerminalPSHostUserInterface : EditorServicesPSHostUserInterface + { + #region Private Fields + + private readonly PSHostUserInterface internalHostUI; + 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. + /// The InternalHost instance from the origin runspace. + public TerminalPSHostUserInterface( + PowerShellContextService powerShellContext, + ILogger logger, + PSHost internalHost) + : base( + powerShellContext, + new TerminalPSHostRawUserInterface(logger, internalHost), + logger) + { + this.internalHostUI = internalHost.UI; + 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 + + /// + /// Gets a value indicating whether writing progress is supported. + /// + internal protected override bool SupportsWriteProgress => true; + + /// + /// 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 ReadCommandLineAsync(CancellationToken cancellationToken) + { + return this.consoleReadLine.ReadCommandLineAsync(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 = ((int)backgroundColor != -1) ? backgroundColor : oldBackgroundColor; + + System.Console.Write(outputString + (includeNewLine ? Environment.NewLine : "")); + + System.Console.ForegroundColor = oldForegroundColor; + System.Console.BackgroundColor = oldBackgroundColor; + } + + /// + /// Invoked by to display a progress record. + /// + /// + /// Unique identifier of the source of the record. An int64 is used because typically, + /// the 'this' pointer of the command from whence the record is originating is used, and + /// that may be from a remote Runspace on a 64-bit machine. + /// + /// + /// The record being reported to the host. + /// + protected override void WriteProgressImpl(long sourceId, ProgressRecord record) + { + this.internalHostUI.WriteProgress(sourceId, record); + } + + /// + /// 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.Engine/Services/PowerShellContext/Session/IPromptContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs new file mode 100644 index 000000000..157715e7d --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides methods for interacting with implementations of ReadLine. + /// + public interface IPromptContext + { + /// + /// Read a string that has been input by the user. + /// + /// Indicates if ReadLine should act like a command REPL. + /// + /// The cancellation token can be used to cancel reading user input. + /// + /// + /// A task object that represents the completion of reading input. The Result property will + /// return the input string. + /// + Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken); + + /// + /// Performs any additional actions required to cancel the current ReadLine invocation. + /// + void AbortReadLine(); + + /// + /// Creates a task that completes when the current ReadLine invocation has been aborted. + /// + /// + /// A task object that represents the abortion of the current ReadLine invocation. + /// + Task AbortReadLineAsync(); + + /// + /// Blocks until the current ReadLine invocation has exited. + /// + void WaitForReadLineExit(); + + /// + /// Creates a task that completes when the current ReadLine invocation has exited. + /// + /// + /// A task object that represents the exit of the current ReadLine invocation. + /// + Task WaitForReadLineExitAsync(); + + /// + /// Adds the specified command to the history managed by the ReadLine implementation. + /// + /// The command to record. + void AddToHistory(string command); + + /// + /// Forces the prompt handler to trigger PowerShell event handling, reliquishing control + /// of the pipeline thread during event processing. + /// + void ForcePSEventHandling(); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs new file mode 100644 index 000000000..38d14fb96 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs @@ -0,0 +1,12 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal interface IRunspaceCapability + { + // NOTE: This interface is intentionally empty for now. + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs new file mode 100644 index 000000000..c210ff47b --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal interface IVersionSpecificOperations + { + void ConfigureDebugger(Runspace runspace); + + void PauseDebugger(Runspace runspace); + + IEnumerable ExecuteCommandInDebugger( + PowerShellContextService powerShellContext, + Runspace currentRunspace, + PSCommand psCommand, + bool sendOutputToHost, + out DebuggerResumeAction? debuggerResumeAction); + + void StopCommandInDebugger(PowerShellContextService powerShellContext); + + bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace); + + void ExitNestedPrompt(PSHost host); + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs new file mode 100644 index 000000000..deeec8939 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs @@ -0,0 +1,263 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Management.Automation.Runspaces; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System.Management.Automation; + + /// + /// Provides the ability to take over the current pipeline in a runspace. + /// + internal class InvocationEventQueue + { + private const string ShouldProcessInExecutionThreadPropertyName = "ShouldProcessInExecutionThread"; + + private static readonly PropertyInfo s_shouldProcessInExecutionThreadProperty = + typeof(PSEventSubscriber) + .GetProperty( + ShouldProcessInExecutionThreadPropertyName, + BindingFlags.Instance | BindingFlags.NonPublic); + + private readonly PromptNest _promptNest; + + private readonly Runspace _runspace; + + private readonly PowerShellContextService _powerShellContext; + + private InvocationRequest _invocationRequest; + + private SemaphoreSlim _lock = AsyncUtils.CreateSimpleLockingSemaphore(); + + private InvocationEventQueue(PowerShellContextService powerShellContext, PromptNest promptNest) + { + _promptNest = promptNest; + _powerShellContext = powerShellContext; + _runspace = powerShellContext.CurrentRunspace.Runspace; + } + + internal static InvocationEventQueue Create(PowerShellContextService powerShellContext, PromptNest promptNest) + { + var eventQueue = new InvocationEventQueue(powerShellContext, promptNest); + eventQueue.CreateInvocationSubscriber(); + return eventQueue; + } + + /// + /// Executes a command on the main pipeline thread through + /// eventing. A event subscriber will + /// be created that creates a nested PowerShell instance for + /// to utilize. + /// + /// + /// Avoid using this method directly if possible. + /// will route commands + /// through this method if required. + /// + /// The expected result type. + /// The to be executed. + /// + /// Error messages from PowerShell will be written to the . + /// + /// Specifies options to be used when executing this command. + /// + /// An awaitable which will provide results once the command + /// execution completes. + /// + internal async Task> ExecuteCommandOnIdleAsync( + PSCommand psCommand, + StringBuilder errorMessages, + ExecutionOptions executionOptions) + { + var request = new PipelineExecutionRequest( + _powerShellContext, + psCommand, + errorMessages, + executionOptions); + + await SetInvocationRequestAsync( + new InvocationRequest( + pwsh => request.ExecuteAsync().GetAwaiter().GetResult())); + + try + { + return await request.Results; + } + finally + { + await SetInvocationRequestAsync(request: null); + } + } + + /// + /// Marshals a to run on the pipeline thread. A new + /// will be created for the invocation. + /// + /// + /// The to invoke on the pipeline thread. The nested + /// instance for the created + /// will be passed as an argument. + /// + /// + /// An awaitable that the caller can use to know when execution completes. + /// + internal async Task InvokeOnPipelineThreadAsync(Action invocationAction) + { + var request = new InvocationRequest(pwsh => + { + using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: false)) + { + pwsh.Runspace = _runspace; + invocationAction(pwsh); + } + }); + + await SetInvocationRequestAsync(request); + try + { + await request.Task; + } + finally + { + await SetInvocationRequestAsync(null); + } + } + + private async Task WaitForExistingRequestAsync() + { + InvocationRequest existingRequest; + await _lock.WaitAsync(); + try + { + existingRequest = _invocationRequest; + if (existingRequest == null || existingRequest.Task.IsCompleted) + { + return; + } + } + finally + { + _lock.Release(); + } + + await existingRequest.Task; + } + + private async Task SetInvocationRequestAsync(InvocationRequest request) + { + await WaitForExistingRequestAsync(); + await _lock.WaitAsync(); + try + { + _invocationRequest = request; + } + finally + { + _lock.Release(); + } + + _powerShellContext.ForcePSEventHandling(); + } + + private void OnPowerShellIdle(object sender, EventArgs e) + { + if (!_lock.Wait(0)) + { + return; + } + + InvocationRequest currentRequest = null; + try + { + if (_invocationRequest == null) + { + return; + } + + currentRequest = _invocationRequest; + } + finally + { + _lock.Release(); + } + + _promptNest.PushPromptContext(); + try + { + currentRequest.Invoke(_promptNest.GetPowerShell()); + } + finally + { + _promptNest.PopPromptContext(); + } + } + + private PSEventSubscriber CreateInvocationSubscriber() + { + PSEventSubscriber subscriber = _runspace.Events.SubscribeEvent( + source: null, + eventName: PSEngineEvent.OnIdle, + sourceIdentifier: PSEngineEvent.OnIdle, + data: null, + handlerDelegate: OnPowerShellIdle, + supportEvent: true, + forwardEvent: false); + + SetSubscriberExecutionThreadWithReflection(subscriber); + + subscriber.Unsubscribed += OnInvokerUnsubscribed; + + return subscriber; + } + + private void OnInvokerUnsubscribed(object sender, PSEventUnsubscribedEventArgs e) + { + CreateInvocationSubscriber(); + } + + private void SetSubscriberExecutionThreadWithReflection(PSEventSubscriber subscriber) + { + // We need to create the PowerShell object in the same thread so we can get a nested + // PowerShell. This is the only way to consistently take control of the pipeline. The + // alternative is to make the subscriber a script block and have that create and process + // the PowerShell object, but that puts us in a different SessionState and is a lot slower. + s_shouldProcessInExecutionThreadProperty.SetValue(subscriber, true); + } + + private class InvocationRequest : TaskCompletionSource + { + private readonly Action _invocationAction; + + internal InvocationRequest(Action invocationAction) + { + _invocationAction = invocationAction; + } + + internal void Invoke(PowerShell pwsh) + { + try + { + _invocationAction(pwsh); + + // Ensure the result is set in another thread otherwise the caller + // may take over the pipeline thread. + System.Threading.Tasks.Task.Run(() => SetResult(true)); + } + catch (Exception e) + { + System.Threading.Tasks.Task.Run(() => SetException(e)); + } + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs new file mode 100644 index 000000000..281b410c2 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Console; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal class LegacyReadLineContext : IPromptContext + { + private readonly ConsoleReadLine _legacyReadLine; + + internal LegacyReadLineContext(PowerShellContextService powerShellContext) + { + _legacyReadLine = new ConsoleReadLine(powerShellContext); + } + + public Task AbortReadLineAsync() + { + return Task.FromResult(true); + } + + public async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await _legacyReadLine.InvokeLegacyReadLineAsync(isCommandLine, cancellationToken); + } + + public Task WaitForReadLineExitAsync() + { + return Task.FromResult(true); + } + + public void AddToHistory(string command) + { + // Do nothing, history is managed completely by the PowerShell engine in legacy ReadLine. + } + + public void AbortReadLine() + { + // Do nothing, no additional actions are needed to cancel ReadLine. + } + + public void WaitForReadLineExit() + { + // Do nothing, ReadLine cancellation is instant or not appliciable. + } + + public void ForcePSEventHandling() + { + // Do nothing, the pipeline thread is not occupied by legacy ReadLine. + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs new file mode 100644 index 000000000..ad67f6891 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Enumerates the types of output lines that will be sent + /// to an IConsoleHost implementation. + /// + public enum OutputType + { + /// + /// A normal output line, usually written with the or Write-Host or + /// Write-Output cmdlets. + /// + Normal, + + /// + /// A debug output line, written with the Write-Debug cmdlet. + /// + Debug, + + /// + /// A verbose output line, written with the Write-Verbose cmdlet. + /// + Verbose, + + /// + /// A warning output line, written with the Write-Warning cmdlet. + /// + Warning, + + /// + /// An error output line, written with the Write-Error cmdlet or + /// as a result of some error during PowerShell pipeline execution. + /// + Error + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs new file mode 100644 index 000000000..b1408a991 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides details about output that has been written to the + /// PowerShell host. + /// + public class OutputWrittenEventArgs + { + /// + /// Gets the text of the output. + /// + public string OutputText { get; private set; } + + /// + /// Gets the type of the output. + /// + public OutputType OutputType { get; private set; } + + /// + /// Gets a boolean which indicates whether a newline + /// should be written after the output. + /// + public bool IncludeNewLine { get; private set; } + + /// + /// Gets the foreground color of the output text. + /// + public ConsoleColor ForegroundColor { get; private set; } + + /// + /// Gets the background color of the output text. + /// + public ConsoleColor BackgroundColor { get; private set; } + + /// + /// Creates an instance of the OutputWrittenEventArgs class. + /// + /// The text of the output. + /// A boolean which indicates whether a newline should be written after the output. + /// The type of the output. + /// The foreground color of the output text. + /// The background color of the output text. + public OutputWrittenEventArgs( + string outputText, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor) + { + this.OutputText = outputText; + this.IncludeNewLine = includeNewLine; + this.OutputType = outputType; + this.ForegroundColor = foregroundColor; + this.BackgroundColor = backgroundColor; + } + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs new file mode 100644 index 000000000..9058579e8 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs @@ -0,0 +1,203 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System; +using System.Management.Automation.Runspaces; +using Microsoft.PowerShell.EditorServices.Console; + +namespace Microsoft.PowerShell.EditorServices.Session { + using System.Management.Automation; + using Microsoft.Extensions.Logging; + + internal class PSReadLinePromptContext : IPromptContext { + private const string ReadLineScript = @" + [System.Diagnostics.DebuggerHidden()] + [System.Diagnostics.DebuggerStepThrough()] + param() + return [Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null]::ReadLine( + $Host.Runspace, + $ExecutionContext, + $args[0])"; + + private const string ReadLineInitScript = @" + [System.Diagnostics.DebuggerHidden()] + [System.Diagnostics.DebuggerStepThrough()] + param() + end { + $module = Get-Module -ListAvailable PSReadLine | + Where-Object Version -eq '2.0.0' | + Where-Object { $_.PrivateData.PSData.Prerelease -notin 'beta1','beta2','beta3' } | + Sort-Object -Descending Version | + Select-Object -First 1 + if (-not $module) { + return + } + + Import-Module -ModuleInfo $module + return [Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null] + }"; + + private readonly PowerShellContextService _powerShellContext; + + private PromptNest _promptNest; + + private InvocationEventQueue _invocationEventQueue; + + private ConsoleReadLine _consoleReadLine; + + private CancellationTokenSource _readLineCancellationSource; + + private PSReadLineProxy _readLineProxy; + + internal PSReadLinePromptContext( + PowerShellContextService powerShellContext, + PromptNest promptNest, + InvocationEventQueue invocationEventQueue, + PSReadLineProxy readLineProxy) + { + _promptNest = promptNest; + _powerShellContext = powerShellContext; + _invocationEventQueue = invocationEventQueue; + _consoleReadLine = new ConsoleReadLine(powerShellContext); + _readLineProxy = readLineProxy; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + _readLineProxy.OverrideReadKey( + intercept => ConsoleProxy.UnixReadKey( + intercept, + _readLineCancellationSource.Token)); + } + + internal static bool TryGetPSReadLineProxy( + ILogger logger, + Runspace runspace, + out PSReadLineProxy readLineProxy) + { + readLineProxy = null; + using (var pwsh = PowerShell.Create()) + { + pwsh.Runspace = runspace; + var psReadLineType = pwsh + .AddScript(ReadLineInitScript) + .Invoke() + .FirstOrDefault(); + + if (psReadLineType == null) + { + return false; + } + + try + { + readLineProxy = new PSReadLineProxy(psReadLineType, logger); + } + catch (InvalidOperationException) + { + // The Type we got back from PowerShell doesn't have the members we expected. + // Could be an older version, a custom build, or something a newer version with + // breaking changes. + return false; + } + } + + return true; + } + + public async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + _readLineCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var localTokenSource = _readLineCancellationSource; + if (localTokenSource.Token.IsCancellationRequested) + { + throw new TaskCanceledException(); + } + + try + { + if (!isCommandLine) + { + return await _consoleReadLine.InvokeLegacyReadLineAsync( + false, + _readLineCancellationSource.Token); + } + + var result = (await _powerShellContext.ExecuteCommandAsync( + new PSCommand() + .AddScript(ReadLineScript) + .AddArgument(_readLineCancellationSource.Token), + null, + new ExecutionOptions() + { + WriteErrorsToHost = false, + WriteOutputToHost = false, + InterruptCommandPrompt = false, + AddToHistory = false, + IsReadLine = isCommandLine + })) + .FirstOrDefault(); + + return cancellationToken.IsCancellationRequested + ? string.Empty + : result; + } + finally + { + _readLineCancellationSource = null; + } + } + + public void AbortReadLine() + { + if (_readLineCancellationSource == null) + { + return; + } + + _readLineCancellationSource.Cancel(); + + WaitForReadLineExit(); + } + + public async Task AbortReadLineAsync() { + if (_readLineCancellationSource == null) + { + return; + } + + _readLineCancellationSource.Cancel(); + + await WaitForReadLineExitAsync(); + } + + public void WaitForReadLineExit() + { + using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: true)) + { } + } + + public async Task WaitForReadLineExitAsync() { + using (await _promptNest.GetRunspaceHandleAsync(CancellationToken.None, isReadLine: true)) + { } + } + + public void AddToHistory(string command) + { + _readLineProxy.AddToHistory(command); + } + + public void ForcePSEventHandling() + { + _readLineProxy.ForcePSEventHandling(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs new file mode 100644 index 000000000..494a3f9f7 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs @@ -0,0 +1,118 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Reflection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal class PSReadLineProxy + { + private const string FieldMemberType = "field"; + + private const string MethodMemberType = "method"; + + private const string AddToHistoryMethodName = "AddToHistory"; + + private const string SetKeyHandlerMethodName = "SetKeyHandler"; + + private const string ReadKeyOverrideFieldName = "_readKeyOverride"; + + private const string VirtualTerminalTypeName = "Microsoft.PowerShell.Internal.VirtualTerminal"; + + private const string ForcePSEventHandlingMethodName = "ForcePSEventHandling"; + + private static readonly Type[] s_setKeyHandlerTypes = + { + typeof(string[]), + typeof(Action), + typeof(string), + typeof(string) + }; + + private static readonly Type[] s_addToHistoryTypes = { typeof(string) }; + + private readonly FieldInfo _readKeyOverrideField; + + internal PSReadLineProxy(Type psConsoleReadLine, ILogger logger) + { + ForcePSEventHandling = + (Action)psConsoleReadLine.GetMethod( + ForcePSEventHandlingMethodName, + BindingFlags.Static | BindingFlags.NonPublic) + ?.CreateDelegate(typeof(Action)); + + AddToHistory = (Action)psConsoleReadLine.GetMethod( + AddToHistoryMethodName, + s_addToHistoryTypes) + ?.CreateDelegate(typeof(Action)); + + SetKeyHandler = + (Action, string, string>)psConsoleReadLine.GetMethod( + SetKeyHandlerMethodName, + s_setKeyHandlerTypes) + ?.CreateDelegate(typeof(Action, string, string>)); + + _readKeyOverrideField = psConsoleReadLine.GetTypeInfo().Assembly + .GetType(VirtualTerminalTypeName) + ?.GetField(ReadKeyOverrideFieldName, BindingFlags.Static | BindingFlags.NonPublic); + + if (_readKeyOverrideField == null) + { + throw NewInvalidPSReadLineVersionException( + FieldMemberType, + ReadKeyOverrideFieldName, + logger); + } + + if (SetKeyHandler == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + SetKeyHandlerMethodName, + logger); + } + + if (AddToHistory == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + AddToHistoryMethodName, + logger); + } + + if (ForcePSEventHandling == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + ForcePSEventHandlingMethodName, + logger); + } + } + + internal Action AddToHistory { get; } + + internal Action, object>, string, string> SetKeyHandler { get; } + + internal Action ForcePSEventHandling { get; } + + internal void OverrideReadKey(Func readKeyFunc) + { + _readKeyOverrideField.SetValue(null, readKeyFunc); + } + + private static InvalidOperationException NewInvalidPSReadLineVersionException( + string memberType, + string memberName, + ILogger logger) + { + logger.LogError( + $"The loaded version of PSReadLine is not supported. The {memberType} \"{memberName}\" was not found."); + + return new InvalidOperationException(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs new file mode 100644 index 000000000..f2d61192c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal interface IPipelineExecutionRequest + { + Task ExecuteAsync(); + + Task WaitTask { get; } + } + + /// + /// Contains details relating to a request to execute a + /// command on the PowerShell pipeline thread. + /// + /// The expected result type of the execution. + internal class PipelineExecutionRequest : IPipelineExecutionRequest + { + private PowerShellContextService _powerShellContext; + private PSCommand _psCommand; + private StringBuilder _errorMessages; + private ExecutionOptions _executionOptions; + private TaskCompletionSource> _resultsTask; + + public Task> Results + { + get { return this._resultsTask.Task; } + } + + public Task WaitTask { get { return Results; } } + + public PipelineExecutionRequest( + PowerShellContextService powerShellContext, + PSCommand psCommand, + StringBuilder errorMessages, + bool sendOutputToHost) + : this( + powerShellContext, + psCommand, + errorMessages, + new ExecutionOptions() + { + WriteOutputToHost = sendOutputToHost + }) + { } + + + public PipelineExecutionRequest( + PowerShellContextService powerShellContext, + PSCommand psCommand, + StringBuilder errorMessages, + ExecutionOptions executionOptions) + { + _powerShellContext = powerShellContext; + _psCommand = psCommand; + _errorMessages = errorMessages; + _executionOptions = executionOptions; + _resultsTask = new TaskCompletionSource>(); + } + + public async Task ExecuteAsync() + { + var results = + await _powerShellContext.ExecuteCommandAsync( + _psCommand, + _errorMessages, + _executionOptions); + + var unusedTask = Task.Run(() => _resultsTask.SetResult(results)); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs new file mode 100644 index 000000000..c1024887a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs @@ -0,0 +1,107 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal class PowerShell5Operations : IVersionSpecificOperations + { + public void ConfigureDebugger(Runspace runspace) + { + if (runspace.Debugger != null) + { + runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); + } + } + + public virtual void PauseDebugger(Runspace runspace) + { + if (runspace.Debugger != null) + { + runspace.Debugger.SetDebuggerStepMode(true); + } + } + + public virtual bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace) + { + return runspace.Debugger.InBreakpoint || (promptNest.IsRemote && promptNest.IsInDebugger); + } + + public IEnumerable ExecuteCommandInDebugger( + PowerShellContextService powerShellContext, + Runspace currentRunspace, + PSCommand psCommand, + bool sendOutputToHost, + out DebuggerResumeAction? debuggerResumeAction) + { + debuggerResumeAction = null; + PSDataCollection outputCollection = new PSDataCollection(); + + if (sendOutputToHost) + { + outputCollection.DataAdded += + (obj, e) => + { + for (int i = e.Index; i < outputCollection.Count; i++) + { + powerShellContext.WriteOutput( + outputCollection[i].ToString(), + true); + } + }; + } + + DebuggerCommandResults commandResults = + currentRunspace.Debugger.ProcessCommand( + psCommand, + outputCollection); + + // Pass along the debugger's resume action if the user's + // command caused one to be returned + debuggerResumeAction = commandResults.ResumeAction; + + IEnumerable results = null; + if (typeof(TResult) != typeof(PSObject)) + { + results = + outputCollection + .Select(pso => pso.BaseObject) + .Cast(); + } + else + { + results = outputCollection.Cast(); + } + + return results; + } + + public void StopCommandInDebugger(PowerShellContextService powerShellContext) + { + // If the RunspaceAvailability is None, the runspace is dead and we should not try to run anything in it. + if (powerShellContext.CurrentRunspace.Runspace.RunspaceAvailability != RunspaceAvailability.None) + { + powerShellContext.CurrentRunspace.Runspace.Debugger.StopProcessCommand(); + } + } + + public void ExitNestedPrompt(PSHost host) + { + try + { + host.ExitNestedPrompt(); + } + catch (FlowControlException) + { + } + } + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs new file mode 100644 index 000000000..2075c04d5 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Enumerates the possible states for a PowerShellContext. + /// + public enum PowerShellContextState + { + /// + /// Indicates an unknown, potentially uninitialized state. + /// + Unknown = 0, + + /// + /// Indicates the state where the session is starting but + /// not yet fully initialized. + /// + NotStarted, + + /// + /// Indicates that the session is ready to accept commands + /// for execution. + /// + Ready, + + /// + /// Indicates that the session is currently running a command. + /// + Running, + + /// + /// Indicates that the session is aborting the current execution. + /// + Aborting, + + /// + /// Indicates that the session is already disposed and cannot + /// accept further execution requests. + /// + Disposed + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs new file mode 100644 index 000000000..ee15ae97a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Enumerates the possible execution results that can occur after + /// executing a command or script. + /// + public enum PowerShellExecutionResult + { + /// + /// Indicates that execution is not yet finished. + /// + NotFinished, + + /// + /// Indicates that execution has failed. + /// + Failed, + + /// + /// Indicates that execution was aborted by the user. + /// + Aborted, + + /// + /// Indicates that execution was stopped by the debugger. + /// + Stopped, + + /// + /// Indicates that execution completed successfully. + /// + Completed + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs new file mode 100644 index 000000000..d0b8e56e2 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs @@ -0,0 +1,166 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Extensions.Logging; +using System; +using System.Collections; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Defines the possible enumeration values for the PowerShell process architecture. + /// + public enum PowerShellProcessArchitecture + { + /// + /// The processor architecture is unknown or wasn't accessible. + /// + Unknown, + + /// + /// The processor architecture is 32-bit. + /// + X86, + + /// + /// The processor architecture is 64-bit. + /// + X64 + } + + /// + /// Provides details about the version of the PowerShell runtime. + /// + public class PowerShellVersionDetails + { + #region Properties + + /// + /// Gets the version of the PowerShell runtime. + /// + public Version Version { get; private set; } + + /// + /// Gets the full version string, either the ToString of the Version + /// property or the GitCommitId for open-source PowerShell releases. + /// + public string VersionString { get; private set; } + + /// + /// Gets the PowerShell edition (generally Desktop or Core). + /// + public string Edition { get; private set; } + + /// + /// Gets the architecture of the PowerShell process. + /// + public PowerShellProcessArchitecture Architecture { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the PowerShellVersionDetails class. + /// + /// The version of the PowerShell runtime. + /// A string representation of the PowerShell version. + /// The string representation of the PowerShell edition. + /// The processor architecture. + public PowerShellVersionDetails( + Version version, + string versionString, + string editionString, + PowerShellProcessArchitecture architecture) + { + this.Version = version; + this.VersionString = versionString; + this.Edition = editionString; + this.Architecture = architecture; + } + + #endregion + + #region Public Methods + + /// + /// Gets the PowerShell version details for the given runspace. + /// + /// The runspace for which version details will be gathered. + /// An ILogger implementation used for writing log messages. + /// A new PowerShellVersionDetails instance. + public static PowerShellVersionDetails GetVersionDetails(Runspace runspace, ILogger logger) + { + Version powerShellVersion = new Version(5, 0); + string versionString = null; + string powerShellEdition = "Desktop"; + var architecture = PowerShellProcessArchitecture.Unknown; + + try + { + var psVersionTable = PowerShellContextService.ExecuteScriptAndGetItem("$PSVersionTable", runspace); + if (psVersionTable != null) + { + var edition = psVersionTable["PSEdition"] as string; + if (edition != null) + { + powerShellEdition = edition; + } + + // The PSVersion value will either be of Version or SemanticVersion. + // In the former case, take the value directly. In the latter case, + // generate a Version from its string representation. + var version = psVersionTable["PSVersion"]; + if (version is Version) + { + powerShellVersion = (Version)version; + } + else if (version != null) + { + // Expected version string format is 6.0.0-alpha so build a simpler version from that + powerShellVersion = new Version(version.ToString().Split('-')[0]); + } + + var gitCommitId = psVersionTable["GitCommitId"] as string; + if (gitCommitId != null) + { + versionString = gitCommitId; + } + else + { + versionString = powerShellVersion.ToString(); + } + + var arch = PowerShellContextService.ExecuteScriptAndGetItem("$env:PROCESSOR_ARCHITECTURE", runspace); + if (arch != null) + { + if (string.Equals(arch, "AMD64", StringComparison.CurrentCultureIgnoreCase)) + { + architecture = PowerShellProcessArchitecture.X64; + } + else if (string.Equals(arch, "x86", StringComparison.CurrentCultureIgnoreCase)) + { + architecture = PowerShellProcessArchitecture.X86; + } + } + } + } + catch (Exception ex) + { + logger.LogWarning( + "Failed to look up PowerShell version, defaulting to version 5.\r\n\r\n" + ex.ToString()); + } + + return new PowerShellVersionDetails( + powerShellVersion, + versionString, + powerShellEdition, + architecture); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs new file mode 100644 index 000000000..b88d7c1ae --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides details about the progress of a particular activity. + /// + public class ProgressDetails + { + /// + /// Gets the percentage of the activity that has been completed. + /// + public int PercentComplete { get; private set; } + + internal static ProgressDetails Create(ProgressRecord progressRecord) + { + //progressRecord.RecordType == ProgressRecordType.Completed; + //progressRecord.Activity; + //progressRecord. + + return new ProgressDetails + { + PercentComplete = progressRecord.PercentComplete + }; + } + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs new file mode 100644 index 000000000..a39f25f34 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs @@ -0,0 +1,564 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System; + using System.Management.Automation; + + /// + /// Represents the stack of contexts in which PowerShell commands can be invoked. + /// + internal class PromptNest : IDisposable + { + private ConcurrentStack _frameStack; + + private PromptNestFrame _readLineFrame; + + private IHostInput _consoleReader; + + private PowerShellContextService _powerShellContext; + + private IVersionSpecificOperations _versionSpecificOperations; + + private bool _isDisposed; + + private object _syncObject = new object(); + + private object _disposeSyncObject = new object(); + + /// + /// Initializes a new instance of the class. + /// + /// + /// The to track prompt status for. + /// + /// + /// The instance for the first frame. + /// + /// + /// The input handler. + /// + /// + /// The for the calling + /// instance. + /// + /// + /// This constructor should only be called when + /// is set to the initial runspace. + /// + internal PromptNest( + PowerShellContextService powerShellContext, + PowerShell initialPowerShell, + IHostInput consoleReader, + IVersionSpecificOperations versionSpecificOperations) + { + _versionSpecificOperations = versionSpecificOperations; + _consoleReader = consoleReader; + _powerShellContext = powerShellContext; + _frameStack = new ConcurrentStack(); + _frameStack.Push( + new PromptNestFrame( + initialPowerShell, + NewHandleQueue())); + + var readLineShell = PowerShell.Create(); + readLineShell.Runspace = powerShellContext.CurrentRunspace.Runspace; + _readLineFrame = new PromptNestFrame( + readLineShell, + new AsyncQueue()); + + ReleaseRunspaceHandleImpl(isReadLine: true); + } + + /// + /// Gets a value indicating whether the current frame was created by a debugger stop event. + /// + internal bool IsInDebugger => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Debug); + + /// + /// Gets a value indicating whether the current frame was created for an out of process runspace. + /// + internal bool IsRemote => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Remote); + + /// + /// Gets a value indicating whether the current frame was created by PSHost.EnterNestedPrompt(). + /// + internal bool IsNestedPrompt => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt); + + /// + /// Gets a value indicating the current number of frames managed by this PromptNest. + /// + internal int NestedPromptLevel => _frameStack.Count; + + private PromptNestFrame CurrentFrame + { + get + { + _frameStack.TryPeek(out PromptNestFrame currentFrame); + return _isDisposed ? _readLineFrame : currentFrame; + } + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + lock (_disposeSyncObject) + { + if (_isDisposed || !disposing) + { + return; + } + + while (NestedPromptLevel > 1) + { + _consoleReader?.StopCommandLoop(); + var currentFrame = CurrentFrame; + if (currentFrame.FrameType.HasFlag(PromptNestFrameType.Debug)) + { + _versionSpecificOperations.StopCommandInDebugger(_powerShellContext); + currentFrame.ThreadController.StartThreadExit(DebuggerResumeAction.Stop); + currentFrame.WaitForFrameExit(CancellationToken.None); + continue; + } + + if (currentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt)) + { + _powerShellContext.ExitAllNestedPrompts(); + continue; + } + + currentFrame.PowerShell.BeginStop(null, null); + currentFrame.WaitForFrameExit(CancellationToken.None); + } + + _consoleReader?.StopCommandLoop(); + _readLineFrame.Dispose(); + CurrentFrame.Dispose(); + _frameStack.Clear(); + _powerShellContext = null; + _consoleReader = null; + _isDisposed = true; + } + } + + /// + /// Gets the for the current frame. + /// + /// + /// The for the current frame, or + /// if the current frame does not have one. + /// + internal ThreadController GetThreadController() + { + if (_isDisposed) + { + return null; + } + + return CurrentFrame.IsThreadController ? CurrentFrame.ThreadController : null; + } + + /// + /// Create a new and set it as the current frame. + /// + internal void PushPromptContext() + { + if (_isDisposed) + { + return; + } + + PushPromptContext(PromptNestFrameType.Normal); + } + + /// + /// Create a new and set it as the current frame. + /// + /// The frame type. + internal void PushPromptContext(PromptNestFrameType frameType) + { + if (_isDisposed) + { + return; + } + + _frameStack.Push( + new PromptNestFrame( + frameType.HasFlag(PromptNestFrameType.Remote) + ? PowerShell.Create() + : PowerShell.Create(RunspaceMode.CurrentRunspace), + NewHandleQueue(), + frameType)); + } + + /// + /// Dispose of the current and revert to the previous frame. + /// + internal void PopPromptContext() + { + PromptNestFrame currentFrame; + lock (_syncObject) + { + if (_isDisposed || _frameStack.Count == 1) + { + return; + } + + _frameStack.TryPop(out currentFrame); + } + + currentFrame.Dispose(); + } + + /// + /// Get the instance for the current + /// . + /// + /// Indicates whether this is for a PSReadLine command. + /// The instance for the current frame. + internal PowerShell GetPowerShell(bool isReadLine = false) + { + if (_isDisposed) + { + return null; + } + + // Typically we want to run PSReadLine on the current nest frame. + // The exception is when the current frame is remote, in which + // case we need to run it in it's own frame because we can't take + // over a remote pipeline through event invocation. + if (NestedPromptLevel > 1 && !IsRemote) + { + return CurrentFrame.PowerShell; + } + + return isReadLine ? _readLineFrame.PowerShell : CurrentFrame.PowerShell; + } + + /// + /// Get the for the current . + /// + /// + /// The that can be used to cancel the request. + /// + /// Indicates whether this is for a PSReadLine command. + /// The for the current frame. + internal RunspaceHandle GetRunspaceHandle(CancellationToken cancellationToken, bool isReadLine) + { + if (_isDisposed) + { + return null; + } + + // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace + // is in process. + if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + GetRunspaceHandleImpl(cancellationToken, isReadLine: false); + } + + return GetRunspaceHandleImpl(cancellationToken, isReadLine); + } + + + /// + /// Get the for the current . + /// + /// + /// The that will be checked prior to + /// completing the returned task. + /// + /// Indicates whether this is for a PSReadLine command. + /// + /// A object representing the asynchronous operation. + /// The property will return the + /// for the current frame. + /// + internal async Task GetRunspaceHandleAsync(CancellationToken cancellationToken, bool isReadLine) + { + if (_isDisposed) + { + return null; + } + + // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace + // is in process. + if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + await GetRunspaceHandleImplAsync(cancellationToken, isReadLine: false); + } + + return await GetRunspaceHandleImplAsync(cancellationToken, isReadLine); + } + + /// + /// Releases control of the runspace aquired via the . + /// + /// + /// The representing the control to release. + /// + internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) + { + if (_isDisposed) + { + return; + } + + ReleaseRunspaceHandleImpl(runspaceHandle.IsReadLine); + if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + ReleaseRunspaceHandleImpl(isReadLine: false); + } + } + + /// + /// Releases control of the runspace aquired via the . + /// + /// + /// The representing the control to release. + /// + /// + /// A object representing the release of the + /// . + /// + internal async Task ReleaseRunspaceHandleAsync(RunspaceHandle runspaceHandle) + { + if (_isDisposed) + { + return; + } + + await ReleaseRunspaceHandleImplAsync(runspaceHandle.IsReadLine); + if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + await ReleaseRunspaceHandleImplAsync(isReadLine: false); + } + } + + /// + /// Determines if the current frame is unavailable for commands. + /// + /// + /// A value indicating whether the current frame is unavailable for commands. + /// + internal bool IsMainThreadBusy() + { + return !_isDisposed && CurrentFrame.Queue.IsEmpty; + } + + /// + /// Determines if a PSReadLine command is currently running. + /// + /// + /// A value indicating whether a PSReadLine command is currently running. + /// + internal bool IsReadLineBusy() + { + return !_isDisposed && _readLineFrame.Queue.IsEmpty; + } + + /// + /// Blocks until the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + internal void WaitForCurrentFrameExit(Action initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + initiator.Invoke(currentFrame); + } + finally + { + currentFrame.WaitForFrameExit(CancellationToken.None); + } + } + + /// + /// Blocks until the current frame has been disposed. + /// + internal void WaitForCurrentFrameExit() + { + if (_isDisposed) + { + return; + } + + CurrentFrame.WaitForFrameExit(CancellationToken.None); + } + + /// + /// Blocks until the current frame has been disposed. + /// + /// + /// The used the exit the block prior to + /// the current frame being disposed. + /// + internal void WaitForCurrentFrameExit(CancellationToken cancellationToken) + { + if (_isDisposed) + { + return; + } + + CurrentFrame.WaitForFrameExit(cancellationToken); + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(Func initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + await initiator.Invoke(currentFrame); + } + finally + { + await currentFrame.WaitForFrameExitAsync(CancellationToken.None); + } + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(Action initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + initiator.Invoke(currentFrame); + } + finally + { + await currentFrame.WaitForFrameExitAsync(CancellationToken.None); + } + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync() + { + if (_isDisposed) + { + return; + } + + await WaitForCurrentFrameExitAsync(CancellationToken.None); + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// The used the exit the block prior to the current frame being disposed. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(CancellationToken cancellationToken) + { + if (_isDisposed) + { + return; + } + + await CurrentFrame.WaitForFrameExitAsync(cancellationToken); + } + + private AsyncQueue NewHandleQueue() + { + var queue = new AsyncQueue(); + queue.Enqueue(new RunspaceHandle(_powerShellContext)); + return queue; + } + + private RunspaceHandle GetRunspaceHandleImpl(CancellationToken cancellationToken, bool isReadLine) + { + if (isReadLine) + { + return _readLineFrame.Queue.Dequeue(cancellationToken); + } + + return CurrentFrame.Queue.Dequeue(cancellationToken); + } + + private async Task GetRunspaceHandleImplAsync(CancellationToken cancellationToken, bool isReadLine) + { + if (isReadLine) + { + return await _readLineFrame.Queue.DequeueAsync(cancellationToken); + } + + return await CurrentFrame.Queue.DequeueAsync(cancellationToken); + } + + private void ReleaseRunspaceHandleImpl(bool isReadLine) + { + if (isReadLine) + { + _readLineFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, true)); + return; + } + + CurrentFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, false)); + } + + private async Task ReleaseRunspaceHandleImplAsync(bool isReadLine) + { + if (isReadLine) + { + await _readLineFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, true)); + return; + } + + await CurrentFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, false)); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs new file mode 100644 index 000000000..cae7dfb8a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs @@ -0,0 +1,137 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System.Management.Automation; + + /// + /// Represents a single frame in the . + /// + internal class PromptNestFrame : IDisposable + { + private const PSInvocationState IndisposableStates = PSInvocationState.Stopping | PSInvocationState.Running; + + private SemaphoreSlim _frameExited = new SemaphoreSlim(initialCount: 0); + + private bool _isDisposed = false; + + /// + /// Gets the instance. + /// + internal PowerShell PowerShell { get; } + + /// + /// Gets the queue that controls command invocation order. + /// + internal AsyncQueue Queue { get; } + + /// + /// Gets the frame type. + /// + internal PromptNestFrameType FrameType { get; } + + /// + /// Gets the . + /// + internal ThreadController ThreadController { get; } + + /// + /// Gets a value indicating whether the frame requires command invocations + /// to be routed to a specific thread. + /// + internal bool IsThreadController { get; } + + internal PromptNestFrame(PowerShell powerShell, AsyncQueue handleQueue) + : this(powerShell, handleQueue, PromptNestFrameType.Normal) + { } + + internal PromptNestFrame( + PowerShell powerShell, + AsyncQueue handleQueue, + PromptNestFrameType frameType) + { + PowerShell = powerShell; + Queue = handleQueue; + FrameType = frameType; + IsThreadController = (frameType & (PromptNestFrameType.Debug | PromptNestFrameType.NestedPrompt)) != 0; + if (!IsThreadController) + { + return; + } + + ThreadController = new ThreadController(this); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + if (disposing) + { + if (IndisposableStates.HasFlag(PowerShell.InvocationStateInfo.State)) + { + PowerShell.BeginStop( + asyncResult => + { + PowerShell.Runspace = null; + PowerShell.Dispose(); + }, + state: null); + } + else + { + PowerShell.Runspace = null; + PowerShell.Dispose(); + } + + _frameExited.Release(); + } + + _isDisposed = true; + } + + /// + /// Blocks until the frame has been disposed. + /// + /// + /// The that will exit the block when cancelled. + /// + internal void WaitForFrameExit(CancellationToken cancellationToken) + { + _frameExited.Wait(cancellationToken); + _frameExited.Release(); + } + + /// + /// Creates a task object that is completed when the frame has been disposed. + /// + /// + /// The that will be checked prior to completing + /// the returned task. + /// + /// + /// A object that represents this frame being disposed. + /// + internal async Task WaitForFrameExitAsync(CancellationToken cancellationToken) + { + await _frameExited.WaitAsync(cancellationToken); + _frameExited.Release(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs new file mode 100644 index 000000000..b42b42098 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + [Flags] + internal enum PromptNestFrameType + { + Normal = 0, + + NestedPrompt = 1, + + Debug = 2, + + Remote = 4 + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs new file mode 100644 index 000000000..0b24dfa48 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs @@ -0,0 +1,785 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Extensions; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Manages files that are accessed from a remote PowerShell session. + /// Also manages the registration and handling of the 'psedit' function. + /// + public class RemoteFileManager + { + #region Fields + + private ILogger logger; + private string remoteFilesPath; + private string processTempPath; + private PowerShellContextService powerShellContext; + private IEditorOperations editorOperations; + + private Dictionary filesPerComputer = + new Dictionary(); + + private const string RemoteSessionOpenFile = "PSESRemoteSessionOpenFile"; + + private const string PSEditModule = @"<# + .SYNOPSIS + Opens the specified files in your editor window + .DESCRIPTION + Opens the specified files in your editor window + .EXAMPLE + PS > Open-EditorFile './foo.ps1' + Opens foo.ps1 in your editor + .EXAMPLE + PS > gci ./myDir | Open-EditorFile + Opens everything in 'myDir' in your editor + .INPUTS + Path + an array of files you want to open in your editor + #> + function Open-EditorFile { + param ( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [String[]] + $Path + ) + + begin { + $Paths = @() + } + + process { + $Paths += $Path + } + + end { + if ($Paths.Count -gt 1) { + $preview = $false + } else { + $preview = $true + } + + foreach ($fileName in $Paths) + { + Microsoft.PowerShell.Management\Get-ChildItem $fileName | Where-Object { ! $_.PSIsContainer } | Foreach-Object { + $filePathName = $_.FullName + + # Get file contents + $params = @{ Path=$filePathName; Raw=$true } + if ($PSVersionTable.PSEdition -eq 'Core') + { + $params['AsByteStream']=$true + } + else + { + $params['Encoding']='Byte' + } + + $contentBytes = Microsoft.PowerShell.Management\Get-Content @params + + # Notify client for file open. + Microsoft.PowerShell.Utility\New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($filePathName, $contentBytes, $preview) > $null + } + } + } + } + + <# + .SYNOPSIS + Creates new files and opens them in your editor window + .DESCRIPTION + Creates new files and opens them in your editor window + .EXAMPLE + PS > New-EditorFile './foo.ps1' + Creates and opens a new foo.ps1 in your editor + .EXAMPLE + PS > Get-Process | New-EditorFile proc.txt + Creates and opens a new foo.ps1 in your editor with the contents of the call to Get-Process + .EXAMPLE + PS > Get-Process | New-EditorFile proc.txt -Force + Creates and opens a new foo.ps1 in your editor with the contents of the call to Get-Process. Overwrites the file if it already exists + .INPUTS + Path + an array of files you want to open in your editor + Value + The content you want in the new files + Force + Overwrites a file if it exists + #> + function New-EditorFile { + [CmdletBinding()] + param ( + [Parameter()] + [String[]] + [ValidateNotNullOrEmpty()] + $Path, + + [Parameter(ValueFromPipeline=$true)] + $Value, + + [Parameter()] + [switch] + $Force + ) + + begin { + $valueList = @() + } + + process { + $valueList += $Value + } + + end { + if ($Path) { + foreach ($fileName in $Path) + { + if (-not (Microsoft.PowerShell.Management\Test-Path $fileName) -or $Force) { + $valueList > $fileName + + # Get file contents + $params = @{ Path=$fileName; Raw=$true } + if ($PSVersionTable.PSEdition -eq 'Core') + { + $params['AsByteStream']=$true + } + else + { + $params['Encoding']='Byte' + } + + $contentBytes = Microsoft.PowerShell.Management\Get-Content @params + + if ($Path.Count -gt 1) { + $preview = $false + } else { + $preview = $true + } + + # Notify client for file open. + Microsoft.PowerShell.Utility\New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($fileName, $contentBytes, $preview) > $null + } else { + $PSCmdlet.WriteError( ( + Microsoft.PowerShell.Utility\New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList @( + [System.Exception]'File already exists.' + $Null + [System.Management.Automation.ErrorCategory]::ResourceExists + $fileName ) ) ) + } + } + } else { + $bytes = [System.Text.Encoding]::UTF8.GetBytes(($valueList | Microsoft.PowerShell.Utility\Out-String)) + Microsoft.PowerShell.Utility\New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($null, $bytes) > $null + } + } + } + + Microsoft.PowerShell.Utility\Set-Alias psedit Open-EditorFile -Scope Global + Microsoft.PowerShell.Core\Export-ModuleMember -Function Open-EditorFile, New-EditorFile + "; + + // This script is templated so that the '-Forward' parameter can be added + // to the script when in non-local sessions + private const string CreatePSEditFunctionScript = @" + param ( + [string] $PSEditModule + ) + + Microsoft.PowerShell.Utility\Register-EngineEvent -SourceIdentifier PSESRemoteSessionOpenFile -Forward -SupportEvent + Microsoft.PowerShell.Core\New-Module -ScriptBlock ([Scriptblock]::Create($PSEditModule)) -Name PSEdit | + Microsoft.PowerShell.Core\Import-Module -Global + "; + + private const string RemovePSEditFunctionScript = @" + Microsoft.PowerShell.Core\Get-Module PSEdit | Microsoft.PowerShell.Core\Remove-Module + + Microsoft.PowerShell.Utility\Unregister-Event -SourceIdentifier PSESRemoteSessionOpenFile -Force -ErrorAction Ignore + "; + + private const string SetRemoteContentsScript = @" + param( + [string] $RemoteFilePath, + [byte[]] $Content + ) + + # Set file contents + $params = @{ Path=$RemoteFilePath; Value=$Content; Force=$true } + if ($PSVersionTable.PSEdition -eq 'Core') + { + $params['AsByteStream']=$true + } + else + { + $params['Encoding']='Byte' + } + + Microsoft.PowerShell.Management\Set-Content @params 2>&1 + "; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the RemoteFileManager class. + /// + /// + /// The PowerShellContext to use for file loading operations. + /// + /// + /// The IEditorOperations instance to use for opening/closing files in the editor. + /// + /// An ILogger implementation used for writing log messages. + public RemoteFileManager( + PowerShellContextService powerShellContext, + IEditorOperations editorOperations, + ILogger logger) + { + Validate.IsNotNull(nameof(powerShellContext), powerShellContext); + + this.logger = logger; + this.powerShellContext = powerShellContext; + this.powerShellContext.RunspaceChanged += HandleRunspaceChangedAsync; + + this.editorOperations = editorOperations; + + this.processTempPath = + Path.Combine( + Path.GetTempPath(), + "PSES-" + Process.GetCurrentProcess().Id); + + this.remoteFilesPath = Path.Combine(this.processTempPath, "RemoteFiles"); + + // Delete existing temporary file cache path if it already exists + this.TryDeleteTemporaryPath(); + + // Register the psedit function in the current runspace + this.RegisterPSEditFunction(this.powerShellContext.CurrentRunspace); + } + + #endregion + + #region Public Methods + + /// + /// Opens a remote file, fetching its contents if necessary. + /// + /// + /// The remote file path to be opened. + /// + /// + /// The runspace from which where the remote file will be fetched. + /// + /// + /// The local file path where the remote file's contents have been stored. + /// + public async Task FetchRemoteFileAsync( + string remoteFilePath, + RunspaceDetails runspaceDetails) + { + string localFilePath = null; + + if (!string.IsNullOrEmpty(remoteFilePath)) + { + try + { + RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); + localFilePath = this.GetMappedPath(remoteFilePath, runspaceDetails); + + if (!pathMappings.IsRemotePathOpened(remoteFilePath)) + { + // Does the local file already exist? + if (!File.Exists(localFilePath)) + { + // Load the file contents from the remote machine and create the buffer + PSCommand command = new PSCommand(); + command.AddCommand("Microsoft.PowerShell.Management\\Get-Content"); + command.AddParameter("Path", remoteFilePath); + command.AddParameter("Raw"); + command.AddParameter("Encoding", "Byte"); + + byte[] fileContent = + (await this.powerShellContext.ExecuteCommandAsync(command, false, false)) + .FirstOrDefault(); + + if (fileContent != null) + { + this.StoreRemoteFile(localFilePath, fileContent, pathMappings); + } + else + { + this.logger.LogWarning( + $"Could not load contents of remote file '{remoteFilePath}'"); + } + } + } + } + catch (IOException e) + { + this.logger.LogError( + $"Caught {e.GetType().Name} while attempting to get remote file at path '{remoteFilePath}'\r\n\r\n{e.ToString()}"); + } + } + + return localFilePath; + } + + /// + /// Saves the contents of the file under the temporary local + /// file cache to its corresponding remote file. + /// + /// + /// The local file whose contents will be saved. It is assumed + /// that the editor has saved the contents of the local cache + /// file to disk before this method is called. + /// + /// A Task to be awaited for completion. + public async Task SaveRemoteFileAsync(string localFilePath) + { + string remoteFilePath = + this.GetMappedPath( + localFilePath, + this.powerShellContext.CurrentRunspace); + + this.logger.LogTrace( + $"Saving remote file {remoteFilePath} (local path: {localFilePath})"); + + byte[] localFileContents = null; + try + { + localFileContents = File.ReadAllBytes(localFilePath); + } + catch (IOException e) + { + this.logger.LogException( + "Failed to read contents of local copy of remote file", + e); + + return; + } + + PSCommand saveCommand = new PSCommand(); + saveCommand + .AddScript(SetRemoteContentsScript) + .AddParameter("RemoteFilePath", remoteFilePath) + .AddParameter("Content", localFileContents); + + StringBuilder errorMessages = new StringBuilder(); + + await this.powerShellContext.ExecuteCommandAsync( + saveCommand, + errorMessages, + false, + false); + + if (errorMessages.Length > 0) + { + this.logger.LogError($"Remote file save failed due to an error:\r\n\r\n{errorMessages}"); + } + } + + /// + /// Creates a temporary file with the given name and contents + /// corresponding to the specified runspace. + /// + /// + /// The name of the file to be created under the session path. + /// + /// + /// The contents of the file to be created. + /// + /// + /// The runspace for which the temporary file relates. + /// + /// The full temporary path of the file if successful, null otherwise. + public string CreateTemporaryFile(string fileName, string fileContents, RunspaceDetails runspaceDetails) + { + string temporaryFilePath = Path.Combine(this.processTempPath, fileName); + + try + { + File.WriteAllText(temporaryFilePath, fileContents); + + RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); + pathMappings.AddOpenedLocalPath(temporaryFilePath); + } + catch (IOException e) + { + this.logger.LogError( + $"Caught {e.GetType().Name} while attempting to write temporary file at path '{temporaryFilePath}'\r\n\r\n{e.ToString()}"); + + temporaryFilePath = null; + } + + return temporaryFilePath; + } + + /// + /// For a remote or local cache path, get the corresponding local or + /// remote file path. + /// + /// + /// The remote or local file path. + /// + /// + /// The runspace from which the remote file was fetched. + /// + /// The mapped file path. + public string GetMappedPath( + string filePath, + RunspaceDetails runspaceDetails) + { + RemotePathMappings remotePathMappings = this.GetPathMappings(runspaceDetails); + return remotePathMappings.GetMappedPath(filePath); + } + + /// + /// Returns true if the given file path is under the remote files + /// path in the temporary folder. + /// + /// The local file path to check. + /// + /// True if the file path is under the temporary remote files path. + /// + public bool IsUnderRemoteTempPath(string filePath) + { + return filePath.StartsWith( + this.remoteFilesPath, + System.StringComparison.CurrentCultureIgnoreCase); + } + + #endregion + + #region Private Methods + + private string StoreRemoteFile( + string remoteFilePath, + byte[] fileContent, + RunspaceDetails runspaceDetails) + { + RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); + string localFilePath = pathMappings.GetMappedPath(remoteFilePath); + + this.StoreRemoteFile( + localFilePath, + fileContent, + pathMappings); + + return localFilePath; + } + + private void StoreRemoteFile( + string localFilePath, + byte[] fileContent, + RemotePathMappings pathMappings) + { + File.WriteAllBytes(localFilePath, fileContent); + pathMappings.AddOpenedLocalPath(localFilePath); + } + + private RemotePathMappings GetPathMappings(RunspaceDetails runspaceDetails) + { + RemotePathMappings remotePathMappings = null; + string computerName = runspaceDetails.SessionDetails.ComputerName; + + if (!this.filesPerComputer.TryGetValue(computerName, out remotePathMappings)) + { + remotePathMappings = new RemotePathMappings(runspaceDetails, this); + this.filesPerComputer.Add(computerName, remotePathMappings); + } + + return remotePathMappings; + } + + private async void HandleRunspaceChangedAsync(object sender, RunspaceChangedEventArgs e) + { + if (e.ChangeAction == RunspaceChangeAction.Enter) + { + this.RegisterPSEditFunction(e.NewRunspace); + } + else + { + // Close any remote files that were opened + if (e.PreviousRunspace.Location == RunspaceLocation.Remote && + (e.ChangeAction == RunspaceChangeAction.Shutdown || + !string.Equals( + e.NewRunspace.SessionDetails.ComputerName, + e.PreviousRunspace.SessionDetails.ComputerName, + StringComparison.CurrentCultureIgnoreCase))) + { + RemotePathMappings remotePathMappings; + if (this.filesPerComputer.TryGetValue(e.PreviousRunspace.SessionDetails.ComputerName, out remotePathMappings)) + { + foreach (string remotePath in remotePathMappings.OpenedPaths) + { + await this.editorOperations?.CloseFileAsync(remotePath); + } + } + } + + if (e.PreviousRunspace != null) + { + this.RemovePSEditFunction(e.PreviousRunspace); + } + } + } + + private async void HandlePSEventReceivedAsync(object sender, PSEventArgs args) + { + if (string.Equals(RemoteSessionOpenFile, args.SourceIdentifier, StringComparison.CurrentCultureIgnoreCase)) + { + try + { + if (args.SourceArgs.Length >= 1) + { + string localFilePath = string.Empty; + string remoteFilePath = args.SourceArgs[0] as string; + + // Is this a local process runspace? Treat as a local file + if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Local) + { + localFilePath = remoteFilePath; + } + else + { + byte[] fileContent = null; + + if (args.SourceArgs.Length >= 2) + { + // Try to cast as a PSObject to get the BaseObject, if not, then try to case as a byte[] + PSObject sourceObj = args.SourceArgs[1] as PSObject; + if (sourceObj != null) + { + fileContent = sourceObj.BaseObject as byte[]; + } + else + { + fileContent = args.SourceArgs[1] as byte[]; + } + } + + // If fileContent is still null after trying to + // unpack the contents, just return an empty byte + // array. + fileContent = fileContent ?? new byte[0]; + + if (remoteFilePath != null) + { + localFilePath = + this.StoreRemoteFile( + remoteFilePath, + fileContent, + this.powerShellContext.CurrentRunspace); + } + else + { + await this.editorOperations?.NewFileAsync(); + EditorContext context = await this.editorOperations?.GetEditorContextAsync(); + context?.CurrentFile.InsertText(Encoding.UTF8.GetString(fileContent, 0, fileContent.Length)); + } + } + + bool preview = true; + if (args.SourceArgs.Length >= 3) + { + bool? previewCheck = args.SourceArgs[2] as bool?; + preview = previewCheck ?? true; + } + + // Open the file in the editor + this.editorOperations?.OpenFileAsync(localFilePath, preview); + } + } + catch (NullReferenceException e) + { + this.logger.LogException("Could not store null remote file content", e); + } + } + } + + private void RegisterPSEditFunction(RunspaceDetails runspaceDetails) + { + if (runspaceDetails.Location == RunspaceLocation.Remote && + runspaceDetails.Context == RunspaceContext.Original) + { + try + { + runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived += HandlePSEventReceivedAsync; + + PSCommand createCommand = new PSCommand(); + createCommand + .AddScript(CreatePSEditFunctionScript) + .AddParameter("PSEditModule", PSEditModule); + + if (runspaceDetails.Context == RunspaceContext.DebuggedRunspace) + { + this.powerShellContext.ExecuteCommandAsync(createCommand).Wait(); + } + else + { + using (var powerShell = System.Management.Automation.PowerShell.Create()) + { + powerShell.Runspace = runspaceDetails.Runspace; + powerShell.Commands = createCommand; + powerShell.Invoke(); + } + } + } + catch (RemoteException e) + { + this.logger.LogException("Could not create psedit function.", e); + } + } + } + + private void RemovePSEditFunction(RunspaceDetails runspaceDetails) + { + if (runspaceDetails.Location == RunspaceLocation.Remote && + runspaceDetails.Context == RunspaceContext.Original) + { + try + { + if (runspaceDetails.Runspace.Events != null) + { + runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived -= HandlePSEventReceivedAsync; + } + + if (runspaceDetails.Runspace.RunspaceStateInfo.State == RunspaceState.Opened) + { + using (var powerShell = System.Management.Automation.PowerShell.Create()) + { + powerShell.Runspace = runspaceDetails.Runspace; + powerShell.Commands.AddScript(RemovePSEditFunctionScript); + powerShell.Invoke(); + } + } + } + catch (Exception e) when (e is RemoteException || e is PSInvalidOperationException) + { + this.logger.LogException("Could not remove psedit function.", e); + } + } + } + + private void TryDeleteTemporaryPath() + { + try + { + if (Directory.Exists(this.processTempPath)) + { + Directory.Delete(this.processTempPath, true); + } + + Directory.CreateDirectory(this.processTempPath); + } + catch (IOException e) + { + this.logger.LogException( + $"Could not delete temporary folder for current process: {this.processTempPath}", e); + } + } + + #endregion + + #region Nested Classes + + private class RemotePathMappings + { + private RunspaceDetails runspaceDetails; + private RemoteFileManager remoteFileManager; + private HashSet openedPaths = new HashSet(); + private Dictionary pathMappings = new Dictionary(); + + public IEnumerable OpenedPaths + { + get { return openedPaths; } + } + + public RemotePathMappings( + RunspaceDetails runspaceDetails, + RemoteFileManager remoteFileManager) + { + this.runspaceDetails = runspaceDetails; + this.remoteFileManager = remoteFileManager; + } + + public void AddPathMapping(string remotePath, string localPath) + { + // Add mappings in both directions + this.pathMappings[localPath.ToLower()] = remotePath; + this.pathMappings[remotePath.ToLower()] = localPath; + } + + public void AddOpenedLocalPath(string openedLocalPath) + { + this.openedPaths.Add(openedLocalPath); + } + + public bool IsRemotePathOpened(string remotePath) + { + return this.openedPaths.Contains(remotePath); + } + + public string GetMappedPath(string filePath) + { + string mappedPath = filePath; + + if (!this.pathMappings.TryGetValue(filePath.ToLower(), out mappedPath)) + { + // If the path isn't mapped yet, generate it + if (!filePath.StartsWith(this.remoteFileManager.remoteFilesPath)) + { + mappedPath = + this.MapRemotePathToLocal( + filePath, + runspaceDetails.SessionDetails.ComputerName); + + this.AddPathMapping(filePath, mappedPath); + } + } + + return mappedPath; + } + + private string MapRemotePathToLocal(string remotePath, string connectionString) + { + // The path generated by this code will look something like + // %TEMP%\PSES-[PID]\RemoteFiles\1205823508\computer-name\MyFile.ps1 + // The "path hash" is just the hashed representation of the remote + // file's full path (sans directory) to try and ensure some amount of + // uniqueness across different files on the remote machine. We put + // the "connection string" after the path slug so that it can be used + // as the differentiator string in editors like VS Code when more than + // one tab has the same filename. + + var sessionDir = Directory.CreateDirectory(this.remoteFileManager.remoteFilesPath); + var pathHashDir = + sessionDir.CreateSubdirectory( + Path.GetDirectoryName(remotePath).GetHashCode().ToString()); + + var remoteFileDir = pathHashDir.CreateSubdirectory(connectionString); + + return + Path.Combine( + remoteFileDir.FullName, + Path.GetFileName(remotePath)); + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs new file mode 100644 index 000000000..7efaf57d8 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Defines the set of actions that will cause the runspace to be changed. + /// + public enum RunspaceChangeAction + { + /// + /// The runspace change was caused by entering a new session. + /// + Enter, + + /// + /// The runspace change was caused by exiting the current session. + /// + Exit, + + /// + /// The runspace change was caused by shutting down the service. + /// + Shutdown + } + + /// + /// Provides arguments for the PowerShellContext.RunspaceChanged event. + /// + public class RunspaceChangedEventArgs + { + /// + /// Gets the RunspaceChangeAction which caused this event. + /// + public RunspaceChangeAction ChangeAction { get; private set; } + + /// + /// Gets a RunspaceDetails object describing the previous runspace. + /// + public RunspaceDetails PreviousRunspace { get; private set; } + + /// + /// Gets a RunspaceDetails object describing the new runspace. + /// + public RunspaceDetails NewRunspace { get; private set; } + + /// + /// Creates a new instance of the RunspaceChangedEventArgs class. + /// + /// The action which caused the runspace to change. + /// The previously active runspace. + /// The newly active runspace. + public RunspaceChangedEventArgs( + RunspaceChangeAction changeAction, + RunspaceDetails previousRunspace, + RunspaceDetails newRunspace) + { + Validate.IsNotNull(nameof(previousRunspace), previousRunspace); + + this.ChangeAction = changeAction; + this.PreviousRunspace = previousRunspace; + this.NewRunspace = newRunspace; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs new file mode 100644 index 000000000..1188d20a1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs @@ -0,0 +1,321 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.CSharp.RuntimeBinder; +using System; +using System.Management.Automation.Runspaces; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Specifies the possible types of a runspace. + /// + public enum RunspaceLocation + { + /// + /// A runspace on the local machine. + /// + Local, + + /// + /// A runspace on a different machine. + /// + Remote + } + + /// + /// Specifies the context in which the runspace was encountered. + /// + public enum RunspaceContext + { + /// + /// The original runspace in a local or remote session. + /// + Original, + + /// + /// A runspace in a process that was entered with Enter-PSHostProcess. + /// + EnteredProcess, + + /// + /// A runspace that is being debugged with Debug-Runspace. + /// + DebuggedRunspace + } + + /// + /// Provides details about a runspace being used in the current + /// editing session. + /// + public class RunspaceDetails + { + #region Private Fields + + private Dictionary capabilities = + new Dictionary(); + + #endregion + + #region Properties + + /// + /// Gets the Runspace instance for which this class contains details. + /// + internal Runspace Runspace { get; private set; } + + /// + /// Gets the PowerShell version of the new runspace. + /// + public PowerShellVersionDetails PowerShellVersion { get; private set; } + + /// + /// Gets the runspace location, either Local or Remote. + /// + public RunspaceLocation Location { get; private set; } + + /// + /// Gets the context in which the runspace was encountered. + /// + public RunspaceContext Context { get; private set; } + + /// + /// Gets the "connection string" for the runspace, generally the + /// ComputerName for a remote runspace or the ProcessId of an + /// "Attach" runspace. + /// + public string ConnectionString { get; private set; } + + /// + /// Gets the details of the runspace's session at the time this + /// RunspaceDetails object was created. + /// + public SessionDetails SessionDetails { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the RunspaceDetails class. + /// + /// + /// The runspace for which this instance contains details. + /// + /// + /// The SessionDetails for the runspace. + /// + /// + /// The PowerShellVersionDetails of the runspace. + /// + /// + /// The RunspaceLocation of the runspace. + /// + /// + /// The RunspaceContext of the runspace. + /// + /// + /// The connection string of the runspace. + /// + public RunspaceDetails( + Runspace runspace, + SessionDetails sessionDetails, + PowerShellVersionDetails powerShellVersion, + RunspaceLocation runspaceLocation, + RunspaceContext runspaceContext, + string connectionString) + { + this.Runspace = runspace; + this.SessionDetails = sessionDetails; + this.PowerShellVersion = powerShellVersion; + this.Location = runspaceLocation; + this.Context = runspaceContext; + this.ConnectionString = connectionString; + } + + #endregion + + #region Public Methods + + internal void AddCapability(TCapability capability) + where TCapability : IRunspaceCapability + { + this.capabilities.Add(typeof(TCapability), capability); + } + + internal TCapability GetCapability() + where TCapability : IRunspaceCapability + { + TCapability capability = default(TCapability); + this.TryGetCapability(out capability); + return capability; + } + + internal bool TryGetCapability(out TCapability capability) + where TCapability : IRunspaceCapability + { + IRunspaceCapability capabilityAsInterface = default(TCapability); + if (this.capabilities.TryGetValue(typeof(TCapability), out capabilityAsInterface)) + { + capability = (TCapability)capabilityAsInterface; + return true; + } + + capability = default(TCapability); + return false; + } + + internal bool HasCapability() + { + return this.capabilities.ContainsKey(typeof(TCapability)); + } + + /// + /// Creates and populates a new RunspaceDetails instance for the given runspace. + /// + /// + /// The runspace for which details will be gathered. + /// + /// + /// The SessionDetails for the runspace. + /// + /// An ILogger implementation used for writing log messages. + /// A new RunspaceDetails instance. + internal static RunspaceDetails CreateFromRunspace( + Runspace runspace, + SessionDetails sessionDetails, + ILogger logger) + { + Validate.IsNotNull(nameof(runspace), runspace); + Validate.IsNotNull(nameof(sessionDetails), sessionDetails); + + var runspaceLocation = RunspaceLocation.Local; + var runspaceContext = RunspaceContext.Original; + var versionDetails = PowerShellVersionDetails.GetVersionDetails(runspace, logger); + + string connectionString = null; + + if (runspace.ConnectionInfo != null) + { + // Use 'dynamic' to avoid missing NamedPipeRunspaceConnectionInfo + // on PS v3 and v4 + try + { + dynamic connectionInfo = runspace.ConnectionInfo; + if (connectionInfo.ProcessId != null) + { + connectionString = connectionInfo.ProcessId.ToString(); + runspaceContext = RunspaceContext.EnteredProcess; + } + } + catch (RuntimeBinderException) + { + // ProcessId property isn't on the object, move on. + } + + // Grab the $host.name which will tell us if we're in a PSRP session or not + string hostName = + PowerShellContextService.ExecuteScriptAndGetItem( + "$Host.Name", + runspace, + defaultValue: string.Empty); + + // hostname is 'ServerRemoteHost' when the user enters a session. + // ex. Enter-PSSession + // Attaching to process currently needs to be marked as a local session + // so we skip this if block if the runspace is from Enter-PSHostProcess + if (hostName.Equals("ServerRemoteHost", StringComparison.Ordinal) + && runspace.OriginalConnectionInfo?.GetType().ToString() != "System.Management.Automation.Runspaces.NamedPipeConnectionInfo") + { + runspaceLocation = RunspaceLocation.Remote; + connectionString = + runspace.ConnectionInfo.ComputerName + + (connectionString != null ? $"-{connectionString}" : string.Empty); + } + } + + return + new RunspaceDetails( + runspace, + sessionDetails, + versionDetails, + runspaceLocation, + runspaceContext, + connectionString); + } + + /// + /// Creates a clone of the given runspace through which another + /// runspace was attached. Sets the IsAttached property of the + /// resulting RunspaceDetails object to true. + /// + /// + /// The RunspaceDetails object which the new object based. + /// + /// + /// The RunspaceContext of the runspace. + /// + /// + /// The SessionDetails for the runspace. + /// + /// + /// A new RunspaceDetails instance for the attached runspace. + /// + public static RunspaceDetails CreateFromContext( + RunspaceDetails runspaceDetails, + RunspaceContext runspaceContext, + SessionDetails sessionDetails) + { + return + new RunspaceDetails( + runspaceDetails.Runspace, + sessionDetails, + runspaceDetails.PowerShellVersion, + runspaceDetails.Location, + runspaceContext, + runspaceDetails.ConnectionString); + } + + /// + /// Creates a new RunspaceDetails object from a remote + /// debugging session. + /// + /// + /// The RunspaceDetails object which the new object based. + /// + /// + /// The RunspaceLocation of the runspace. + /// + /// + /// The RunspaceContext of the runspace. + /// + /// + /// The SessionDetails for the runspace. + /// + /// + /// A new RunspaceDetails instance for the attached runspace. + /// + public static RunspaceDetails CreateFromDebugger( + RunspaceDetails runspaceDetails, + RunspaceLocation runspaceLocation, + RunspaceContext runspaceContext, + SessionDetails sessionDetails) + { + // TODO: Get the PowerShellVersion correctly! + return + new RunspaceDetails( + runspaceDetails.Runspace, + sessionDetails, + runspaceDetails.PowerShellVersion, + runspaceLocation, + runspaceContext, + runspaceDetails.ConnectionString); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs new file mode 100644 index 000000000..ab0906fe6 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs @@ -0,0 +1,60 @@ +// +// 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.Management.Automation.Host; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides a handle to the runspace that is managed by + /// a PowerShellContext. The holder of this handle. + /// + public class RunspaceHandle : IDisposable + { + private PowerShellContextService powerShellContext; + + /// + /// Gets the runspace that is held by this handle. + /// + public Runspace Runspace + { + get + { + return ((IHostSupportsInteractiveSession)this.powerShellContext).Runspace; + } + } + + internal bool IsReadLine { get; } + + /// + /// Initializes a new instance of the RunspaceHandle class using the + /// given runspace. + /// + /// The PowerShellContext instance which manages the runspace. + public RunspaceHandle(PowerShellContextService powerShellContext) + : this(powerShellContext, false) + { } + + internal RunspaceHandle(PowerShellContextService powerShellContext, bool isReadLine) + { + this.powerShellContext = powerShellContext; + this.IsReadLine = isReadLine; + } + + /// + /// Disposes the RunspaceHandle once the holder is done using it. + /// Causes the handle to be released back to the PowerShellContext. + /// + public void Dispose() + { + // Release the handle and clear the runspace so that + // no further operations can be performed on it. + this.powerShellContext.ReleaseRunspaceHandle(this); + } + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs new file mode 100644 index 000000000..2347ce69a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.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; +using System.Management.Automation; +using System.Collections; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides details about the current PowerShell session. + /// + public class SessionDetails + { + /// + /// Gets the process ID of the current process. + /// + public int? ProcessId { get; private set; } + + /// + /// Gets the name of the current computer. + /// + public string ComputerName { get; private set; } + + /// + /// Gets the current PSHost instance ID. + /// + public Guid? InstanceId { get; private set; } + + /// + /// Creates an instance of SessionDetails using the information + /// contained in the PSObject which was obtained using the + /// PSCommand returned by GetDetailsCommand. + /// + /// + public SessionDetails(PSObject detailsObject) + { + Validate.IsNotNull(nameof(detailsObject), detailsObject); + + Hashtable innerHashtable = detailsObject.BaseObject as Hashtable; + + this.ProcessId = (int)innerHashtable["processId"] as int?; + this.ComputerName = innerHashtable["computerName"] as string; + this.InstanceId = innerHashtable["instanceId"] as Guid?; + } + + /// + /// Gets the PSCommand that gathers details from the + /// current session. + /// + /// A PSCommand used to gather session details. + public static PSCommand GetDetailsCommand() + { + PSCommand infoCommand = new PSCommand(); + infoCommand.AddScript( + "@{ 'computerName' = if ([Environment]::MachineName) {[Environment]::MachineName} else {'localhost'}; 'processId' = $PID; 'instanceId' = $host.InstanceId }"); + + return infoCommand; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs new file mode 100644 index 000000000..9a0fed0f3 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides details about a change in state of a PowerShellContext. + /// + public class SessionStateChangedEventArgs + { + /// + /// Gets the new state for the session. + /// + public PowerShellContextState NewSessionState { get; private set; } + + /// + /// Gets the execution result of the operation that caused + /// the state change. + /// + public PowerShellExecutionResult ExecutionResult { get; private set; } + + /// + /// Gets the exception that caused a failure state or null otherwise. + /// + public Exception ErrorException { get; private set; } + + /// + /// Creates a new instance of the SessionStateChangedEventArgs class. + /// + /// The new session state. + /// The result of the operation that caused the state change. + /// An exception that describes the failure, if any. + public SessionStateChangedEventArgs( + PowerShellContextState newSessionState, + PowerShellExecutionResult executionResult, + Exception errorException) + { + this.NewSessionState = newSessionState; + this.ExecutionResult = executionResult; + this.ErrorException = errorException; + } + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs new file mode 100644 index 000000000..8720e3fa7 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs @@ -0,0 +1,131 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides the ability to route PowerShell command invocations to a specific thread. + /// + internal class ThreadController + { + private PromptNestFrame _nestFrame; + + internal AsyncQueue PipelineRequestQueue { get; } + + internal TaskCompletionSource FrameExitTask { get; } + + internal int ManagedThreadId { get; } + + internal bool IsPipelineThread { get; } + + /// + /// Initializes an new instance of the ThreadController class. This constructor should only + /// ever been called from the thread it is meant to control. + /// + /// The parent PromptNestFrame object. + internal ThreadController(PromptNestFrame nestFrame) + { + _nestFrame = nestFrame; + PipelineRequestQueue = new AsyncQueue(); + FrameExitTask = new TaskCompletionSource(); + ManagedThreadId = Thread.CurrentThread.ManagedThreadId; + + // If the debugger stop is triggered on a thread with no default runspace we + // shouldn't attempt to route commands to it. + IsPipelineThread = Runspace.DefaultRunspace != null; + } + + /// + /// Determines if the caller is already on the thread that this object maintains. + /// + /// + /// A value indicating if the caller is already on the thread maintained by this object. + /// + internal bool IsCurrentThread() + { + return Thread.CurrentThread.ManagedThreadId == ManagedThreadId; + } + + /// + /// Requests the invocation of a PowerShell command on the thread maintained by this object. + /// + /// The execution request to send. + /// + /// A task object representing the asynchronous operation. The Result property will return + /// the output of the command invocation. + /// + internal async Task> RequestPipelineExecutionAsync( + PipelineExecutionRequest executionRequest) + { + await PipelineRequestQueue.EnqueueAsync(executionRequest); + return await executionRequest.Results; + } + + /// + /// Retrieves the first currently queued execution request. If there are no pending + /// execution requests then the task will be completed when one is requested. + /// + /// + /// A task object representing the asynchronous operation. The Result property will return + /// the retrieved pipeline execution request. + /// + internal async Task TakeExecutionRequestAsync() + { + return await PipelineRequestQueue.DequeueAsync(); + } + + /// + /// Marks the thread to be exited. + /// + /// + /// The resume action for the debugger. If the frame is not a debugger frame this parameter + /// is ignored. + /// + internal void StartThreadExit(DebuggerResumeAction action) + { + StartThreadExit(action, waitForExit: false); + } + + /// + /// Marks the thread to be exited. + /// + /// + /// The resume action for the debugger. If the frame is not a debugger frame this parameter + /// is ignored. + /// + /// + /// Indicates whether the method should block until the exit is completed. + /// + internal void StartThreadExit(DebuggerResumeAction action, bool waitForExit) + { + Task.Run(() => FrameExitTask.TrySetResult(action)); + if (!waitForExit) + { + return; + } + + _nestFrame.WaitForFrameExit(CancellationToken.None); + } + + /// + /// Creates a task object that completes when the thread has be marked for exit. + /// + /// + /// A task object representing the frame receiving a request to exit. The Result property + /// will return the DebuggerResumeAction supplied with the request. + /// + internal async Task Exit() + { + return await FrameExitTask.Task.ConfigureAwait(false); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs index b97485dc0..88fcc1f79 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs @@ -16,15 +16,23 @@ public class ConfigurationHandler : IDidChangeConfigurationHandler private readonly AnalysisService _analysisService; private readonly WorkspaceService _workspaceService; private readonly ConfigurationService _configurationService; + private readonly PowerShellContextService _powerShellContextService; private DidChangeConfigurationCapability _capability; + private bool _profilesLoaded; + private bool _consoleReplStarted; - - public ConfigurationHandler(ILoggerFactory factory, WorkspaceService workspaceService, AnalysisService analysisService, ConfigurationService configurationService) + public ConfigurationHandler( + ILoggerFactory factory, + WorkspaceService workspaceService, + AnalysisService analysisService, + ConfigurationService configurationService, + PowerShellContextService powerShellContextService) { _logger = factory.CreateLogger(); _workspaceService = workspaceService; _analysisService = analysisService; _configurationService = configurationService; + _powerShellContextService = powerShellContextService; } public object GetRegistrationOptions() @@ -40,7 +48,7 @@ public async Task Handle(DidChangeConfigurationParams request, Cancellatio return await Unit.Task; } // TODO ADD THIS BACK IN - // bool oldLoadProfiles = this.currentSettings.EnableProfileLoading; + bool oldLoadProfiles = _configurationService.CurrentSettings.EnableProfileLoading; bool oldScriptAnalysisEnabled = _configurationService.CurrentSettings.ScriptAnalysis.Enable ?? false; string oldScriptAnalysisSettingsPath = @@ -51,23 +59,22 @@ public async Task Handle(DidChangeConfigurationParams request, Cancellatio _workspaceService.WorkspacePath, _logger); - // TODO ADD THIS BACK IN - // if (!this.profilesLoaded && - // this.currentSettings.EnableProfileLoading && - // oldLoadProfiles != this.currentSettings.EnableProfileLoading) - // { - // await this.editorSession.PowerShellContext.LoadHostProfilesAsync(); - // this.profilesLoaded = true; - // } + if (!this._profilesLoaded && + _configurationService.CurrentSettings.EnableProfileLoading && + oldLoadProfiles != _configurationService.CurrentSettings.EnableProfileLoading) + { + await _powerShellContextService.LoadHostProfilesAsync(); + this._profilesLoaded = true; + } - // // Wait until after profiles are loaded (or not, if that's the - // // case) before starting the interactive console. - // if (!this.consoleReplStarted) - // { - // // Start the interactive terminal - // this.editorSession.HostInput.StartCommandLoop(); - // this.consoleReplStarted = true; - // } + // Wait until after profiles are loaded (or not, if that's the + // case) before starting the interactive console. + if (!this._consoleReplStarted) + { + // Start the interactive terminal + _powerShellContextService.ConsoleReader.StartCommandLoop(); + this._consoleReplStarted = true; + } // If there is a new settings file path, restart the analyzer with the new settigs. bool settingsPathChanged = false; diff --git a/src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs b/src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs new file mode 100644 index 000000000..5eba1b24f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs @@ -0,0 +1,128 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides a simple wrapper over a SemaphoreSlim to allow + /// synchronization locking inside of async calls. Cannot be + /// used recursively. + /// + public class AsyncLock + { + #region Fields + + private Task lockReleaseTask; + private SemaphoreSlim lockSemaphore = new SemaphoreSlim(1, 1); + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the AsyncLock class. + /// + public AsyncLock() + { + this.lockReleaseTask = + Task.FromResult( + (IDisposable)new LockReleaser(this)); + } + + #endregion + + #region Public Methods + + /// + /// Locks + /// + /// A task which has an IDisposable + public Task LockAsync() + { + return this.LockAsync(CancellationToken.None); + } + + /// + /// Obtains or waits for a lock which can be used to synchronize + /// access to a resource. The wait may be cancelled with the + /// given CancellationToken. + /// + /// + /// A CancellationToken which can be used to cancel the lock. + /// + /// + public Task LockAsync(CancellationToken cancellationToken) + { + Task waitTask = lockSemaphore.WaitAsync(cancellationToken); + + return waitTask.IsCompleted ? + this.lockReleaseTask : + waitTask.ContinueWith( + (t, releaser) => + { + return (IDisposable)releaser; + }, + this.lockReleaseTask.Result, + cancellationToken, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + + /// + /// Obtains or waits for a lock which can be used to synchronize + /// access to a resource. + /// + /// + public IDisposable Lock() + { + return Lock(CancellationToken.None); + } + + /// + /// Obtains or waits for a lock which can be used to synchronize + /// access to a resource. The wait may be cancelled with the + /// given CancellationToken. + /// + /// + /// A CancellationToken which can be used to cancel the lock. + /// + /// + public IDisposable Lock(CancellationToken cancellationToken) + { + lockSemaphore.Wait(cancellationToken); + return this.lockReleaseTask.Result; + } + + #endregion + + #region Private Classes + + /// + /// Provides an IDisposable wrapper around an AsyncLock so + /// that it can easily be used inside of a 'using' block. + /// + private class LockReleaser : IDisposable + { + private AsyncLock lockToRelease; + + internal LockReleaser(AsyncLock lockToRelease) + { + this.lockToRelease = lockToRelease; + } + + public void Dispose() + { + this.lockToRelease.lockSemaphore.Release(); + } + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs b/src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs new file mode 100644 index 000000000..85bbc1592 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs @@ -0,0 +1,224 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides a synchronized queue which can be used from within async + /// operations. This is primarily used for producer/consumer scenarios. + /// + /// The type of item contained in the queue. + public class AsyncQueue + { + #region Private Fields + + private AsyncLock queueLock = new AsyncLock(); + private Queue itemQueue; + private Queue> requestQueue; + + #endregion + + #region Properties + + /// + /// Returns true if the queue is currently empty. + /// + public bool IsEmpty { get; private set; } + + #endregion + + #region Constructors + + /// + /// Initializes an empty instance of the AsyncQueue class. + /// + public AsyncQueue() : this(Enumerable.Empty()) + { + } + + /// + /// Initializes an instance of the AsyncQueue class, pre-populated + /// with the given collection of items. + /// + /// + /// An IEnumerable containing the initial items with which the queue will + /// be populated. + /// + public AsyncQueue(IEnumerable initialItems) + { + this.itemQueue = new Queue(initialItems); + this.requestQueue = new Queue>(); + } + + #endregion + + #region Public Methods + + /// + /// Enqueues an item onto the end of the queue. + /// + /// The item to be added to the queue. + /// + /// A Task which can be awaited until the synchronized enqueue + /// operation completes. + /// + public async Task EnqueueAsync(T item) + { + using (await queueLock.LockAsync()) + { + TaskCompletionSource requestTaskSource = null; + + // Are any requests waiting? + while (this.requestQueue.Count > 0) + { + // Is the next request cancelled already? + requestTaskSource = this.requestQueue.Dequeue(); + if (!requestTaskSource.Task.IsCanceled) + { + // Dispatch the item + requestTaskSource.SetResult(item); + return; + } + } + + // No more requests waiting, queue the item for a later request + this.itemQueue.Enqueue(item); + this.IsEmpty = false; + } + } + + /// + /// Enqueues an item onto the end of the queue. + /// + /// The item to be added to the queue. + public void Enqueue(T item) + { + using (queueLock.Lock()) + { + while (this.requestQueue.Count > 0) + { + var requestTaskSource = this.requestQueue.Dequeue(); + if (requestTaskSource.Task.IsCanceled) + { + continue; + } + + requestTaskSource.SetResult(item); + return; + } + } + + this.itemQueue.Enqueue(item); + this.IsEmpty = false; + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. + /// + /// + /// A Task which can be awaited until a value can be dequeued. + /// + public Task DequeueAsync() + { + return this.DequeueAsync(CancellationToken.None); + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. The wait can be cancelled + /// using the given CancellationToken. + /// + /// + /// A CancellationToken with which a dequeue wait can be cancelled. + /// + /// + /// A Task which can be awaited until a value can be dequeued. + /// + public async Task DequeueAsync(CancellationToken cancellationToken) + { + Task requestTask; + + using (await queueLock.LockAsync(cancellationToken)) + { + if (this.itemQueue.Count > 0) + { + // Items are waiting to be taken so take one immediately + T item = this.itemQueue.Dequeue(); + this.IsEmpty = this.itemQueue.Count == 0; + + return item; + } + else + { + // Queue the request for the next item + var requestTaskSource = new TaskCompletionSource(); + this.requestQueue.Enqueue(requestTaskSource); + + // Register the wait task for cancel notifications + cancellationToken.Register( + () => requestTaskSource.TrySetCanceled()); + + requestTask = requestTaskSource.Task; + } + } + + // Wait for the request task to complete outside of the lock + return await requestTask; + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. + /// + /// + public T Dequeue() + { + return Dequeue(CancellationToken.None); + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. The wait can be cancelled + /// using the given CancellationToken. + /// + /// + /// A CancellationToken with which a dequeue wait can be cancelled. + /// + /// + public T Dequeue(CancellationToken cancellationToken) + { + TaskCompletionSource requestTask; + using (queueLock.Lock(cancellationToken)) + { + if (this.itemQueue.Count > 0) + { + T item = this.itemQueue.Dequeue(); + this.IsEmpty = this.itemQueue.Count == 0; + + return item; + } + + requestTask = new TaskCompletionSource(); + this.requestQueue.Enqueue(requestTask); + + if (cancellationToken.CanBeCanceled) + { + cancellationToken.Register(() => requestTask.TrySetCanceled()); + } + } + + return requestTask.Task.GetAwaiter().GetResult(); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Utility/AsyncUtils.cs b/src/PowerShellEditorServices.Engine/Utility/AsyncUtils.cs new file mode 100644 index 000000000..8da21b942 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/AsyncUtils.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides utility methods for common asynchronous operations. + /// + internal static class AsyncUtils + { + /// + /// Creates a with an handle initial and + /// max count of one. + /// + /// A simple single handle . + internal static SemaphoreSlim CreateSimpleLockingSemaphore() + { + return new SemaphoreSlim(initialCount: 1, maxCount: 1); + } + } +} diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 4a4acf9c6..15ace52f8 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -147,6 +147,12 @@ public EditorServicesHost( this.serverCompletedTask = new TaskCompletionSource(); this.internalHost = internalHost; + while (!System.Diagnostics.Debugger.IsAttached) + { + System.Console.WriteLine(System.Diagnostics.Process.GetCurrentProcess().Id); + System.Threading.Thread.Sleep(2000); + } + #if DEBUG if (waitForDebugger) { diff --git a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj index 020372b3f..2245a4a22 100644 --- a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj +++ b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj @@ -16,7 +16,7 @@ - + From c798d01d45fecbb7a4ffd9083ee75998110f0fe4 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 7 Aug 2019 19:46:42 -0700 Subject: [PATCH 2/3] using file sink now instead --- PowerShellEditorServices.build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 464bfb1f2..972a17766 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -83,7 +83,7 @@ $script:RequiredBuildAssets = @{ 'publish/OmniSharp.Extensions.LanguageServer.dll', 'publish/Serilog.dll', 'publish/Serilog.Extensions.Logging.dll', - 'publish/Serilog.Sinks.Console.dll', + 'publish/Serilog.Sinks.File.dll', 'publish/Microsoft.Extensions.DependencyInjection.Abstractions.dll', 'publish/Microsoft.Extensions.DependencyInjection.dll', 'publish/Microsoft.Extensions.Logging.Abstractions.dll', From e1be7c4a2b092978192fb8cb03eff3a959837b30 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Fri, 9 Aug 2019 11:12:50 -0700 Subject: [PATCH 3/3] all the newlines --- .../PowerShellContext/Components/IComponentRegistry.cs | 2 +- .../Components/IComponentRegistryExtensions.cs | 2 +- .../PowerShellContext/Console/ChoicePromptHandler.cs | 1 - .../Extensions/EditorCommandAttribute.cs | 2 +- .../PowerShellContext/Extensions/EditorContext.cs | 1 - .../PowerShellContext/Extensions/EditorObject.cs | 1 - .../PowerShellContext/Extensions/EditorWorkspace.cs | 1 - .../PowerShellContext/Extensions/ExtensionService.cs | 1 - .../Services/PowerShellContext/Extensions/FileContext.cs | 1 - .../PowerShellContext/Extensions/IEditorOperations.cs | 1 - .../PowerShellContext/Session/Host/IHostInput.cs | 2 +- .../PowerShellContext/Session/OutputWrittenEventArgs.cs | 9 ++++----- .../PowerShellContext/Session/PowerShell5Operations.cs | 1 - .../PowerShellContext/Session/PowerShellContextState.cs | 7 +++---- .../Session/PowerShellExecutionResult.cs | 1 - .../PowerShellContext/Session/ProgressDetails.cs | 1 - .../Session/SessionStateChangedEventArgs.cs | 1 - src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs | 1 - .../Utility/AsyncQueue.cs | 1 - 19 files changed, 11 insertions(+), 26 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs index 1acd59588..c9f99000f 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs @@ -58,4 +58,4 @@ object Register( /// bool TryGet(Type componentType, out object componentInstance); } -} \ No newline at end of file +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs index cbbb119c1..0c6307d5a 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs @@ -84,4 +84,4 @@ public static bool TryGet( return false; } } -} \ No newline at end of file +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs index 6f49bab73..b4524789f 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs @@ -351,4 +351,3 @@ private int GetSingleResult(int[] choiceArray) #endregion } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs index 8020cb844..71b96d300 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs @@ -30,4 +30,4 @@ public class EditorCommandAttribute : Attribute #endregion } -} \ No newline at end of file +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs index c669acd19..83e94a7a5 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs @@ -114,4 +114,3 @@ public void SetSelection(BufferRange selectionRange) #endregion } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs index 28a0b8a32..39da959cf 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs @@ -108,4 +108,3 @@ public EditorContext GetEditorContext() } } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs index ad679f371..4eb22de4a 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs @@ -73,4 +73,3 @@ public void OpenFile(string filePath, bool preview) #endregion } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs index 89f0f7411..b96ac9e12 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs @@ -214,4 +214,3 @@ private void OnCommandRemoved(EditorCommand command) #endregion } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs index 5f6d98b7c..ace8f5271 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs @@ -277,4 +277,3 @@ public void SaveAs(string newFilePath) #endregion } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs index 7ef6bdf0e..4e3f532fd 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs @@ -125,4 +125,3 @@ public interface IEditorOperations Task SetStatusBarMessageAsync(string message, int? timeout); } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs index 28c79839d..95da783db 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs @@ -25,4 +25,4 @@ public interface IHostInput /// void SendControlC(); } -} \ No newline at end of file +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs index b1408a991..0e4663094 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs @@ -24,7 +24,7 @@ public class OutputWrittenEventArgs public OutputType OutputType { get; private set; } /// - /// Gets a boolean which indicates whether a newline + /// Gets a boolean which indicates whether a newline /// should be written after the output. /// public bool IncludeNewLine { get; private set; } @@ -49,9 +49,9 @@ public class OutputWrittenEventArgs /// The background color of the output text. public OutputWrittenEventArgs( string outputText, - bool includeNewLine, - OutputType outputType, - ConsoleColor foregroundColor, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, ConsoleColor backgroundColor) { this.OutputText = outputText; @@ -62,4 +62,3 @@ public OutputWrittenEventArgs( } } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs index c1024887a..a001d84bf 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs @@ -104,4 +104,3 @@ public void ExitNestedPrompt(PSHost host) } } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs index 2075c04d5..6ebdbc947 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs @@ -14,9 +14,9 @@ public enum PowerShellContextState /// Indicates an unknown, potentially uninitialized state. /// Unknown = 0, - + /// - /// Indicates the state where the session is starting but + /// Indicates the state where the session is starting but /// not yet fully initialized. /// NotStarted, @@ -26,7 +26,7 @@ public enum PowerShellContextState /// for execution. /// Ready, - + /// /// Indicates that the session is currently running a command. /// @@ -44,4 +44,3 @@ public enum PowerShellContextState Disposed } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs index ee15ae97a..3c941b5aa 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs @@ -37,4 +37,3 @@ public enum PowerShellExecutionResult Completed } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs index b88d7c1ae..d2ec4b1bd 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs @@ -30,4 +30,3 @@ internal static ProgressDetails Create(ProgressRecord progressRecord) } } } - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs index 9a0fed0f3..1d285636e 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs @@ -45,4 +45,3 @@ public SessionStateChangedEventArgs( } } } - diff --git a/src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs b/src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs index 5eba1b24f..65e92aa4f 100644 --- a/src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs +++ b/src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs @@ -125,4 +125,3 @@ public void Dispose() #endregion } } - diff --git a/src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs b/src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs index 85bbc1592..8125076b7 100644 --- a/src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs +++ b/src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs @@ -221,4 +221,3 @@ public T Dequeue(CancellationToken cancellationToken) #endregion } } -