diff --git a/.vsts-ci/azure-pipelines-ci.yml b/.vsts-ci/azure-pipelines-ci.yml index 25706f2c0..dc2f2d461 100644 --- a/.vsts-ci/azure-pipelines-ci.yml +++ b/.vsts-ci/azure-pipelines-ci.yml @@ -8,14 +8,6 @@ variables: - name: DOTNET_CLI_TELEMETRY_OPTOUT value: 'true' -trigger: - branches: - include: - - master - -pr: -- master - jobs: - job: PS51_Win2016 displayName: PowerShell 5.1 - Windows Server 2016 diff --git a/CHANGELOG.md b/CHANGELOG.md index ec126a985..77e7761b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,80 @@ # PowerShell Editor Services Release History +## v3.0.0 +### Thursday, October 28, 2021 + +This preview release includes a complete overhaul of the core PowerShell engine +of PowerShell Editor Services. +This represents over a year's work, +tracked in [PSES #1295](https://github.com/PowerShell/PowerShellEditorServices/issues/1295) +and implemented in [PSES #1459](https://github.com/PowerShell/PowerShellEditorServices/pull/1459), +and is our answer to many, many issues +opened by users over the last few years. +We're hoping you'll see a marked improvement +in the reliability, performance and footprint +of the extension as a result. + +Previously the Integrated Console was run +by setting threadpool tasks on a shared main runspace, +and where LSP servicing was done with PowerShell idle events. +This lead to overhead, threading issues +and a complex implementation intended to work around +the asymmetry between PowerShell as a synchronous, +single-threaded runtime and a language server +as an asynchronous, multi-threaded service. + +Now, PowerShell Editor Services maintains its own dedicated pipeline thread, +which is able to service requests similar to JavaScript's event loop, +meaning we can run everything synchronously on the correct thread. +We also get more efficiency because we can directly call +PowerShell APIs and code written in C# from this thread, +without the overhead of a PowerShell pipeline. + +This change has overhauled how we service LSP requests, +how the Integrated Console works, +how PSReadLine is integrated, +how debugging is implemented, +how remoting is handled, +and a long tail of other features in PowerShell Editor Services. + +Also, in making it, while 6,000 lines of code were added, +we removed 12,000, +for a more maintainable, more efficient +and easier to understand extension backend. + +While most of our testing has been re-enabled +(and we're working on adding more), +there are bound to be issues with this new implementation. +Please give this a try and let us know if you run into anything. + +We also want to thank [@SeeminglyScience](https://github.com/SeeminglyScience) +for his help and knowledge as we've made this migration. + +Finally, a crude breakdown of the work from the commits: + +- An initial dedicated pipeline thread consumer implementation +- Implement the console REPL +- Implement PSRL idle handling +- Implement completions +- Move to invoking PSRL as a C# delegate +- Implement cancellation and Ctrl+C +- Make F8 work again +- Ensure execution policy is set correctly +- Implement $PROFILE support +- Make nested prompts work +- Implement REPL debugging +- Implement remote debugging in the REPL +- Hook up the debugging UI +- Implement a new concurrent priority queue for PowerShell tasks +- Reimplement the REPL synchronously rather than on its own thread +- Really get debugging working... +- Implement DSC breakpoint support +- Reimplement legacy readline support +- Ensure stdio is still supported as an LSP transport +- Remove PowerShellContextService and other defunct code +- Get integration tests working again (and improve diagnosis of PSES failures) +- Get unit testing working again (except debug service tests) + ## v2.5.2 ### Monday, October 18, 2021 diff --git a/PowerShellEditorServices.Common.props b/PowerShellEditorServices.Common.props index d2371b014..0e8d135fe 100644 --- a/PowerShellEditorServices.Common.props +++ b/PowerShellEditorServices.Common.props @@ -1,6 +1,6 @@ - 2.5.2 + 3.0.0 Microsoft © Microsoft Corporation. diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psd1 b/module/PowerShellEditorServices/PowerShellEditorServices.psd1 index 071a37c8e..ff89a1890 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psd1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psd1 @@ -19,7 +19,7 @@ RootModule = if ($PSEdition -eq 'Core') } # Version number of this module. -ModuleVersion = '2.5.2' +ModuleVersion = '3.0.0' # ID used to uniquely identify this module GUID = '9ca15887-53a2-479a-9cda-48d26bcb6c47' diff --git a/modules.json b/modules.json index 5b1559a21..c879da4a9 100644 --- a/modules.json +++ b/modules.json @@ -6,6 +6,7 @@ "Version": "1.1.3" }, "PSReadLine": { - "Version": "2.1.0" + "Version": "2.2.0-beta4", + "AllowPrerelease": true } } diff --git a/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineConstructorCommand.cs b/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineConstructorCommand.cs deleted file mode 100644 index 58038d13e..000000000 --- a/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineConstructorCommand.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Management.Automation; -using System.Runtime.CompilerServices; - -namespace Microsoft.PowerShell.EditorServices.Commands -{ - /// - /// The Start-EditorServices command, the conventional entrypoint for PowerShell Editor Services. - /// - public sealed class InvokeReadLineConstructorCommand : PSCmdlet - { - protected override void EndProcessing() - { - Type type = Type.GetType("Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2"); - RuntimeHelpers.RunClassConstructor(type.TypeHandle); - } - } -} diff --git a/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineForEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineForEditorServicesCommand.cs deleted file mode 100644 index b0adf6fc8..000000000 --- a/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineForEditorServicesCommand.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Reflection; -using System.Threading; - -namespace Microsoft.PowerShell.EditorServices.Commands -{ - /// - /// The Start-EditorServices command, the conventional entrypoint for PowerShell Editor Services. - /// - public sealed class InvokeReadLineForEditorServicesCommand : PSCmdlet - { - private delegate string ReadLineInvoker( - Runspace runspace, - EngineIntrinsics engineIntrinsics, - CancellationToken cancellationToken); - - private static Lazy s_readLine = new Lazy(() => - { - Type type = Type.GetType("Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2"); - MethodInfo method = type?.GetMethod( - "ReadLine", - new[] { typeof(Runspace), typeof(EngineIntrinsics), typeof(CancellationToken) }); - - // TODO: Handle method being null here. This shouldn't ever happen. - - return (ReadLineInvoker)method.CreateDelegate(typeof(ReadLineInvoker)); - }); - - /// - /// The ID to give to the host's profile. - /// - [Parameter(Mandatory = true)] - [ValidateNotNullOrEmpty] - public CancellationToken CancellationToken { get; set; } - - protected override void EndProcessing() - { - // This returns a string. - object result = s_readLine.Value( - Runspace.DefaultRunspace, - SessionState.PSVariable.Get("ExecutionContext").Value as EngineIntrinsics, - CancellationToken - ); - - WriteObject(result); - } - } -} diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 10b7281af..7b48b9c3d 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -120,6 +120,11 @@ public static EditorServicesLoader Create( { AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) => { + if (args.LoadedAssembly.IsDynamic) + { + return; + } + logger.Log( PsesLogLevel.Diagnostic, $"Loaded '{args.LoadedAssembly.GetName()}' from '{args.LoadedAssembly.Location}'"); diff --git a/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs b/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs index 2b880c805..9229f7071 100644 --- a/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs +++ b/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs @@ -4,7 +4,7 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Services.Extension; using OmniSharp.Extensions.LanguageServer.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.Extensions.Services diff --git a/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs b/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs index 8d25db01d..73e8e209e 100644 --- a/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs +++ b/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs @@ -5,7 +5,7 @@ using System.Linq.Expressions; using System.Reflection; using Microsoft.Extensions.DependencyInjection; -using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Extension; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Server; diff --git a/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs b/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs index f83dd7d20..1fcafabcf 100644 --- a/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs +++ b/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.Extension; using OmniSharp.Extensions.LanguageServer.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.Extensions.Services @@ -116,7 +116,9 @@ public async Task PromptInputAsync(string message) new ShowInputPromptRequest { Name = message, - }).Returning(CancellationToken.None).ConfigureAwait(false); + }) + .Returning(CancellationToken.None) + .ConfigureAwait(false); if (response.PromptCancelled) { @@ -142,7 +144,9 @@ public async Task> PromptMultipleSelectionAsync(string mes Message = message, Choices = choiceDetails, DefaultChoices = defaultChoiceIndexes?.ToArray(), - }).Returning(CancellationToken.None).ConfigureAwait(false); + }) + .Returning(CancellationToken.None) + .ConfigureAwait(false); if (response.PromptCancelled) { @@ -168,7 +172,9 @@ public async Task PromptSelectionAsync(string message, IReadOnlyList -1 ? new[] { defaultChoiceIndex } : null, - }).Returning(CancellationToken.None).ConfigureAwait(false); + }) + .Returning(CancellationToken.None) + .ConfigureAwait(false); if (response.PromptCancelled) { diff --git a/src/PowerShellEditorServices/Extensions/Api/ExtensionCommandService.cs b/src/PowerShellEditorServices/Extensions/Api/ExtensionCommandService.cs index 229dc0e9b..64435154f 100644 --- a/src/PowerShellEditorServices/Extensions/Api/ExtensionCommandService.cs +++ b/src/PowerShellEditorServices/Extensions/Api/ExtensionCommandService.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Extension; using System; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs b/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs index 466e6b106..1a5254319 100644 --- a/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs +++ b/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using MediatR; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System.Threading; using System.Threading.Tasks; diff --git a/src/PowerShellEditorServices/Extensions/Api/WorkspaceService.cs b/src/PowerShellEditorServices/Extensions/Api/WorkspaceService.cs index 98eb9f06e..dc1300e00 100644 --- a/src/PowerShellEditorServices/Extensions/Api/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Extensions/Api/WorkspaceService.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Management.Automation.Language; -using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Extensions.Services { diff --git a/src/PowerShellEditorServices/Extensions/EditorContext.cs b/src/PowerShellEditorServices/Extensions/EditorContext.cs index f8e65f472..a844febe3 100644 --- a/src/PowerShellEditorServices/Extensions/EditorContext.cs +++ b/src/PowerShellEditorServices/Extensions/EditorContext.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using Microsoft.PowerShell.EditorServices.Services.TextDocument; namespace Microsoft.PowerShell.EditorServices.Extensions diff --git a/src/PowerShellEditorServices/Extensions/EditorFileRanges.cs b/src/PowerShellEditorServices/Extensions/EditorFileRanges.cs index f3d84e963..c7a7d5b38 100644 --- a/src/PowerShellEditorServices/Extensions/EditorFileRanges.cs +++ b/src/PowerShellEditorServices/Extensions/EditorFileRanges.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Services.Extension; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using System; diff --git a/src/PowerShellEditorServices/Extensions/EditorObject.cs b/src/PowerShellEditorServices/Extensions/EditorObject.cs index b88f41f84..68747ac27 100644 --- a/src/PowerShellEditorServices/Extensions/EditorObject.cs +++ b/src/PowerShellEditorServices/Extensions/EditorObject.cs @@ -5,7 +5,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Extensions.Services; -using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Extension; namespace Microsoft.PowerShell.EditorServices.Extensions { diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 02d0be68f..6a5c07bc2 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Threading.Tasks; - namespace Microsoft.PowerShell.EditorServices.Extensions { /// diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index 82fe5214a..ff827646e 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -8,10 +8,10 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Server; -using Microsoft.PowerShell.EditorServices.Services; using Serilog; using Serilog.Events; using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using Microsoft.PowerShell.EditorServices.Services.Extension; #if DEBUG using Serilog.Debugging; diff --git a/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs b/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs index 802d6c762..41026a5f1 100644 --- a/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs +++ b/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs @@ -3,8 +3,6 @@ using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.PowerShell.EditorServices.Logging { diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 55802fc82..6fdc36b3c 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -42,6 +42,9 @@ - + + + + diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index e4327cfbe..0b1bf209b 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -3,18 +3,15 @@ using System; using System.IO; -using System.Management.Automation; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Handlers; using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Utility; -using OmniSharp.Extensions.DebugAdapter.Protocol; -using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using OmniSharp.Extensions.DebugAdapter.Server; -using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Server; namespace Microsoft.PowerShell.EditorServices.Server @@ -24,17 +21,6 @@ namespace Microsoft.PowerShell.EditorServices.Server /// internal class PsesDebugServer : IDisposable { - /// - /// This is a bool but must be an int, since Interlocked.Exchange can't handle a bool - /// - private static int s_hasRunPsrlStaticCtor = 0; - - private static readonly Lazy s_lazyInvokeReadLineConstructorCmdletInfo = new Lazy(() => - { - var type = Type.GetType("Microsoft.PowerShell.EditorServices.Commands.InvokeReadLineConstructorCommand, Microsoft.PowerShell.EditorServices.Hosting"); - return new CmdletInfo("__Invoke-ReadLineConstructor", type); - }); - private readonly Stream _inputStream; private readonly Stream _outputStream; private readonly bool _useTempSession; @@ -42,7 +28,10 @@ internal class PsesDebugServer : IDisposable private readonly TaskCompletionSource _serverStopped; private DebugAdapterServer _debugAdapterServer; - private PowerShellContextService _powerShellContextService; + + private PsesInternalHost _psesHost; + + private bool _startedPses; protected readonly ILoggerFactory _loggerFactory; @@ -75,30 +64,17 @@ public async Task StartAsync() { // We need to let the PowerShell Context Service know that we are in a debug session // so that it doesn't send the powerShell/startDebugger message. - _powerShellContextService = ServiceProvider.GetService(); - _powerShellContextService.IsDebugServerActive = true; - - // Needed to make sure PSReadLine's static properties are initialized in the pipeline thread. - // This is only needed for Temp sessions who only have a debug server. - if (_usePSReadLine && _useTempSession && Interlocked.Exchange(ref s_hasRunPsrlStaticCtor, 1) == 0) - { - var command = new PSCommand() - .AddCommand(s_lazyInvokeReadLineConstructorCmdletInfo.Value); - - // This must be run synchronously to ensure debugging works - _powerShellContextService - .ExecuteCommandAsync(command, sendOutputToHost: true, sendErrorToHost: true) - .GetAwaiter() - .GetResult(); - } + _psesHost = ServiceProvider.GetService(); + _psesHost.DebugContext.IsDebugServerActive = true; options .WithInput(_inputStream) .WithOutput(_outputStream) - .WithServices(serviceCollection => serviceCollection - .AddLogging() - .AddOptions() - .AddPsesDebugServices(ServiceProvider, this, _useTempSession)) + .WithServices(serviceCollection => + serviceCollection + .AddLogging() + .AddOptions() + .AddPsesDebugServices(ServiceProvider, this, _useTempSession)) // TODO: Consider replacing all WithHandler with AddSingleton .WithHandler() .WithHandler() @@ -115,6 +91,12 @@ public async Task StartAsync() // The OnInitialize delegate gets run when we first receive the _Initialize_ request: // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize .OnInitialize(async (server, request, cancellationToken) => { + // We need to make sure the host has been started + _startedPses = !(await _psesHost.TryStartAsync(new HostStartOptions(), CancellationToken.None).ConfigureAwait(false)); + + // Ensure the debugger mode is set correctly - this is required for remote debugging to work + _psesHost.DebugContext.EnableDebugMode(); + var breakpointService = server.GetService(); // Clear any existing breakpoints before proceeding await breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(false); @@ -136,17 +118,27 @@ public async Task StartAsync() public void Dispose() { - _powerShellContextService.IsDebugServerActive = false; - // TODO: If the debugger has stopped, should we clear the breakpoints? + // Note that the lifetime of the DebugContext is longer than the debug server; + // It represents the debugger on the PowerShell process we're in, + // while a new debug server is spun up for every debugging session + _psesHost.DebugContext.IsDebugServerActive = false; _debugAdapterServer.Dispose(); _inputStream.Dispose(); _outputStream.Dispose(); _serverStopped.SetResult(true); + // TODO: If the debugger has stopped, should we clear the breakpoints? } public async Task WaitForShutdown() { await _serverStopped.Task.ConfigureAwait(false); + + // If we started the host, we need to ensure any errors are marshalled back to us like this + if (_startedPses) + { + _psesHost.TriggerShutdown(); + await _psesHost.Shutdown.ConfigureAwait(false); + } } #region Events diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index bf74f8bba..5b0522818 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -8,6 +9,9 @@ using Microsoft.PowerShell.EditorServices.Handlers; using Microsoft.PowerShell.EditorServices.Hosting; using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Extension; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.Template; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using OmniSharp.Extensions.LanguageServer.Server; using Serilog; @@ -29,6 +33,8 @@ internal class PsesLanguageServer private readonly HostStartupInfo _hostDetails; private readonly TaskCompletionSource _serverStart; + private PsesInternalHost _psesHost; + /// /// Create a new language server instance. /// @@ -72,8 +78,12 @@ public async Task StartAsync() options .WithInput(_inputStream) .WithOutput(_outputStream) - .WithServices(serviceCollection => serviceCollection - .AddPsesLanguageServices(_hostDetails)) // NOTE: This adds a lot of services! + .WithServices(serviceCollection => + { + + // NOTE: This adds a lot of services! + serviceCollection.AddPsesLanguageServices(_hostDetails); + }) .ConfigureLogging(builder => builder .AddSerilog(Log.Logger) // TODO: Set dispose to true? .AddLanguageProtocolLogging() @@ -107,12 +117,14 @@ public async Task StartAsync() // _Initialize_ request: // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize .OnInitialize( - // TODO: Either fix or ignore "method lacks 'await'" warning. - async (languageServer, request, cancellationToken) => + (languageServer, request, cancellationToken) => { Log.Logger.Debug("Initializing OmniSharp Language Server"); - var serviceProvider = languageServer.Services; + IServiceProvider serviceProvider = languageServer.Services; + + _psesHost = serviceProvider.GetService(); + var workspaceService = serviceProvider.GetService(); // Grab the workspace path from the parameters @@ -130,6 +142,8 @@ public async Task StartAsync() break; } } + + return Task.CompletedTask; }); }).ConfigureAwait(false); @@ -145,6 +159,10 @@ public async Task WaitForShutdown() Log.Logger.Debug("Shutting down OmniSharp Language Server"); await _serverStart.Task.ConfigureAwait(false); await LanguageServer.WaitForExit.ConfigureAwait(false); + + // Doing this means we're able to route through any exceptions experienced on the pipeline thread + _psesHost.TriggerShutdown(); + await _psesHost.Shutdown.ConfigureAwait(false); } } } diff --git a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs index e0fda77c0..55f440a07 100644 --- a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs +++ b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs @@ -2,12 +2,16 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; -using System.Management.Automation.Host; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Hosting; using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Extension; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; +using Microsoft.PowerShell.EditorServices.Services.Template; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.Server { @@ -17,33 +21,36 @@ public static IServiceCollection AddPsesLanguageServices( this IServiceCollection collection, HostStartupInfo hostStartupInfo) { - return collection.AddSingleton() + return collection + .AddSingleton(hostStartupInfo) + .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton( + (provider) => provider.GetService()) + .AddSingleton( + (provider) => provider.GetService()) .AddSingleton() - .AddSingleton( - (provider) => - PowerShellContextService.Create( - provider.GetService(), - // NOTE: Giving the context service access to the language server this - // early is dangerous because it allows it to start sending - // notifications etc. before it has initialized, potentially resulting - // in deadlocks. We're working on a solution to this. - provider.GetService(), - hostStartupInfo)) - .AddSingleton() // TODO: What's the difference between this and the TemplateHandler? + .AddSingleton( + (provider) => provider.GetService().DebugContext) + .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton( - (provider) => + .AddSingleton((provider) => { var extensionService = new ExtensionService( - provider.GetService(), - // NOTE: See above warning. - provider.GetService()); - extensionService.InitializeAsync( - serviceProvider: provider, - editorOperations: provider.GetService()) - .Wait(); + provider.GetService(), + provider, + provider.GetService(), + provider.GetService()); + + // This is where we create the $psEditor variable + // so that when the console is ready, it will be available + // TODO: Improve the sequencing here so that: + // - The variable is guaranteed to be initialized when the console first appears + // - Any errors that occur are handled rather than lost by the unawaited task + extensionService.InitializeAsync(); + return extensionService; }) .AddSingleton(); @@ -55,7 +62,13 @@ public static IServiceCollection AddPsesDebugServices( PsesDebugServer psesDebugServer, bool useTempSession) { - return collection.AddSingleton(languageServiceProvider.GetService()) + PsesInternalHost internalHost = languageServiceProvider.GetService(); + + return collection + .AddSingleton(internalHost) + .AddSingleton(internalHost) + .AddSingleton(internalHost.DebugContext) + .AddSingleton(languageServiceProvider.GetService()) .AddSingleton(languageServiceProvider.GetService()) .AddSingleton(languageServiceProvider.GetService()) .AddSingleton(psesDebugServer) diff --git a/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs index eee266e0f..36f8f5181 100644 --- a/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs @@ -133,7 +133,7 @@ public AnalysisService( /// The files to run script analysis on. /// A cancellation token to cancel this call with. /// A task that finishes when script diagnostics have been published. - public void RunScriptDiagnostics( + public void StartScriptDiagnostics( ScriptFile[] filesToAnalyze) { if (_configurationService.CurrentSettings.ScriptAnalysis.Enable == false) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index 01de55b5a..1718c3168 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -5,17 +5,21 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; namespace Microsoft.PowerShell.EditorServices.Services { internal class BreakpointService { private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; + private readonly IInternalPowerShellExecutionService _executionService; + private readonly PsesInternalHost _editorServicesHost; private readonly DebugStateService _debugStateService; // TODO: This needs to be managed per nested session @@ -27,39 +31,41 @@ internal class BreakpointService public BreakpointService( ILoggerFactory factory, - PowerShellContextService powerShellContextService, + IInternalPowerShellExecutionService executionService, + PsesInternalHost editorServicesHost, DebugStateService debugStateService) { _logger = factory.CreateLogger(); - _powerShellContextService = powerShellContextService; + _executionService = executionService; + _editorServicesHost = editorServicesHost; _debugStateService = debugStateService; } public async Task> GetBreakpointsAsync() { - if (BreakpointApiUtils.SupportsBreakpointApis) + if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace)) { return BreakpointApiUtils.GetBreakpoints( - _powerShellContextService.CurrentRunspace.Runspace.Debugger, + _editorServicesHost.Runspace.Debugger, _debugStateService.RunspaceId); } // Legacy behavior - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); - IEnumerable breakpoints = await _powerShellContextService.ExecuteCommandAsync(psCommand).ConfigureAwait(false); + PSCommand psCommand = new PSCommand() + .AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); + IEnumerable breakpoints = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); return breakpoints.ToList(); } public async Task> SetBreakpointsAsync(string escapedScriptPath, IEnumerable breakpoints) { - if (BreakpointApiUtils.SupportsBreakpointApis) + if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace)) { foreach (BreakpointDetails breakpointDetails in breakpoints) { try { - BreakpointApiUtils.SetBreakpoint(_powerShellContextService.CurrentRunspace.Runspace.Debugger, breakpointDetails, _debugStateService.RunspaceId); + BreakpointApiUtils.SetBreakpoint(_editorServicesHost.Runspace.Debugger, breakpointDetails, _debugStateService.RunspaceId); } catch(InvalidOperationException e) { @@ -133,7 +139,7 @@ public async Task> SetBreakpointsAsync(string esc if (psCommand != null) { IEnumerable setBreakpoints = - await _powerShellContextService.ExecuteCommandAsync(psCommand).ConfigureAwait(false); + await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); configuredBreakpoints.AddRange( setBreakpoints.Select((breakpoint) => BreakpointDetails.Create(breakpoint)) ); @@ -144,13 +150,13 @@ public async Task> SetBreakpointsAsync(string esc public async Task> SetCommandBreakpoints(IEnumerable breakpoints) { - if (BreakpointApiUtils.SupportsBreakpointApis) + if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace)) { foreach (CommandBreakpointDetails commandBreakpointDetails in breakpoints) { try { - BreakpointApiUtils.SetBreakpoint(_powerShellContextService.CurrentRunspace.Runspace.Debugger, commandBreakpointDetails, _debugStateService.RunspaceId); + BreakpointApiUtils.SetBreakpoint(_editorServicesHost.Runspace.Debugger, commandBreakpointDetails, _debugStateService.RunspaceId); } catch(InvalidOperationException e) { @@ -211,7 +217,7 @@ public async Task> SetCommandBreakpoints(I if (psCommand != null) { IEnumerable setBreakpoints = - await _powerShellContextService.ExecuteCommandAsync(psCommand).ConfigureAwait(false); + await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); configuredBreakpoints.AddRange( setBreakpoints.Select(CommandBreakpointDetails.Create)); } @@ -226,16 +232,16 @@ public async Task RemoveAllBreakpointsAsync(string scriptPath = null) { try { - if (BreakpointApiUtils.SupportsBreakpointApis) + if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace)) { foreach (Breakpoint breakpoint in BreakpointApiUtils.GetBreakpoints( - _powerShellContextService.CurrentRunspace.Runspace.Debugger, + _editorServicesHost.Runspace.Debugger, _debugStateService.RunspaceId)) { if (scriptPath == null || scriptPath == breakpoint.Script) { BreakpointApiUtils.RemoveBreakpoint( - _powerShellContextService.CurrentRunspace.Runspace.Debugger, + _editorServicesHost.Runspace.Debugger, breakpoint, _debugStateService.RunspaceId); } @@ -256,7 +262,7 @@ public async Task RemoveAllBreakpointsAsync(string scriptPath = null) psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); - await _powerShellContextService.ExecuteCommandAsync(psCommand).ConfigureAwait(false); + await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); } catch (Exception e) { @@ -266,12 +272,12 @@ public async Task RemoveAllBreakpointsAsync(string scriptPath = null) public async Task RemoveBreakpointsAsync(IEnumerable breakpoints) { - if (BreakpointApiUtils.SupportsBreakpointApis) + if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace)) { foreach (Breakpoint breakpoint in breakpoints) { BreakpointApiUtils.RemoveBreakpoint( - _powerShellContextService.CurrentRunspace.Runspace.Debugger, + _editorServicesHost.Runspace.Debugger, breakpoint, _debugStateService.RunspaceId); @@ -302,7 +308,7 @@ public async Task RemoveBreakpointsAsync(IEnumerable breakpoints) psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); psCommand.AddParameter("Id", breakpoints.Select(b => b.Id).ToArray()); - await _powerShellContextService.ExecuteCommandAsync(psCommand).ConfigureAwait(false); + await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); } } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs index a83e3e87d..d6328801c 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs @@ -3,8 +3,11 @@ using System.Management.Automation; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Handlers; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.DebugAdapter.Protocol.Server; @@ -14,53 +17,57 @@ namespace Microsoft.PowerShell.EditorServices.Services internal class DebugEventHandlerService { private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; + private readonly IInternalPowerShellExecutionService _executionService; private readonly DebugService _debugService; private readonly DebugStateService _debugStateService; private readonly IDebugAdapterServerFacade _debugAdapterServer; + private readonly IPowerShellDebugContext _debugContext; + public DebugEventHandlerService( ILoggerFactory factory, - PowerShellContextService powerShellContextService, + IInternalPowerShellExecutionService executionService, DebugService debugService, DebugStateService debugStateService, - IDebugAdapterServerFacade debugAdapterServer) + IDebugAdapterServerFacade debugAdapterServer, + IPowerShellDebugContext debugContext) { _logger = factory.CreateLogger(); - _powerShellContextService = powerShellContextService; + _executionService = executionService; _debugService = debugService; _debugStateService = debugStateService; _debugAdapterServer = debugAdapterServer; + _debugContext = debugContext; } internal void RegisterEventHandlers() { - _powerShellContextService.RunspaceChanged += PowerShellContext_RunspaceChanged; - _debugService.BreakpointUpdated += DebugService_BreakpointUpdated; - _debugService.DebuggerStopped += DebugService_DebuggerStopped; - _powerShellContextService.DebuggerResumed += PowerShellContext_DebuggerResumed; + _executionService.RunspaceChanged += OnRunspaceChanged; + _debugService.BreakpointUpdated += OnBreakpointUpdated; + _debugService.DebuggerStopped += OnDebuggerStopped; + _debugContext.DebuggerResuming += OnDebuggerResuming; } internal void UnregisterEventHandlers() { - _powerShellContextService.RunspaceChanged -= PowerShellContext_RunspaceChanged; - _debugService.BreakpointUpdated -= DebugService_BreakpointUpdated; - _debugService.DebuggerStopped -= DebugService_DebuggerStopped; - _powerShellContextService.DebuggerResumed -= PowerShellContext_DebuggerResumed; + _executionService.RunspaceChanged -= OnRunspaceChanged; + _debugService.BreakpointUpdated -= OnBreakpointUpdated; + _debugService.DebuggerStopped -= OnDebuggerStopped; + _debugContext.DebuggerResuming -= OnDebuggerResuming; } #region Public methods internal void TriggerDebuggerStopped(DebuggerStoppedEventArgs e) { - DebugService_DebuggerStopped(null, e); + OnDebuggerStopped(null, e); } #endregion #region Event Handlers - private void DebugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) + private void OnDebuggerStopped(object sender, DebuggerStoppedEventArgs e) { // Provide the reason for why the debugger has stopped script execution. // See https://github.com/Microsoft/vscode/issues/3648 @@ -86,44 +93,50 @@ e.OriginalEvent.Breakpoints[0] is CommandBreakpoint }); } - private void PowerShellContext_RunspaceChanged(object sender, RunspaceChangedEventArgs e) + private void OnRunspaceChanged(object sender, RunspaceChangedEventArgs e) { - if (_debugStateService.WaitingForAttach && - e.ChangeAction == RunspaceChangeAction.Enter && - e.NewRunspace.Context == RunspaceContext.DebuggedRunspace) - { - // Sends the InitializedEvent so that the debugger will continue - // sending configuration requests - _debugStateService.WaitingForAttach = false; - _debugStateService.ServerStarted.SetResult(true); - } - else if ( - e.ChangeAction == RunspaceChangeAction.Exit && - _powerShellContextService.IsDebuggerStopped) + switch (e.ChangeAction) { - // Exited the session while the debugger is stopped, - // send a ContinuedEvent so that the client changes the - // UI to appear to be running again - _debugAdapterServer.SendNotification(EventNames.Continued, - new ContinuedEvent + case RunspaceChangeAction.Enter: + if (_debugStateService.WaitingForAttach + && e.NewRunspace.RunspaceOrigin == RunspaceOrigin.DebuggedRunspace) { - ThreadId = 1, - AllThreadsContinued = true - }); + // Sends the InitializedEvent so that the debugger will continue + // sending configuration requests + _debugStateService.WaitingForAttach = false; + _debugStateService.ServerStarted.SetResult(true); + } + return; + + case RunspaceChangeAction.Exit: + if (_debugContext.IsStopped) + { + // Exited the session while the debugger is stopped, + // send a ContinuedEvent so that the client changes the + // UI to appear to be running again + _debugAdapterServer.SendNotification( + EventNames.Continued, + new ContinuedEvent + { + ThreadId = ThreadsHandler.PipelineThread.Id, + AllThreadsContinued = true, + }); + } + return; } } - private void PowerShellContext_DebuggerResumed(object sender, DebuggerResumeAction e) + private void OnDebuggerResuming(object sender, DebuggerResumingEventArgs e) { _debugAdapterServer.SendNotification(EventNames.Continued, new ContinuedEvent { - ThreadId = 1, - AllThreadsContinued = true + AllThreadsContinued = true, + ThreadId = ThreadsHandler.PipelineThread.Id, }); } - private void DebugService_BreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) + private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) { // Don't send breakpoint update notifications when setting // breakpoints on behalf of the client. diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index dd69ce9b1..56b6ecb11 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -7,14 +7,16 @@ using System.Management.Automation; using System.Management.Automation.Language; using System.Reflection; -using System.Text; -using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; +using System.Threading; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; namespace Microsoft.PowerShell.EditorServices.Services { @@ -29,10 +31,14 @@ internal class DebugService private const string PsesGlobalVariableNamePrefix = "__psEditorServices_"; private const string TemporaryScriptFileName = "Script Listing.ps1"; - private readonly ILogger logger; - private readonly PowerShellContextService powerShellContext; + private readonly ILogger _logger; + private readonly IInternalPowerShellExecutionService _executionService; private readonly BreakpointService _breakpointService; - private RemoteFileManagerService remoteFileManager; + private readonly RemoteFileManagerService remoteFileManager; + + private readonly PsesInternalHost _psesHost; + + private readonly IPowerShellDebugContext _debugContext; private int nextVariableId; private string temporaryScriptListingPath; @@ -57,7 +63,7 @@ internal class DebugService /// Gets a boolean that indicates whether the debugger is currently /// stopped at a breakpoint. /// - public bool IsDebuggerStopped => this.powerShellContext.IsDebuggerStopped; + public bool IsDebuggerStopped => _debugContext.IsStopped; /// /// Gets the current DebuggerStoppedEventArgs when the debugger @@ -94,20 +100,23 @@ internal class DebugService //// /// An ILogger implementation used for writing log messages. public DebugService( - PowerShellContextService powerShellContext, + IInternalPowerShellExecutionService executionService, + IPowerShellDebugContext debugContext, RemoteFileManagerService remoteFileManager, BreakpointService breakpointService, + PsesInternalHost psesHost, ILoggerFactory factory) { - Validate.IsNotNull(nameof(powerShellContext), powerShellContext); + Validate.IsNotNull(nameof(executionService), executionService); - this.logger = factory.CreateLogger(); - this.powerShellContext = powerShellContext; + this._logger = factory.CreateLogger(); + _executionService = executionService; _breakpointService = breakpointService; - this.powerShellContext.DebuggerStop += this.OnDebuggerStopAsync; - this.powerShellContext.DebuggerResumed += this.OnDebuggerResumed; - - this.powerShellContext.BreakpointUpdated += this.OnBreakpointUpdated; + _psesHost = psesHost; + _debugContext = debugContext; + _debugContext.DebuggerStopped += OnDebuggerStopAsync; + _debugContext.DebuggerResuming += OnDebuggerResuming; + _debugContext.BreakpointUpdated += OnBreakpointUpdated; this.remoteFileManager = remoteFileManager; @@ -134,19 +143,16 @@ public async Task SetLineBreakpointsAsync( BreakpointDetails[] breakpoints, bool clearExisting = true) { - var dscBreakpoints = - this.powerShellContext - .CurrentRunspace - .GetCapability(); + DscBreakpointCapability dscBreakpoints = await _debugContext.GetDscBreakpointCapabilityAsync(CancellationToken.None).ConfigureAwait(false); string scriptPath = scriptFile.FilePath; // Make sure we're using the remote script path - if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && - this.remoteFileManager != null) + if (_psesHost.CurrentRunspace.IsOnRemoteMachine + && this.remoteFileManager != null) { if (!this.remoteFileManager.IsUnderRemoteTempPath(scriptPath)) { - this.logger.LogTrace( + this._logger.LogTrace( $"Could not set breakpoints for local path '{scriptPath}' in a remote session."); return Array.Empty(); @@ -155,7 +161,7 @@ public async Task SetLineBreakpointsAsync( string mappedPath = this.remoteFileManager.GetMappedPath( scriptPath, - this.powerShellContext.CurrentRunspace); + _psesHost.CurrentRunspace); scriptPath = mappedPath; } @@ -163,7 +169,7 @@ public async Task SetLineBreakpointsAsync( this.temporaryScriptListingPath != null && this.temporaryScriptListingPath.Equals(scriptPath, StringComparison.CurrentCultureIgnoreCase)) { - this.logger.LogTrace( + this._logger.LogTrace( $"Could not set breakpoint on temporary script listing path '{scriptPath}'."); return Array.Empty(); @@ -171,8 +177,7 @@ public async Task SetLineBreakpointsAsync( // Fix for issue #123 - file paths that contain wildcard chars [ and ] need to // quoted and have those wildcard chars escaped. - string escapedScriptPath = - PowerShellContextService.WildcardEscapePath(scriptPath); + string escapedScriptPath = PathUtils.WildcardEscapePath(scriptPath); if (dscBreakpoints == null || !dscBreakpoints.IsDscResourcePath(escapedScriptPath)) { @@ -185,9 +190,10 @@ public async Task SetLineBreakpointsAsync( } return await dscBreakpoints.SetLineBreakpointsAsync( - this.powerShellContext, + _executionService, escapedScriptPath, - breakpoints).ConfigureAwait(false); + breakpoints) + .ConfigureAwait(false); } /// @@ -205,9 +211,8 @@ public async Task SetCommandBreakpointsAsync( if (clearExisting) { // Flatten dictionary values into one list and remove them all. - await _breakpointService.RemoveBreakpointsAsync( - (await _breakpointService.GetBreakpointsAsync().ConfigureAwait(false)) - .Where( i => i is CommandBreakpoint)).ConfigureAwait(false); + IEnumerable existingBreakpoints = await _breakpointService.GetBreakpointsAsync().ConfigureAwait(false); + await _breakpointService.RemoveBreakpointsAsync(existingBreakpoints.OfType()).ConfigureAwait(false); } if (breakpoints.Length > 0) @@ -223,8 +228,7 @@ await _breakpointService.RemoveBreakpointsAsync( /// public void Continue() { - this.powerShellContext.ResumeDebugger( - DebuggerResumeAction.Continue); + _debugContext.Continue(); } /// @@ -232,8 +236,7 @@ public void Continue() /// public void StepOver() { - this.powerShellContext.ResumeDebugger( - DebuggerResumeAction.StepOver); + _debugContext.StepOver(); } /// @@ -241,8 +244,7 @@ public void StepOver() /// public void StepIn() { - this.powerShellContext.ResumeDebugger( - DebuggerResumeAction.StepInto); + _debugContext.StepInto(); } /// @@ -250,8 +252,7 @@ public void StepIn() /// public void StepOut() { - this.powerShellContext.ResumeDebugger( - DebuggerResumeAction.StepOut); + _debugContext.StepOut(); } /// @@ -261,8 +262,7 @@ public void StepOut() /// public void Break() { - // Break execution in the debugger - this.powerShellContext.BreakExecution(); + _debugContext.BreakExecution(); } /// @@ -271,7 +271,7 @@ public void Break() /// public void Abort() { - this.powerShellContext.AbortExecution(shouldAbortDebugSession: true); + _debugContext.Abort(); } /// @@ -288,14 +288,14 @@ public VariableDetailsBase[] GetVariables(int variableReferenceId) { if ((variableReferenceId < 0) || (variableReferenceId >= this.variables.Count)) { - logger.LogWarning($"Received request for variableReferenceId {variableReferenceId} that is out of range of valid indices."); + _logger.LogWarning($"Received request for variableReferenceId {variableReferenceId} that is out of range of valid indices."); return Array.Empty(); } VariableDetailsBase parentVariable = this.variables[variableReferenceId]; if (parentVariable.IsExpandable) { - childVariables = parentVariable.GetChildren(this.logger); + childVariables = parentVariable.GetChildren(this._logger); foreach (var child in childVariables) { // Only add child if it hasn't already been added. @@ -394,7 +394,7 @@ public async Task SetVariableAsync(int variableContainerReferenceId, str Validate.IsNotNull(nameof(name), name); Validate.IsNotNull(nameof(value), value); - this.logger.LogTrace($"SetVariableRequest for '{name}' to value string (pre-quote processing): '{value}'"); + this._logger.LogTrace($"SetVariableRequest for '{name}' to value string (pre-quote processing): '{value}'"); // An empty or whitespace only value is not a valid expression for SetVariable. if (value.Trim().Length == 0) @@ -403,26 +403,13 @@ public async Task SetVariableAsync(int variableContainerReferenceId, str } // Evaluate the expression to get back a PowerShell object from the expression string. - PSCommand psCommand = new PSCommand(); - psCommand.AddScript(value); - var errorMessages = new StringBuilder(); - var results = - await this.powerShellContext.ExecuteCommandAsync( - psCommand, - errorMessages, - false, - false).ConfigureAwait(false); - - // Check if PowerShell's evaluation of the expression resulted in an error. - object psobject = results.FirstOrDefault(); - if ((psobject == null) && (errorMessages.Length > 0)) - { - throw new InvalidPowerShellExpressionException(errorMessages.ToString()); - } + // This may throw, in which case the exception is propagated to the caller + PSCommand evaluateExpressionCommand = new PSCommand().AddScript(value); + object expressionResult = (await _executionService.ExecutePSCommandAsync(evaluateExpressionCommand, CancellationToken.None).ConfigureAwait(false)).FirstOrDefault(); // If PowerShellContext.ExecuteCommand returns an ErrorRecord as output, the expression failed evaluation. // Ideally we would have a separate means from communicating error records apart from normal output. - if (psobject is ErrorRecord errorRecord) + if (expressionResult is ErrorRecord errorRecord) { throw new InvalidPowerShellExpressionException(errorRecord.ToString()); } @@ -473,14 +460,12 @@ await this.powerShellContext.ExecuteCommandAsync( } // Now that we have the scope, get the associated PSVariable object for the variable to be set. - psCommand.Commands.Clear(); - psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable"); - psCommand.AddParameter("Name", name.TrimStart('$')); - psCommand.AddParameter("Scope", scope); - - IEnumerable result = await this.powerShellContext.ExecuteCommandAsync(psCommand, sendErrorToHost: false).ConfigureAwait(false); - PSVariable psVariable = result.FirstOrDefault(); + var getVariableCommand = new PSCommand() + .AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable") + .AddParameter("Name", name.TrimStart('$')) + .AddParameter("Scope", scope); + + PSVariable psVariable = (await _executionService.ExecutePSCommandAsync(getVariableCommand, CancellationToken.None).ConfigureAwait(false)).FirstOrDefault(); if (psVariable == null) { throw new Exception($"Failed to retrieve PSVariable object for '{name}' from scope '{scope}'."); @@ -491,47 +476,47 @@ await this.powerShellContext.ExecuteCommandAsync( // If it is not strongly typed, we simply assign the object directly to the PSVariable potentially changing its type. // Turns out ArgumentTypeConverterAttribute is not public. So we call the attribute through it's base class - // ArgumentTransformationAttribute. - var argTypeConverterAttr = - psVariable.Attributes - .OfType() - .FirstOrDefault(a => a.GetType().Name.Equals("ArgumentTypeConverterAttribute")); + ArgumentTransformationAttribute argTypeConverterAttr = null; + foreach (Attribute variableAttribute in psVariable.Attributes) + { + if (variableAttribute is ArgumentTransformationAttribute argTransformAttr + && argTransformAttr.GetType().Name.Equals("ArgumentTypeConverterAttribute")) + { + argTypeConverterAttr = argTransformAttr; + break; + } + } if (argTypeConverterAttr != null) { - // PSVariable is strongly typed. Need to apply the conversion/transform to the new value. - psCommand.Commands.Clear(); - psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable"); - psCommand.AddParameter("Name", "ExecutionContext"); - psCommand.AddParameter("ValueOnly"); + _logger.LogTrace($"Setting variable '{name}' using conversion to value: {expressionResult ?? ""}"); - errorMessages.Clear(); - - var getExecContextResults = - await this.powerShellContext.ExecuteCommandAsync( - psCommand, - errorMessages, - sendErrorToHost: false).ConfigureAwait(false); + psVariable.Value = await _executionService.ExecuteDelegateAsync( + "PS debugger argument converter", + ExecutionOptions.Default, + (pwsh, cancellationToken) => + { + var engineIntrinsics = (EngineIntrinsics)pwsh.Runspace.SessionStateProxy.GetVariable("ExecutionContext"); - EngineIntrinsics executionContext = getExecContextResults.OfType().FirstOrDefault(); + // TODO: This is almost (but not quite) the same as LanguagePrimitives.Convert(), which does not require the pipeline thread. + // We should investigate changing it. + return argTypeConverterAttr.Transform(engineIntrinsics, expressionResult); - var msg = $"Setting variable '{name}' using conversion to value: {psobject ?? ""}"; - this.logger.LogTrace(msg); + }, + CancellationToken.None).ConfigureAwait(false); - psVariable.Value = argTypeConverterAttr.Transform(executionContext, psobject); } else { // PSVariable is *not* strongly typed. In this case, whack the old value with the new value. - var msg = $"Setting variable '{name}' directly to value: {psobject ?? ""} - previous type was {psVariable.Value?.GetType().Name ?? ""}"; - this.logger.LogTrace(msg); - psVariable.Value = psobject; + _logger.LogTrace($"Setting variable '{name}' directly to value: {expressionResult ?? ""} - previous type was {psVariable.Value?.GetType().Name ?? ""}"); + psVariable.Value = expressionResult; } // Use the VariableDetails.ValueString functionality to get the string representation for client debugger. // This makes the returned string consistent with the strings normally displayed for variables in the debugger. var tempVariable = new VariableDetails(psVariable); - this.logger.LogTrace($"Set variable '{name}' to: {tempVariable.ValueString ?? ""}"); + _logger.LogTrace($"Set variable '{name}' to: {tempVariable.ValueString ?? ""}"); return tempVariable.ValueString; } @@ -551,29 +536,26 @@ public async Task EvaluateExpressionAsync( int stackFrameId, bool writeResultAsOutput) { - var results = - await this.powerShellContext.ExecuteScriptStringAsync( - expressionString, - false, - writeResultAsOutput).ConfigureAwait(false); + var command = new PSCommand().AddScript(expressionString); + IReadOnlyList results = await _executionService.ExecutePSCommandAsync( + command, + CancellationToken.None, + new PowerShellExecutionOptions { WriteOutputToHost = writeResultAsOutput, ThrowOnError = !writeResultAsOutput }).ConfigureAwait(false); // Since this method should only be getting invoked in the debugger, // we can assume that Out-String will be getting used to format results // of command executions into string output. However, if null is returned // then return null so that no output gets displayed. - string outputString = - results != null && results.Any() ? - string.Join(Environment.NewLine, results) : - null; - - // If we've written the result as output, don't return a - // VariableDetails instance. - return - writeResultAsOutput ? - null : - new VariableDetails( - expressionString, - outputString); + if (writeResultAsOutput || results == null || results.Count == 0) + { + return null; + } + + // If we didn't write output, + // return a VariableDetails instance. + return new VariableDetails( + expressionString, + string.Join(Environment.NewLine, results)); } /// @@ -698,19 +680,27 @@ private async Task FetchVariableContainerAsync( string scope, VariableContainerDetails autoVariables) { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand("Get-Variable"); - psCommand.AddParameter("Scope", scope); + PSCommand psCommand = new PSCommand() + .AddCommand("Get-Variable") + .AddParameter("Scope", scope); - var scopeVariableContainer = - new VariableContainerDetails(this.nextVariableId++, "Scope: " + scope); + var scopeVariableContainer = new VariableContainerDetails(this.nextVariableId++, "Scope: " + scope); this.variables.Add(scopeVariableContainer); - var results = await this.powerShellContext.ExecuteCommandAsync(psCommand, sendErrorToHost: false).ConfigureAwait(false); + IReadOnlyList results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None) + .ConfigureAwait(false); + if (results != null) { foreach (PSObject psVariableObject in results) { + // Under some circumstances, we seem to get variables back with no "Name" field + // We skip over those here + if (psVariableObject.Properties["Name"] is null) + { + continue; + } + var variableDetails = new VariableDetails(psVariableObject) { Id = this.nextVariableId++ }; this.variables.Add(variableDetails); scopeVariableContainer.Children.Add(variableDetails.Name, variableDetails); @@ -754,7 +744,7 @@ private bool AddToAutoVariables(PSObject psvariable, string scope) optionsProperty.Value as string, out variableScope)) { - this.logger.LogWarning( + this._logger.LogWarning( $"Could not parse a variable's ScopedItemOptions value of '{optionsProperty.Value}'"); } } @@ -810,7 +800,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) var callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; psCommand.AddScript($"{callStackVarName} = Get-PSCallStack; {callStackVarName}"); - var results = await this.powerShellContext.ExecuteCommandAsync(psCommand).ConfigureAwait(false); + var results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); var callStackFrames = results.ToArray(); @@ -830,7 +820,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) // When debugging, this is the best way I can find to get what is likely the workspace root. // This is controlled by the "cwd:" setting in the launch config. - string workspaceRootPath = this.powerShellContext.InitialWorkingDirectory; + string workspaceRootPath = _psesHost.InitialWorkingDirectory; this.stackFrameDetails[i] = StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables, workspaceRootPath); @@ -841,14 +831,14 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) { this.stackFrameDetails[i].ScriptPath = scriptNameOverride; } - else if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && - this.remoteFileManager != null && - !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) + else if (_psesHost.CurrentRunspace.IsOnRemoteMachine + && this.remoteFileManager != null + && !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) { this.stackFrameDetails[i].ScriptPath = this.remoteFileManager.GetMappedPath( stackFrameScriptPath, - this.powerShellContext.CurrentRunspace); + _psesHost.CurrentRunspace); } } } @@ -893,9 +883,9 @@ internal async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e) PSCommand command = new PSCommand(); command.AddScript($"list 1 {int.MaxValue}"); - IEnumerable scriptListingLines = - await this.powerShellContext.ExecuteCommandAsync( - command, false, false).ConfigureAwait(false); + IReadOnlyList scriptListingLines = + await _executionService.ExecutePSCommandAsync( + command, CancellationToken.None).ConfigureAwait(false); if (scriptListingLines != null) { @@ -910,9 +900,9 @@ await this.powerShellContext.ExecuteCommandAsync( this.temporaryScriptListingPath = this.remoteFileManager.CreateTemporaryFile( - $"[{this.powerShellContext.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}", + $"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}", scriptListing, - this.powerShellContext.CurrentRunspace); + _psesHost.CurrentRunspace); localScriptPath = this.temporaryScriptListingPath @@ -922,7 +912,7 @@ await this.powerShellContext.ExecuteCommandAsync( } else { - this.logger.LogWarning($"Could not load script context"); + this._logger.LogWarning($"Could not load script context"); } } @@ -932,14 +922,14 @@ await this.FetchStackFramesAndVariablesAsync( // If this is a remote connection and the debugger stopped at a line // in a script file, get the file contents - if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && - this.remoteFileManager != null && - !noScriptName) + if (_psesHost.CurrentRunspace.IsOnRemoteMachine + && this.remoteFileManager != null + && !noScriptName) { localScriptPath = await this.remoteFileManager.FetchRemoteFileAsync( e.InvocationInfo.ScriptName, - powerShellContext.CurrentRunspace).ConfigureAwait(false); + _psesHost.CurrentRunspace).ConfigureAwait(false); } if (this.stackFrameDetails.Length > 0) @@ -959,7 +949,7 @@ await this.remoteFileManager.FetchRemoteFileAsync( this.CurrentDebuggerStoppedEventArgs = new DebuggerStoppedEventArgs( e, - this.powerShellContext.CurrentRunspace, + _psesHost.CurrentRunspace, localScriptPath); // Notify the host that the debugger is stopped @@ -968,7 +958,7 @@ await this.remoteFileManager.FetchRemoteFileAsync( this.CurrentDebuggerStoppedEventArgs); } - private void OnDebuggerResumed(object sender, DebuggerResumeAction e) + private void OnDebuggerResuming(object sender, DebuggerResumingEventArgs debuggerResumingEventArgs) { this.CurrentDebuggerStoppedEventArgs = null; } @@ -989,17 +979,17 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) { // TODO: This could be either a path or a script block! string scriptPath = lineBreakpoint.Script; - if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && - this.remoteFileManager != null) + if (_psesHost.CurrentRunspace.IsOnRemoteMachine + && this.remoteFileManager != null) { string mappedPath = this.remoteFileManager.GetMappedPath( scriptPath, - this.powerShellContext.CurrentRunspace); + _psesHost.CurrentRunspace); if (mappedPath == null) { - this.logger.LogError( + this._logger.LogError( $"Could not map remote path '{scriptPath}' to a local path."); return; diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs index 2f03dd091..c12e89db0 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs @@ -13,7 +13,7 @@ internal class DebugStateService internal bool NoDebug { get; set; } - internal string Arguments { get; set; } + internal string[] Arguments { get; set; } internal bool IsRemoteAttach { get; set; } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs index 2ef66ddd8..82eacfe73 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Text; using System.Threading; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter @@ -27,6 +28,8 @@ internal static class BreakpointApiUtils private static readonly Lazy> s_removeBreakpointLazy; + private static readonly Version s_minimumBreakpointApiPowerShellVersion = new(7, 0, 0, 0); + private static int breakpointHitCounter; #endregion @@ -37,7 +40,7 @@ static BreakpointApiUtils() { // If this version of PowerShell does not support the new Breakpoint APIs introduced in PowerShell 7.0.0, // do nothing as this class will not get used. - if (!SupportsBreakpointApis) + if (!VersionUtils.IsPS7OrGreater) { return; } @@ -89,7 +92,7 @@ static BreakpointApiUtils() #endregion - #region Public Static Properties + #region Private Static Properties private static Func SetLineBreakpointDelegate => s_setLineBreakpointLazy.Value; @@ -103,9 +106,7 @@ static BreakpointApiUtils() #region Public Static Properties - // TODO: Try to compute this more dynamically. If we're launching a script in the PSIC, there are APIs are available in PS 5.1 and up. - // For now, only PS7 or greater gets this feature. - public static bool SupportsBreakpointApis => VersionUtils.IsPS7OrGreater; + public static bool SupportsBreakpointApis(IRunspaceInfo targetRunspace) => targetRunspace.PowerShellVersionDetails.Version >= s_minimumBreakpointApiPowerShellVersion; #endregion diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs index a79564a97..25942c7c9 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; using Microsoft.PowerShell.EditorServices.Utility; using System.Management.Automation; @@ -26,7 +26,7 @@ internal class DebuggerStoppedEventArgs /// public bool IsRemoteSession { - get { return this.RunspaceDetails.Location == RunspaceLocation.Remote; } + get => RunspaceInfo.RunspaceOrigin != RunspaceOrigin.Local; } /// @@ -37,7 +37,7 @@ public bool IsRemoteSession /// /// Gets the RunspaceDetails for the current runspace. /// - public RunspaceDetails RunspaceDetails { get; private set; } + public IRunspaceInfo RunspaceInfo { get; private set; } /// /// Gets the line number at which the debugger stopped execution. @@ -77,8 +77,8 @@ public int ColumnNumber /// The RunspaceDetails of the runspace which raised this event. public DebuggerStoppedEventArgs( DebuggerStopEventArgs originalEvent, - RunspaceDetails runspaceDetails) - : this(originalEvent, runspaceDetails, null) + IRunspaceInfo runspaceInfo) + : this(originalEvent, runspaceInfo, null) { } @@ -90,11 +90,11 @@ public DebuggerStoppedEventArgs( /// The local path of the remote script being debugged. public DebuggerStoppedEventArgs( DebuggerStopEventArgs originalEvent, - RunspaceDetails runspaceDetails, + IRunspaceInfo runspaceInfo, string localScriptPath) { Validate.IsNotNull(nameof(originalEvent), originalEvent); - Validate.IsNotNull(nameof(runspaceDetails), runspaceDetails); + Validate.IsNotNull(nameof(runspaceInfo), runspaceInfo); if (!string.IsNullOrEmpty(localScriptPath)) { @@ -107,7 +107,7 @@ public DebuggerStoppedEventArgs( } this.OriginalEvent = originalEvent; - this.RunspaceDetails = runspaceDetails; + this.RunspaceInfo = runspaceInfo; } #endregion diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetailsBase.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetailsBase.cs index badaff9bc..51e613f2c 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetailsBase.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetailsBase.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs index f612dd88c..7055a5a5f 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs @@ -10,6 +10,7 @@ using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Models; @@ -19,21 +20,30 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { internal class BreakpointHandlers : ISetFunctionBreakpointsHandler, ISetBreakpointsHandler, ISetExceptionBreakpointsHandler { + private static readonly string[] s_supportedDebugFileExtensions = new[] + { + ".ps1", + ".psm1" + }; + private readonly ILogger _logger; private readonly DebugService _debugService; private readonly DebugStateService _debugStateService; private readonly WorkspaceService _workspaceService; + private readonly IRunspaceContext _runspaceContext; public BreakpointHandlers( ILoggerFactory loggerFactory, DebugService debugService, DebugStateService debugStateService, - WorkspaceService workspaceService) + WorkspaceService workspaceService, + IRunspaceContext runspaceContext) { _logger = loggerFactory.CreateLogger(); _debugService = debugService; _debugStateService = debugStateService; _workspaceService = workspaceService; + _runspaceContext = runspaceContext; } public async Task Handle(SetBreakpointsArguments request, CancellationToken cancellationToken) @@ -53,10 +63,7 @@ public async Task Handle(SetBreakpointsArguments request } // Verify source file is a PowerShell script file. - string fileExtension = Path.GetExtension(scriptFile?.FilePath ?? "")?.ToLower(); - bool isUntitledPath = ScriptFile.IsUntitledPath(request.Source.Path); - if ((!isUntitledPath && fileExtension != ".ps1" && fileExtension != ".psm1") || - (!BreakpointApiUtils.SupportsBreakpointApis && isUntitledPath)) + if (!IsFileSupportedForBreakpoints(request.Source.Path, scriptFile)) { _logger.LogWarning( $"Attempted to set breakpoints on a non-PowerShell file: {request.Source.Path}"); @@ -182,5 +189,22 @@ public Task Handle(SetExceptionBreakpointsArgum return Task.FromResult(new SetExceptionBreakpointsResponse()); } + + private bool IsFileSupportedForBreakpoints(string requestedPath, ScriptFile resolvedScriptFile) + { + // PowerShell 7 and above support breakpoints in untitled files + if (ScriptFile.IsUntitledPath(requestedPath)) + { + return BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace); + } + + if (string.IsNullOrEmpty(resolvedScriptFile?.FilePath)) + { + return false; + } + + string fileExtension = Path.GetExtension(resolvedScriptFile.FilePath); + return s_supportedDebugFileExtensions.Contains(fileExtension, StringComparer.OrdinalIgnoreCase); + } } } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index abe43e3ac..7750585c0 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -1,47 +1,69 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Management.Automation; -using System.Management.Automation.Language; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using OmniSharp.Extensions.DebugAdapter.Protocol.Server; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Handlers { internal class ConfigurationDoneHandler : IConfigurationDoneHandler { + private static readonly PowerShellExecutionOptions s_debuggerExecutionOptions = new() + { + MustRunInForeground = true, + WriteInputToHost = true, + WriteOutputToHost = true, + ThrowOnError = false, + AddToHistory = true, + }; + private readonly ILogger _logger; private readonly IDebugAdapterServerFacade _debugAdapterServer; private readonly DebugService _debugService; private readonly DebugStateService _debugStateService; private readonly DebugEventHandlerService _debugEventHandlerService; - private readonly PowerShellContextService _powerShellContextService; + private readonly IInternalPowerShellExecutionService _executionService; private readonly WorkspaceService _workspaceService; + private readonly IPowerShellDebugContext _debugContext; + private readonly IRunspaceContext _runspaceContext; + public ConfigurationDoneHandler( ILoggerFactory loggerFactory, IDebugAdapterServerFacade debugAdapterServer, DebugService debugService, DebugStateService debugStateService, DebugEventHandlerService debugEventHandlerService, - PowerShellContextService powerShellContextService, - WorkspaceService workspaceService) + IInternalPowerShellExecutionService executionService, + WorkspaceService workspaceService, + IPowerShellDebugContext debugContext, + IRunspaceContext runspaceContext) { _logger = loggerFactory.CreateLogger(); _debugAdapterServer = debugAdapterServer; _debugService = debugService; _debugStateService = debugStateService; _debugEventHandlerService = debugEventHandlerService; - _powerShellContextService = powerShellContextService; + _executionService = executionService; _workspaceService = workspaceService; + _debugContext = debugContext; + _runspaceContext = runspaceContext; } public Task Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken) @@ -52,21 +74,13 @@ public Task Handle(ConfigurationDoneArguments request { // If this is a debug-only session, we need to start // the command loop manually - _powerShellContextService.ConsoleReader.StartCommandLoop(); + //_powerShellContextService.ConsoleReader.StartCommandLoop(); } if (!string.IsNullOrEmpty(_debugStateService.ScriptToLaunch)) { - if (_powerShellContextService.SessionState == PowerShellContextState.Ready) - { - // Configuration is done, launch the script - var nonAwaitedTask = LaunchScriptAsync(_debugStateService.ScriptToLaunch) - .ConfigureAwait(continueOnCapturedContext: false); - } - else - { - _logger.LogTrace("configurationDone request called after script was already launched, skipping it."); - } + LaunchScriptAsync(_debugStateService.ScriptToLaunch) + .HandleErrorsAsync(_logger); } if (_debugStateService.IsInteractiveDebugSession) @@ -83,7 +97,7 @@ public Task Handle(ConfigurationDoneArguments request { // If this is an interactive session and there's a pending breakpoint that has not been propagated through // the debug service, fire the debug service's OnDebuggerStop event. - _debugService.OnDebuggerStopAsync(null, _powerShellContextService.CurrentDebuggerStopEventArgs); + _debugService.OnDebuggerStopAsync(null, _debugContext.LastStopEventArgs); } } } @@ -98,7 +112,7 @@ private async Task LaunchScriptAsync(string scriptToLaunch) { ScriptFile untitledScript = _workspaceService.GetFile(scriptToLaunch); - if (BreakpointApiUtils.SupportsBreakpointApis) + if (BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace)) { // Parse untitled files with their `Untitled:` URI as the file name which will cache the URI & contents within the PowerShell parser. // By doing this, we light up the ability to debug Untitled files with breakpoints. @@ -108,24 +122,60 @@ private async Task LaunchScriptAsync(string scriptToLaunch) // This seems to be the simplest way to invoke a script block (which contains breakpoint information) via the PowerShell API. var cmd = new PSCommand().AddScript(". $args[0]").AddArgument(ast.GetScriptBlock()); - await _powerShellContextService - .ExecuteCommandAsync(cmd, sendOutputToHost: true, sendErrorToHost:true) + await _executionService + .ExecutePSCommandAsync(cmd, CancellationToken.None, s_debuggerExecutionOptions) .ConfigureAwait(false); } else { - await _powerShellContextService - .ExecuteScriptStringAsync(untitledScript.Contents, writeInputToHost: true, writeOutputToHost: true) + await _executionService + .ExecutePSCommandAsync( + new PSCommand().AddScript(untitledScript.Contents), + CancellationToken.None, + s_debuggerExecutionOptions) .ConfigureAwait(false); } } else { - await _powerShellContextService - .ExecuteScriptWithArgsAsync(scriptToLaunch, _debugStateService.Arguments, writeInputToHost: true).ConfigureAwait(false); + await _executionService + .ExecutePSCommandAsync( + BuildPSCommandFromArguments(scriptToLaunch, _debugStateService.Arguments), + CancellationToken.None, + s_debuggerExecutionOptions) + .ConfigureAwait(false); } _debugAdapterServer.SendNotification(EventNames.Terminated); } + + private static PSCommand BuildPSCommandFromArguments(string command, IReadOnlyList arguments) + { + if (arguments is null or { Count: 0 }) + { + return new PSCommand().AddCommand(command); + } + + // We are forced to use a hack here so that we can reuse PowerShell's parameter binding + var sb = new StringBuilder() + .Append("& ") + .Append(StringEscaping.SingleQuoteAndEscape(command)); + + foreach (string arg in arguments) + { + sb.Append(' '); + + if (StringEscaping.PowerShellArgumentNeedsEscaping(arg)) + { + sb.Append(StringEscaping.SingleQuoteAndEscape(arg)); + } + else + { + sb.Append(arg); + } + } + + return new PSCommand().AddScript(sb.ToString()); + } } } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs index d4f100188..0e1a9ce5f 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs @@ -2,27 +2,35 @@ // Licensed under the MIT License. using System; +using System.Management.Automation; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Handlers { internal class DebugEvaluateHandler : IEvaluateHandler { private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; + private readonly IPowerShellDebugContext _debugContext; + private readonly IInternalPowerShellExecutionService _executionService; private readonly DebugService _debugService; public DebugEvaluateHandler( ILoggerFactory factory, - PowerShellContextService powerShellContextService, + IPowerShellDebugContext debugContext, + IInternalPowerShellExecutionService executionService, DebugService debugService) { _logger = factory.CreateLogger(); - _powerShellContextService = powerShellContextService; + _debugContext = debugContext; + _executionService = executionService; _debugService = debugService; } @@ -39,10 +47,10 @@ public async Task Handle(EvaluateRequestArguments request, if (isFromRepl) { - var notAwaited = - _powerShellContextService - .ExecuteScriptStringAsync(request.Expression, false, true) - .ConfigureAwait(false); + _executionService.ExecutePSCommandAsync( + new PSCommand().AddScript(request.Expression), + CancellationToken.None, + new PowerShellExecutionOptions { WriteOutputToHost = true, ThrowOnError = false, AddToHistory = true }).HandleErrorsAsync(_logger); } else { @@ -50,7 +58,7 @@ public async Task Handle(EvaluateRequestArguments request, // VS Code might send this request after the debugger // has been resumed, return an empty result in this case. - if (_powerShellContextService.IsDebuggerStopped) + if (_debugContext.IsStopped) { // First check to see if the watch expression refers to a naked variable reference. result = diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs index 4f2b8e85b..704312d48 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs @@ -2,13 +2,15 @@ // Licensed under the MIT License. using System; +using System.Management.Automation; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Server; using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; namespace Microsoft.PowerShell.EditorServices.Handlers @@ -16,23 +18,26 @@ namespace Microsoft.PowerShell.EditorServices.Handlers internal class DisconnectHandler : IDisconnectHandler { private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; + private readonly IInternalPowerShellExecutionService _executionService; private readonly DebugService _debugService; private readonly DebugStateService _debugStateService; private readonly DebugEventHandlerService _debugEventHandlerService; private readonly PsesDebugServer _psesDebugServer; + private readonly IRunspaceContext _runspaceContext; public DisconnectHandler( ILoggerFactory factory, PsesDebugServer psesDebugServer, - PowerShellContextService powerShellContextService, + IRunspaceContext runspaceContext, + IInternalPowerShellExecutionService executionService, DebugService debugService, DebugStateService debugStateService, DebugEventHandlerService debugEventHandlerService) { _logger = factory.CreateLogger(); _psesDebugServer = psesDebugServer; - _powerShellContextService = powerShellContextService; + _runspaceContext = runspaceContext; + _executionService = executionService; _debugService = debugService; _debugStateService = debugStateService; _debugEventHandlerService = debugEventHandlerService; @@ -40,25 +45,34 @@ public DisconnectHandler( public async Task Handle(DisconnectArguments request, CancellationToken cancellationToken) { + // TODO: We need to sort out the proper order of operations here. + // Currently we just tear things down in some order without really checking what the debugger is doing. + // We should instead ensure that the debugger is in some valid state, lock it and then tear things down + _debugEventHandlerService.UnregisterEventHandlers(); - if (_debugStateService.ExecutionCompleted == false) + + if (!_debugStateService.ExecutionCompleted) { _debugStateService.ExecutionCompleted = true; - _powerShellContextService.AbortExecution(shouldAbortDebugSession: true); + _debugService.Abort(); if (_debugStateService.IsInteractiveDebugSession && _debugStateService.IsAttachSession) { // Pop the sessions - if (_powerShellContextService.CurrentRunspace.Context == RunspaceContext.EnteredProcess) + if (_runspaceContext.CurrentRunspace.RunspaceOrigin == RunspaceOrigin.EnteredProcess) { try { - await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSHostProcess").ConfigureAwait(false); + await _executionService.ExecutePSCommandAsync( + new PSCommand().AddCommand("Exit-PSHostProcess"), + CancellationToken.None).ConfigureAwait(false); if (_debugStateService.IsRemoteAttach && - _powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Remote) + _runspaceContext.CurrentRunspace.RunspaceOrigin == RunspaceOrigin.EnteredProcess) { - await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSSession").ConfigureAwait(false); + await _executionService.ExecutePSCommandAsync( + new PSCommand().AddCommand("Exit-PSSession"), + CancellationToken.None).ConfigureAwait(false); } } catch (Exception e) @@ -67,7 +81,6 @@ public async Task Handle(DisconnectArguments request, Cancel } } } - _debugService.IsClientAttached = false; } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index 040bb4a3f..c2d0e34f8 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -5,18 +5,19 @@ using System.Collections.Generic; using System.IO; using System.Management.Automation; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.JsonRpc; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using OmniSharp.Extensions.DebugAdapter.Protocol.Server; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -87,7 +88,8 @@ internal class LaunchAndAttachHandler : ILaunchHandler _logger; private readonly BreakpointService _breakpointService; private readonly DebugService _debugService; - private readonly PowerShellContextService _powerShellContextService; + private readonly IRunspaceContext _runspaceContext; + private readonly IInternalPowerShellExecutionService _executionService; private readonly DebugStateService _debugStateService; private readonly DebugEventHandlerService _debugEventHandlerService; private readonly IDebugAdapterServerFacade _debugAdapterServer; @@ -99,8 +101,9 @@ public LaunchAndAttachHandler( BreakpointService breakpointService, DebugEventHandlerService debugEventHandlerService, DebugService debugService, + IRunspaceContext runspaceContext, + IInternalPowerShellExecutionService executionService, DebugStateService debugStateService, - PowerShellContextService powerShellContextService, RemoteFileManagerService remoteFileManagerService) { _logger = factory.CreateLogger(); @@ -108,9 +111,10 @@ public LaunchAndAttachHandler( _breakpointService = breakpointService; _debugEventHandlerService = debugEventHandlerService; _debugService = debugService; + _runspaceContext = runspaceContext; + _executionService = executionService; _debugStateService = debugStateService; _debugStateService.ServerStarted = new TaskCompletionSource(); - _powerShellContextService = powerShellContextService; _remoteFileManagerService = remoteFileManagerService; } @@ -119,8 +123,8 @@ public async Task Handle(PsesLaunchRequestArguments request, Can _debugEventHandlerService.RegisterEventHandlers(); // Determine whether or not the working directory should be set in the PowerShellContext. - if ((_powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Local) && - !_debugService.IsDebuggerStopped) + if (_runspaceContext.CurrentRunspace.RunspaceOrigin == RunspaceOrigin.Local + && !_debugService.IsDebuggerStopped) { // Get the working directory that was passed via the debug config // (either via launch.json or generated via no-config debug). @@ -157,24 +161,23 @@ public async Task Handle(PsesLaunchRequestArguments request, Can // the working dir should not be changed. if (!string.IsNullOrEmpty(workingDir)) { - await _powerShellContextService.SetWorkingDirectoryAsync(workingDir, isPathAlreadyEscaped: false).ConfigureAwait(false); + var setDirCommand = new PSCommand().AddCommand("Set-Location").AddParameter("LiteralPath", workingDir); + await _executionService.ExecutePSCommandAsync(setDirCommand, cancellationToken).ConfigureAwait(false); } _logger.LogTrace("Working dir " + (string.IsNullOrEmpty(workingDir) ? "not set." : $"set to '{workingDir}'")); } // Prepare arguments to the script - if specified - string arguments = null; if (request.Args?.Length > 0) { - arguments = string.Join(" ", request.Args); - _logger.LogTrace("Script arguments are: " + arguments); + _logger.LogTrace($"Script arguments are: {string.Join(" ", request.Args)}"); } // Store the launch parameters so that they can be used later _debugStateService.NoDebug = request.NoDebug; _debugStateService.ScriptToLaunch = request.Script; - _debugStateService.Arguments = arguments; + _debugStateService.Arguments = request.Args; _debugStateService.IsUsingTempIntegratedConsole = request.CreateTemporaryIntegratedConsole; if (request.CreateTemporaryIntegratedConsole @@ -186,13 +189,13 @@ public async Task Handle(PsesLaunchRequestArguments request, Can // If the current session is remote, map the script path to the remote // machine if necessary - if (_debugStateService.ScriptToLaunch != null && - _powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Remote) + if (_debugStateService.ScriptToLaunch != null + && _runspaceContext.CurrentRunspace.IsOnRemoteMachine) { _debugStateService.ScriptToLaunch = _remoteFileManagerService.GetMappedPath( _debugStateService.ScriptToLaunch, - _powerShellContextService.CurrentRunspace); + _runspaceContext.CurrentRunspace); } // If no script is being launched, mark this as an interactive @@ -215,8 +218,7 @@ public async Task Handle(PsesAttachRequestArguments request, Can bool processIdIsSet = !string.IsNullOrEmpty(request.ProcessId) && request.ProcessId != "undefined"; bool customPipeNameIsSet = !string.IsNullOrEmpty(request.CustomPipeName) && request.CustomPipeName != "undefined"; - PowerShellVersionDetails runspaceVersion = - _powerShellContextService.CurrentRunspace.PowerShellVersion; + PowerShellVersionDetails runspaceVersion = _runspaceContext.CurrentRunspace.PowerShellVersionDetails; // If there are no host processes to attach to or the user cancels selection, we get a null for the process id. // This is not an error, just a request to stop the original "attach to" request. @@ -230,26 +232,30 @@ public async Task Handle(PsesAttachRequestArguments request, Can throw new RpcErrorException(0, "User aborted attach to PowerShell host process."); } - StringBuilder errorMessages = new StringBuilder(); - if (request.ComputerName != null) { if (runspaceVersion.Version.Major < 4) { throw new RpcErrorException(0, $"Remote sessions are only available with PowerShell 4 and higher (current session is {runspaceVersion.Version})."); } - else if (_powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Remote) + else if (_runspaceContext.CurrentRunspace.RunspaceOrigin != RunspaceOrigin.Local) { throw new RpcErrorException(0, "Cannot attach to a process in a remote session when already in a remote session."); } - await _powerShellContextService.ExecuteScriptStringAsync( - $"Enter-PSSession -ComputerName \"{request.ComputerName}\"", - errorMessages).ConfigureAwait(false); + var enterPSSessionCommand = new PSCommand() + .AddCommand("Enter-PSSession") + .AddParameter("ComputerName", request.ComputerName); - if (errorMessages.Length > 0) + try + { + await _executionService.ExecutePSCommandAsync(enterPSSessionCommand, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) { - throw new RpcErrorException(0, $"Could not establish remote session to computer '{request.ComputerName}'"); + string msg = $"Could not establish remote session to computer '{request.ComputerName}'"; + _logger.LogError(e, msg); + throw new RpcErrorException(0, msg); } _debugStateService.IsRemoteAttach = true; @@ -262,13 +268,19 @@ await _powerShellContextService.ExecuteScriptStringAsync( throw new RpcErrorException(0, $"Attaching to a process is only available with PowerShell 5 and higher (current session is {runspaceVersion.Version})."); } - await _powerShellContextService.ExecuteScriptStringAsync( - $"Enter-PSHostProcess -Id {processId}", - errorMessages).ConfigureAwait(false); + var enterPSHostProcessCommand = new PSCommand() + .AddCommand("Enter-PSHostProcess") + .AddParameter("Id", processId); - if (errorMessages.Length > 0) + try + { + await _executionService.ExecutePSCommandAsync(enterPSHostProcessCommand, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) { - throw new RpcErrorException(0, $"Could not attach to process '{processId}'"); + string msg = $"Could not attach to process '{processId}'"; + _logger.LogError(e, msg); + throw new RpcErrorException(0, msg); } } else if (customPipeNameIsSet) @@ -278,13 +290,19 @@ await _powerShellContextService.ExecuteScriptStringAsync( throw new RpcErrorException(0, $"Attaching to a process with CustomPipeName is only available with PowerShell 6.2 and higher (current session is {runspaceVersion.Version})."); } - await _powerShellContextService.ExecuteScriptStringAsync( - $"Enter-PSHostProcess -CustomPipeName {request.CustomPipeName}", - errorMessages).ConfigureAwait(false); + var enterPSHostProcessCommand = new PSCommand() + .AddCommand("Enter-PSHostProcess") + .AddParameter("CustomPipeName", request.CustomPipeName); - if (errorMessages.Length > 0) + try { - throw new RpcErrorException(0, $"Could not attach to process with CustomPipeName: '{request.CustomPipeName}'"); + await _executionService.ExecutePSCommandAsync(enterPSHostProcessCommand, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + string msg = $"Could not attach to process with CustomPipeName: '{request.CustomPipeName}'"; + _logger.LogError(e, msg); + throw new RpcErrorException(0, msg); } } else if (request.ProcessId != "current") @@ -300,22 +318,26 @@ await _powerShellContextService.ExecuteScriptStringAsync( // InitializedEvent will be sent as soon as the RunspaceChanged // event gets fired with the attached runspace. - string debugRunspaceCmd; + var debugRunspaceCmd = new PSCommand().AddCommand("Debug-Runspace"); if (request.RunspaceName != null) { - IEnumerable ids = await _powerShellContextService.ExecuteCommandAsync( - new PSCommand() - .AddCommand("Microsoft.PowerShell.Utility\\Get-Runspace") + var getRunspaceIdCommand = new PSCommand() + .AddCommand("Microsoft.PowerShell.Utility\\Get-Runspace") .AddParameter("Name", request.RunspaceName) - .AddCommand("Microsoft.PowerShell.Utility\\Select-Object") - .AddParameter("ExpandProperty", "Id"), cancellationToken: cancellationToken).ConfigureAwait(false); + .AddCommand("Microsoft.PowerShell.Utility\\Select-Object") + .AddParameter("ExpandProperty", "Id"); + IEnumerable ids = await _executionService.ExecutePSCommandAsync(getRunspaceIdCommand, cancellationToken).ConfigureAwait(false); foreach (var id in ids) { _debugStateService.RunspaceId = id; break; + + // TODO: If we don't end up setting this, we should throw } - debugRunspaceCmd = $"\nDebug-Runspace -Name '{request.RunspaceName}'"; + + // TODO: We have the ID, why not just use that? + debugRunspaceCmd.AddParameter("Name", request.RunspaceName); } else if (request.RunspaceId != null) { @@ -329,21 +351,21 @@ await _powerShellContextService.ExecuteScriptStringAsync( _debugStateService.RunspaceId = runspaceId; - debugRunspaceCmd = $"\nDebug-Runspace -Id {runspaceId}"; + debugRunspaceCmd.AddParameter("Id", runspaceId); } else { _debugStateService.RunspaceId = 1; - debugRunspaceCmd = "\nDebug-Runspace -Id 1"; + debugRunspaceCmd.AddParameter("Id", 1); } // Clear any existing breakpoints before proceeding await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false); _debugStateService.WaitingForAttach = true; - Task nonAwaitedTask = _powerShellContextService - .ExecuteScriptStringAsync(debugRunspaceCmd) + Task nonAwaitedTask = _executionService + .ExecutePSCommandAsync(debugRunspaceCmd, CancellationToken.None) .ContinueWith(OnExecutionCompletedAsync); if (runspaceVersion.Version.Major >= 7) @@ -393,24 +415,24 @@ private async Task OnExecutionCompletedAsync(Task executeTask) if (_debugStateService.IsAttachSession) { - // Pop the sessions - if (_powerShellContextService.CurrentRunspace.Context == RunspaceContext.EnteredProcess) - { - try - { - await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSHostProcess").ConfigureAwait(false); - - if (_debugStateService.IsRemoteAttach && - _powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Remote) - { - await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSSession").ConfigureAwait(false); - } - } - catch (Exception e) - { - _logger.LogException("Caught exception while popping attached process after debugging", e); - } - } + // Pop the sessions + if (_runspaceContext.CurrentRunspace.RunspaceOrigin == RunspaceOrigin.EnteredProcess) + { + try + { + await _executionService.ExecutePSCommandAsync(new PSCommand().AddCommand("Exit-PSHostProcess"), CancellationToken.None).ConfigureAwait(false); + + if (_debugStateService.IsRemoteAttach && + _runspaceContext.CurrentRunspace.RunspaceOrigin != RunspaceOrigin.Local) + { + await _executionService.ExecutePSCommandAsync(new PSCommand().AddCommand("Exit-PSSession"), CancellationToken.None).ConfigureAwait(false); + } + } + catch (Exception e) + { + _logger.LogException("Caught exception while popping attached process after debugging", e); + } + } } _debugService.IsClientAttached = false; diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/SetVariableHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/SetVariableHandler.cs index 5ce610ead..2eeb2fef6 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/SetVariableHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/SetVariableHandler.cs @@ -2,15 +2,12 @@ // Licensed under the MIT License. using System; -using System.Linq; using System.Management.Automation; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Utility; -using OmniSharp.Extensions.DebugAdapter.Protocol.Models; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using OmniSharp.Extensions.JsonRpc; diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/VariablesHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/VariablesHandler.cs index 494bfc2c0..a97232c06 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/VariablesHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/VariablesHandler.cs @@ -9,7 +9,6 @@ using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using Microsoft.PowerShell.EditorServices.Utility; -using OmniSharp.Extensions.DebugAdapter.Protocol.Models; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; namespace Microsoft.PowerShell.EditorServices.Handlers diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoiceDetails.cs b/src/PowerShellEditorServices/Services/Extension/ChoiceDetails.cs similarity index 98% rename from src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoiceDetails.cs rename to src/PowerShellEditorServices/Services/Extension/ChoiceDetails.cs index 0efc14fbc..fe82210fd 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoiceDetails.cs +++ b/src/PowerShellEditorServices/Services/Extension/ChoiceDetails.cs @@ -4,7 +4,7 @@ using System; using System.Management.Automation.Host; -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.Extension { /// /// Contains the details about a choice that should be displayed diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs similarity index 94% rename from src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs rename to src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs index 1c1775562..409069d4b 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs @@ -2,30 +2,36 @@ // Licensed under the MIT License. using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Services +namespace Microsoft.PowerShell.EditorServices.Services.Extension { internal class EditorOperationsService : IEditorOperations { private const bool DefaultPreviewSetting = true; + private readonly PsesInternalHost _psesHost; private readonly WorkspaceService _workspaceService; - private readonly PowerShellContextService _powerShellContextService; + private readonly ILanguageServerFacade _languageServer; + private readonly IInternalPowerShellExecutionService _executionService; + public EditorOperationsService( + PsesInternalHost psesHost, WorkspaceService workspaceService, - PowerShellContextService powerShellContextService, + IInternalPowerShellExecutionService executionService, ILanguageServerFacade languageServer) { + _psesHost = psesHost; _workspaceService = workspaceService; - _powerShellContextService = powerShellContextService; + _executionService = executionService; _languageServer = languageServer; } @@ -272,7 +278,7 @@ private bool TestHasLanguageServer(bool warnUser = true) if (warnUser) { - _powerShellContextService.ExternalHost.UI.WriteWarningLine( + _psesHost.UI.WriteWarningLine( "Editor operations are not supported in temporary consoles. Re-run the command in the main PowerShell Intergrated Console."); } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs b/src/PowerShellEditorServices/Services/Extension/ExtensionService.cs similarity index 78% rename from src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs rename to src/PowerShellEditorServices/Services/Extension/ExtensionService.cs index db9d6ced9..6e2793f55 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs +++ b/src/PowerShellEditorServices/Services/Extension/ExtensionService.cs @@ -4,13 +4,15 @@ using System; using System.Collections.Generic; using System.Management.Automation; +using System.Threading; using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Services +namespace Microsoft.PowerShell.EditorServices.Services.Extension { /// /// Provides a high-level service which enables PowerShell scripts @@ -18,6 +20,8 @@ namespace Microsoft.PowerShell.EditorServices.Services /// internal sealed class ExtensionService { + public const string PSEditorVariableName = "psEditor"; + #region Fields private readonly Dictionary editorCommands = @@ -25,6 +29,8 @@ internal sealed class ExtensionService private readonly ILanguageServerFacade _languageServer; + private IdempotentLatch _initializedLatch = new(); + #endregion #region Properties @@ -44,7 +50,7 @@ internal sealed class ExtensionService /// /// Gets the PowerShellContext in which extension code will be executed. /// - internal PowerShellContextService PowerShellContext { get; private set; } + internal IInternalPowerShellExecutionService ExecutionService { get; private set; } #endregion @@ -54,11 +60,29 @@ internal sealed class ExtensionService /// 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. - internal ExtensionService(PowerShellContextService powerShellContext, ILanguageServerFacade languageServer) + /// The PSES language server instance. + /// Services for dependency injection into the editor object. + /// Options object to configure the editor. + /// PowerShell execution service to run PowerShell execution requests. + internal ExtensionService( + ILanguageServerFacade languageServer, + IServiceProvider serviceProvider, + IEditorOperations editorOperations, + IInternalPowerShellExecutionService executionService) { - this.PowerShellContext = powerShellContext; + ExecutionService = executionService; _languageServer = languageServer; + + EditorObject = + new EditorObject( + serviceProvider, + this, + editorOperations); + + // Attach to ExtensionService events + CommandAdded += ExtensionService_ExtensionAddedAsync; + CommandUpdated += ExtensionService_ExtensionUpdatedAsync; + CommandRemoved += ExtensionService_ExtensionRemovedAsync; } #endregion @@ -71,32 +95,25 @@ internal ExtensionService(PowerShellContextService powerShellContext, ILanguageS /// /// An IEditorOperations implementation. /// A Task that can be awaited for completion. - internal async Task InitializeAsync( - IServiceProvider serviceProvider, - IEditorOperations editorOperations) + internal Task InitializeAsync() { - // Attach to ExtensionService events - this.CommandAdded += ExtensionService_ExtensionAddedAsync; - this.CommandUpdated += ExtensionService_ExtensionUpdatedAsync; - this.CommandRemoved += ExtensionService_ExtensionRemovedAsync; - - this.EditorObject = - new EditorObject( - serviceProvider, - this, - editorOperations); + if (!_initializedLatch.TryEnter()) + { + return Task.CompletedTask; + } // Assign the new EditorObject to be the static instance available to binary APIs - this.EditorObject.SetAsStaticInstance(); + EditorObject.SetAsStaticInstance(); // Register the editor object in the runspace - PSCommand variableCommand = new PSCommand(); - using (RunspaceHandle handle = await this.PowerShellContext.GetRunspaceHandleAsync().ConfigureAwait(false)) - { - handle.Runspace.SessionStateProxy.PSVariable.Set( - "psEditor", - this.EditorObject); - } + return ExecutionService.ExecuteDelegateAsync( + $"Create ${PSEditorVariableName} object", + ExecutionOptions.Default, + (pwsh, cancellationToken) => + { + pwsh.Runspace.SessionStateProxy.PSVariable.Set(PSEditorVariableName, EditorObject); + }, + CancellationToken.None); } /// @@ -115,10 +132,11 @@ public async Task InvokeCommandAsync(string commandName, EditorContext editorCon executeCommand.AddParameter("ScriptBlock", editorCommand.ScriptBlock); executeCommand.AddParameter("ArgumentList", new object[] { editorContext }); - await this.PowerShellContext.ExecuteCommandAsync( + await ExecutionService.ExecutePSCommandAsync( executeCommand, - sendOutputToHost: !editorCommand.SuppressOutput, - sendErrorToHost: true).ConfigureAwait(false); + CancellationToken.None, + new PowerShellExecutionOptions { WriteOutputToHost = !editorCommand.SuppressOutput, ThrowOnError = false, AddToHistory = !editorCommand.SuppressOutput }) + .ConfigureAwait(false); } else { diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs b/src/PowerShellEditorServices/Services/Extension/Handlers/IInvokeExtensionCommandHandler.cs similarity index 93% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs rename to src/PowerShellEditorServices/Services/Extension/Handlers/IInvokeExtensionCommandHandler.cs index b6d4fb411..325b74073 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs +++ b/src/PowerShellEditorServices/Services/Extension/Handlers/IInvokeExtensionCommandHandler.cs @@ -5,7 +5,7 @@ using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Handlers +namespace Microsoft.PowerShell.EditorServices.Services.Extension { [Serial, Method("powerShell/invokeExtensionCommand")] internal interface IInvokeExtensionCommandHandler : IJsonRpcNotificationHandler { } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs b/src/PowerShellEditorServices/Services/Extension/Handlers/InvokeExtensionCommandHandler.cs similarity index 93% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs rename to src/PowerShellEditorServices/Services/Extension/Handlers/InvokeExtensionCommandHandler.cs index b533787c4..1a50779cc 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs +++ b/src/PowerShellEditorServices/Services/Extension/Handlers/InvokeExtensionCommandHandler.cs @@ -5,10 +5,9 @@ using System.Threading.Tasks; using MediatR; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Extensions; -namespace Microsoft.PowerShell.EditorServices.Handlers +namespace Microsoft.PowerShell.EditorServices.Services.Extension { internal class InvokeExtensionCommandHandler : IInvokeExtensionCommandHandler { diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptEvents.cs b/src/PowerShellEditorServices/Services/Extension/PromptEvents.cs similarity index 93% rename from src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptEvents.cs rename to src/PowerShellEditorServices/Services/Extension/PromptEvents.cs index 3a4314ca0..9b59e0ba6 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptEvents.cs +++ b/src/PowerShellEditorServices/Services/Extension/PromptEvents.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.Extension { internal class ShowChoicePromptRequest { diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/ColorConfiguration.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/ColorConfiguration.cs new file mode 100644 index 000000000..1bd80051c --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/ColorConfiguration.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + internal class ColorConfiguration + { + public ConsoleColor ForegroundColor { get; set; } + + public ConsoleColor BackgroundColor { get; set; } + + public ConsoleColor FormatAccentColor { get; set; } + + public ConsoleColor ErrorAccentColor { get; set; } + + public ConsoleColor ErrorForegroundColor { get; set; } + + public ConsoleColor ErrorBackgroundColor { get; set; } + + public ConsoleColor WarningForegroundColor { get; set; } + + public ConsoleColor WarningBackgroundColor { get; set; } + + public ConsoleColor DebugForegroundColor { get; set; } + + public ConsoleColor DebugBackgroundColor { get; set; } + + public ConsoleColor VerboseForegroundColor { get; set; } + + public ConsoleColor VerboseBackgroundColor { get; set; } + + public ConsoleColor ProgressForegroundColor { get; set; } + + public ConsoleColor ProgressBackgroundColor { get; set; } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleProxy.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleProxy.cs similarity index 99% rename from src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleProxy.cs rename to src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleProxy.cs index 234d5b4f3..c73e4f916 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleProxy.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleProxy.cs @@ -1,12 +1,12 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. using System; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { /// /// Provides asynchronous implementations of the API's as well as diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Console/IConsoleOperations.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/IConsoleOperations.cs similarity index 97% rename from src/PowerShellEditorServices/Services/PowerShellContext/Console/IConsoleOperations.cs rename to src/PowerShellEditorServices/Services/PowerShell/Console/IConsoleOperations.cs index f2d431692..4972f23a0 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/IConsoleOperations.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/IConsoleOperations.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// 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.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { /// /// Provides platform specific console utilities. diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs new file mode 100644 index 000000000..1233df7b0 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Security; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + internal interface IReadLine + { + string ReadLine(CancellationToken cancellationToken); + + SecureString ReadSecureLine(CancellationToken cancellationToken); + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs new file mode 100644 index 000000000..a29fe17f2 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -0,0 +1,539 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + using System; + + internal class LegacyReadLine : TerminalReadLine + { + private readonly PsesInternalHost _psesHost; + + private readonly Task[] _readKeyTasks; + + private readonly Func _readKeyFunc; + + private readonly Action _onIdleAction; + + public LegacyReadLine( + PsesInternalHost psesHost, + Func readKeyFunc, + Action onIdleAction) + { + _psesHost = psesHost; + _readKeyTasks = new Task[2]; + _readKeyFunc = readKeyFunc; + _onIdleAction = onIdleAction; + } + + public override string ReadLine(CancellationToken cancellationToken) + { + string inputBeforeCompletion = null; + string inputAfterCompletion = null; + CommandCompletion currentCompletion = null; + + int historyIndex = -1; + IReadOnlyList currentHistory = null; + + StringBuilder inputLine = new StringBuilder(); + + int initialCursorCol = ConsoleProxy.GetCursorLeft(cancellationToken); + int initialCursorRow = ConsoleProxy.GetCursorTop(cancellationToken); + + int currentCursorIndex = 0; + + Console.TreatControlCAsInput = true; + + try + { + while (!cancellationToken.IsCancellationRequested) + { + ConsoleKeyInfo keyInfo = ReadKey(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; + + switch (keyInfo.Key) + { + case ConsoleKey.Tab: + if (currentCompletion == null) + { + inputBeforeCompletion = inputLine.ToString(); + inputAfterCompletion = null; + + // TODO: This logic should be moved to AstOperations or similar! + + if (_psesHost.DebugContext.IsStopped) + { + PSCommand command = new PSCommand() + .AddCommand("TabExpansion2") + .AddParameter("InputScript", inputBeforeCompletion) + .AddParameter("CursorColumn", currentCursorIndex) + .AddParameter("Options", null); + + currentCompletion = _psesHost.InvokePSCommand(command, PowerShellExecutionOptions.Default, cancellationToken).FirstOrDefault(); + } + else + { + currentCompletion = _psesHost.InvokePSDelegate( + "Legacy readline inline command completion", + ExecutionOptions.Default, + (pwsh, cancellationToken) => CommandCompletion.CompleteInput(inputAfterCompletion, currentCursorIndex, options: null, pwsh), + cancellationToken); + + 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 = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + $"{completion.CompletionText}{inputAfterCompletion}", + currentCursorIndex, + insertIndex: currentCompletion.ReplacementIndex, + replaceLength: inputLine.Length - currentCompletion.ReplacementIndex, + finalCursorIndex: currentCompletion.ReplacementIndex + completion.CompletionText.Length); + } + + continue; + + case ConsoleKey.LeftArrow: + currentCompletion = null; + + if (currentCursorIndex > 0) + { + currentCursorIndex = + MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + currentCursorIndex - 1); + } + + continue; + + case ConsoleKey.Home: + currentCompletion = null; + + currentCursorIndex = + MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + 0); + + continue; + + case ConsoleKey.RightArrow: + currentCompletion = null; + + if (currentCursorIndex < inputLine.Length) + { + currentCursorIndex = + MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + currentCursorIndex + 1); + } + + continue; + + case ConsoleKey.End: + currentCompletion = null; + + currentCursorIndex = + MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + inputLine.Length); + + continue; + + case ConsoleKey.UpArrow: + currentCompletion = null; + + // TODO: Ctrl+Up should allow navigation in multi-line input + + if (currentHistory == null) + { + historyIndex = -1; + + PSCommand command = new PSCommand() + .AddCommand("Get-History"); + + currentHistory = _psesHost.InvokePSCommand(command, PowerShellExecutionOptions.Default, cancellationToken); + + if (currentHistory != null) + { + historyIndex = currentHistory.Count; + } + } + + if (currentHistory != null && currentHistory.Count > 0 && historyIndex > 0) + { + historyIndex--; + + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + (string)currentHistory[historyIndex].Properties["CommandLine"].Value, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + + continue; + + case ConsoleKey.DownArrow: + 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 = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + (string)currentHistory[historyIndex].Properties["CommandLine"].Value, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + else if (historyIndex == currentHistory.Count) + { + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + } + + continue; + + case ConsoleKey.Escape: + currentCompletion = null; + historyIndex = currentHistory != null ? currentHistory.Count : -1; + + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + + continue; + + case ConsoleKey.Backspace: + currentCompletion = null; + + if (currentCursorIndex > 0) + { + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: currentCursorIndex - 1, + replaceLength: 1, + finalCursorIndex: currentCursorIndex - 1); + } + + continue; + + case ConsoleKey.Delete: + currentCompletion = null; + + if (currentCursorIndex < inputLine.Length) + { + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + replaceLength: 1, + finalCursorIndex: currentCursorIndex); + } + + continue; + + case ConsoleKey.Enter: + string completedInput = inputLine.ToString(); + currentCompletion = null; + currentHistory = null; + + // TODO: Add line continuation support: + // - When shift+enter is pressed, or + // - When the parse indicates incomplete input + + //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; + + default: + if (keyInfo.IsCtrlC()) + { + throw new PipelineStoppedException(); + } + + // Normal character input + if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) + { + currentCompletion = null; + + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + keyInfo.KeyChar.ToString(), // TODO: Determine whether this should take culture into account + currentCursorIndex, + finalCursorIndex: currentCursorIndex + 1); + } + + continue; + } + } + } + catch (OperationCanceledException) + { + // We've broken out of the loop + } + finally + { + Console.TreatControlCAsInput = false; + } + + // If we break out of the loop without returning (because of the Enter key) + // then the readline has been aborted in some way and we should return nothing + return null; + } + + protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return _onIdleAction is null + ? InvokeReadKeyFunc() + : ReadKeyWithIdleSupport(cancellationToken); + } + finally + { + cancellationToken.ThrowIfCancellationRequested(); + } + } + + private ConsoleKeyInfo ReadKeyWithIdleSupport(CancellationToken cancellationToken) + { + // We run the readkey function on another thread so we can run an idle handler + Task readKeyTask = Task.Run(InvokeReadKeyFunc); + + _readKeyTasks[0] = readKeyTask; + _readKeyTasks[1] = Task.Delay(millisecondsDelay: 300, cancellationToken); + + while (true) + { + switch (Task.WaitAny(_readKeyTasks, cancellationToken)) + { + // ReadKey returned + case 0: + return readKeyTask.Result; + + // The idle timed out + case 1: + _onIdleAction(cancellationToken); + _readKeyTasks[1] = Task.Delay(millisecondsDelay: 300, cancellationToken); + continue; + } + } + } + + private ConsoleKeyInfo InvokeReadKeyFunc() + { + // intercept = false means we display the key in the console + return _readKeyFunc(/* intercept */ false); + } + + private static 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 + 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 MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + finalCursorIndex); + } + else + { + return inputLine.Length; + } + } + + private static int MoveCursorToIndex( + int promptStartCol, + int promptStartRow, + int consoleWidth, + int newCursorIndex) + { + CalculateCursorFromIndex( + promptStartCol, + promptStartRow, + consoleWidth, + newCursorIndex, + out int newCursorCol, + out int newCursorRow); + + Console.SetCursorPosition(newCursorCol, newCursorRow); + + return newCursorIndex; + } + private static 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; + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/PSReadLineProxy.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/PSReadLineProxy.cs new file mode 100644 index 000000000..a9b95b031 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/PSReadLineProxy.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; +using SMA = System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + using System.Management.Automation.Runspaces; + + 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 ReadLineMethodName = "ReadLine"; + + private const string ReadKeyOverrideFieldName = "_readKeyOverride"; + + private const string HandleIdleOverrideName = "_handleIdleOverride"; + + private const string VirtualTerminalTypeName = "Microsoft.PowerShell.Internal.VirtualTerminal"; + + private static readonly Type[] s_setKeyHandlerTypes = + { + typeof(string[]), + typeof(Action), + typeof(string), + typeof(string) + }; + + private static readonly Type[] s_addToHistoryTypes = { typeof(string) }; + + private static readonly string _psReadLineModulePath = Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName(typeof(PSReadLineProxy).Assembly.Location), + "..", + "..", + "..", + "PSReadLine")); + + private static readonly string ReadLineInitScript = $@" + [System.Diagnostics.DebuggerHidden()] + [System.Diagnostics.DebuggerStepThrough()] + param() + end {{ + $module = Get-Module -ListAvailable PSReadLine | + Where-Object {{ $_.Version -ge '2.2.1' }} | + Sort-Object -Descending Version | + Select-Object -First 1 + if (-not $module) {{ + Import-Module '{_psReadLineModulePath.Replace("'", "''")}' + return [Microsoft.PowerShell.PSConsoleReadLine] + }} + + Import-Module -ModuleInfo $module + return [Microsoft.PowerShell.PSConsoleReadLine] + }}"; + + public static PSReadLineProxy LoadAndCreate( + ILoggerFactory loggerFactory, + SMA.PowerShell pwsh) + { + Type psConsoleReadLineType = pwsh.AddScript(ReadLineInitScript).InvokeAndClear().FirstOrDefault(); + + RuntimeHelpers.RunClassConstructor(psConsoleReadLineType.TypeHandle); + + return new PSReadLineProxy(loggerFactory, psConsoleReadLineType); + } + + private readonly FieldInfo _readKeyOverrideField; + + private readonly FieldInfo _handleIdleOverrideField; + + private readonly ILogger _logger; + + public PSReadLineProxy( + ILoggerFactory loggerFactory, + Type psConsoleReadLine) + { + _logger = loggerFactory.CreateLogger(); + + ReadLine = (Func)psConsoleReadLine.GetMethod( + ReadLineMethodName, + new[] { typeof(Runspace), typeof(EngineIntrinsics), typeof(CancellationToken), typeof(bool?) }) + ?.CreateDelegate(typeof(Func)); + + AddToHistory = (Action)psConsoleReadLine.GetMethod( + AddToHistoryMethodName, + s_addToHistoryTypes) + ?.CreateDelegate(typeof(Action)); + + SetKeyHandler = (Action, string, string>)psConsoleReadLine.GetMethod( + SetKeyHandlerMethodName, + s_setKeyHandlerTypes) + ?.CreateDelegate(typeof(Action, string, string>)); + + _handleIdleOverrideField = psConsoleReadLine.GetField(HandleIdleOverrideName, BindingFlags.Static | BindingFlags.NonPublic); + + _readKeyOverrideField = psConsoleReadLine.GetTypeInfo().Assembly + .GetType(VirtualTerminalTypeName) + ?.GetField(ReadKeyOverrideFieldName, BindingFlags.Static | BindingFlags.NonPublic); + + if (_readKeyOverrideField is null) + { + throw NewInvalidPSReadLineVersionException( + FieldMemberType, + ReadKeyOverrideFieldName, + _logger); + } + + if (_handleIdleOverrideField is null) + { + throw NewInvalidPSReadLineVersionException( + FieldMemberType, + HandleIdleOverrideName, + _logger); + } + + if (ReadLine is null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + ReadLineMethodName, + _logger); + } + + if (SetKeyHandler is null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + SetKeyHandlerMethodName, + _logger); + } + + if (AddToHistory is null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + AddToHistoryMethodName, + _logger); + } + } + + internal Action AddToHistory { get; } + + internal Action, string, string> SetKeyHandler { get; } + + internal Action ForcePSEventHandling { get; } + + internal Func ReadLine { get; } + + internal void OverrideReadKey(Func readKeyFunc) + { + _readKeyOverrideField.SetValue(null, readKeyFunc); + } + + internal void OverrideIdleHandler(Action idleAction) + { + _handleIdleOverrideField.SetValue(null, idleAction); + } + + 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/Services/PowerShell/Console/PsrlReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs new file mode 100644 index 000000000..1bee46f76 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.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 Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using System.Management.Automation; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + using System; + + internal class PsrlReadLine : TerminalReadLine + { + private readonly PSReadLineProxy _psrlProxy; + + private readonly PsesInternalHost _psesHost; + + private readonly EngineIntrinsics _engineIntrinsics; + + #region Constructors + + public PsrlReadLine( + PSReadLineProxy psrlProxy, + PsesInternalHost psesHost, + EngineIntrinsics engineIntrinsics, + Func readKeyFunc, + Action onIdleAction) + { + _psrlProxy = psrlProxy; + _psesHost = psesHost; + _engineIntrinsics = engineIntrinsics; + _psrlProxy.OverrideReadKey(readKeyFunc); + _psrlProxy.OverrideIdleHandler(onIdleAction); + } + + #endregion + + #region Public Methods + + public override string ReadLine(CancellationToken cancellationToken) + { + return _psesHost.InvokeDelegate(representation: "ReadLine", new ExecutionOptions { MustRunInForeground = true }, InvokePSReadLine, cancellationToken); + } + + protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) + { + return ConsoleProxy.ReadKey(intercept: true, cancellationToken); + } + + #endregion + + #region Private Methods + + private string InvokePSReadLine(CancellationToken cancellationToken) + { + EngineIntrinsics engineIntrinsics = _psesHost.IsRunspacePushed ? null : _engineIntrinsics; + return _psrlProxy.ReadLine(_psesHost.Runspace, engineIntrinsics, cancellationToken, /* lastExecutionStatus */ null); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/ReadLineProvider.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/ReadLineProvider.cs new file mode 100644 index 000000000..c85c92097 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/ReadLineProvider.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + internal interface IReadLineProvider + { + IReadLine ReadLine { get; } + } + + internal class ReadLineProvider : IReadLineProvider + { + private readonly ILogger _logger; + + public ReadLineProvider(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public IReadLine ReadLine { get; private set; } + + public void OverrideReadLine(IReadLine readLine) + { + _logger.LogInformation($"ReadLine overridden with '{readLine.GetType()}'"); + ReadLine = readLine; + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs new file mode 100644 index 000000000..474c23f1c --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; +using System.Management.Automation; +using System.Security; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + using System; + + internal abstract class TerminalReadLine : IReadLine + { + public abstract string ReadLine(CancellationToken cancellationToken); + + protected abstract ConsoleKeyInfo ReadKey(CancellationToken cancellationToken); + + public SecureString ReadSecureLine(CancellationToken cancellationToken) + { + Console.TreatControlCAsInput = true; + int previousInputLength = 0; + SecureString secureString = new SecureString(); + try + { + bool enterPressed = false; + while (!enterPressed && !cancellationToken.IsCancellationRequested) + { + ConsoleKeyInfo keyInfo = ReadKey(cancellationToken); + + if (keyInfo.IsCtrlC()) + { + throw new PipelineStoppedException(); + } + + switch (keyInfo.Key) + { + case ConsoleKey.Enter: + // Stop the while loop so we can realign the cursor + // and then return the entered string + enterPressed = true; + continue; + + case ConsoleKey.Tab: + break; + + case ConsoleKey.Backspace: + if (secureString.Length > 0) + { + secureString.RemoveAt(secureString.Length - 1); + } + break; + + default: + if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) + { + secureString.AppendChar(keyInfo.KeyChar); + } + break; + } + + // 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 = ConsoleProxy.GetCursorTop(cancellationToken); + int col = ConsoleProxy.GetCursorLeft(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; + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Console/UnixConsoleOperations.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/UnixConsoleOperations.cs similarity index 97% rename from src/PowerShellEditorServices/Services/PowerShellContext/Console/UnixConsoleOperations.cs rename to src/PowerShellEditorServices/Services/PowerShell/Console/UnixConsoleOperations.cs index 18bb3063a..e1c39536d 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/UnixConsoleOperations.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/UnixConsoleOperations.cs @@ -1,5 +1,5 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. using System; using System.Threading; @@ -7,7 +7,7 @@ using Microsoft.PowerShell.EditorServices.Utility; using UnixConsoleEcho; -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { internal class UnixConsoleOperations : IConsoleOperations { @@ -264,7 +264,8 @@ private static Task SpinUntilKeyAvailableAsync(int millisecondsTimeout, Ca s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); return IsKeyAvailable(cancellationToken); }, - millisecondsTimeout), cancellationToken); + millisecondsTimeout), + cancellationToken); } private static bool IsKeyAvailable(CancellationToken cancellationToken) @@ -280,7 +281,7 @@ private static bool IsKeyAvailable(CancellationToken cancellationToken) } } - private async static Task IsKeyAvailableAsync(CancellationToken cancellationToken) + private static async Task IsKeyAvailableAsync(CancellationToken cancellationToken) { await s_stdInHandle.WaitAsync(cancellationToken).ConfigureAwait(false); try diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Console/WindowsConsoleOperations.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/WindowsConsoleOperations.cs similarity index 92% rename from src/PowerShellEditorServices/Services/PowerShellContext/Console/WindowsConsoleOperations.cs rename to src/PowerShellEditorServices/Services/PowerShell/Console/WindowsConsoleOperations.cs index 09ecd8368..20bc886ce 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/WindowsConsoleOperations.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/WindowsConsoleOperations.cs @@ -1,12 +1,12 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. using System; using System.Threading; using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { internal class WindowsConsoleOperations : IConsoleOperations { @@ -37,7 +37,7 @@ public async Task ReadKeyAsync(bool intercept, CancellationToken { if (_bufferedKey == null) { - _bufferedKey = await Task.Run(() => Console.ReadKey(intercept)).ConfigureAwait(false); + _bufferedKey = await Task.Run(() => System.Console.ReadKey(intercept)).ConfigureAwait(false); } return _bufferedKey.Value; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs new file mode 100644 index 000000000..9a31bf629 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; +using System; +using SMA = System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Context +{ + internal class PowerShellContextFrame : IDisposable + { + public static PowerShellContextFrame CreateForPowerShellInstance( + ILogger logger, + SMA.PowerShell pwsh, + PowerShellFrameType frameType, + string localComputerName) + { + var runspaceInfo = RunspaceInfo.CreateFromPowerShell(logger, pwsh, localComputerName); + return new PowerShellContextFrame(pwsh, runspaceInfo, frameType); + } + + private bool disposedValue; + + public PowerShellContextFrame(SMA.PowerShell powerShell, RunspaceInfo runspaceInfo, PowerShellFrameType frameType) + { + PowerShell = powerShell; + RunspaceInfo = runspaceInfo; + FrameType = frameType; + } + + public SMA.PowerShell PowerShell { get; } + + public RunspaceInfo RunspaceInfo { get; } + + public PowerShellFrameType FrameType { get; } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + PowerShell.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellFrameType.cs b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellFrameType.cs new file mode 100644 index 000000000..cb20ff8ff --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellFrameType.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Context +{ + [Flags] + internal enum PowerShellFrameType + { + Normal = 0x0, + Nested = 0x1, + Debug = 0x2, + Remote = 0x4, + NonInteractive = 0x8, + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellVersionDetails.cs b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs similarity index 88% rename from src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellVersionDetails.cs rename to src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs index 8a91f75bc..2b1980cc1 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellVersionDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs @@ -2,12 +2,15 @@ // Licensed under the MIT License. using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; using System; using System.Collections; -using System.Management.Automation.Runspaces; +using System.Linq; -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Context { + using System.Management.Automation; + /// /// Defines the possible enumeration values for the PowerShell process architecture. /// @@ -87,10 +90,9 @@ public PowerShellVersionDetails( /// /// 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) + public static PowerShellVersionDetails GetVersionDetails(ILogger logger, PowerShell pwsh) { Version powerShellVersion = new Version(5, 0); string versionString = null; @@ -99,7 +101,11 @@ public static PowerShellVersionDetails GetVersionDetails(Runspace runspace, ILog try { - var psVersionTable = PowerShellContextService.ExecuteScriptAndGetItem("$PSVersionTable", runspace, useLocalScope: true); + Hashtable psVersionTable = pwsh + .AddScript("$PSVersionTable", useLocalScope: true) + .InvokeAndClear() + .FirstOrDefault(); + if (psVersionTable != null) { var edition = psVersionTable["PSEdition"] as string; @@ -132,7 +138,13 @@ public static PowerShellVersionDetails GetVersionDetails(Runspace runspace, ILog versionString = powerShellVersion.ToString(); } - var arch = PowerShellContextService.ExecuteScriptAndGetItem("$env:PROCESSOR_ARCHITECTURE", runspace, useLocalScope: true); + var procArchCommand = new PSCommand().AddScript("$env:PROCESSOR_ARCHITECTURE", useLocalScope: true); + + string arch = pwsh + .AddScript("$env:PROCESSOR_ARCHITECTURE", useLocalScope: true) + .InvokeAndClear() + .FirstOrDefault(); + if (arch != null) { if (string.Equals(arch, "AMD64", StringComparison.CurrentCultureIgnoreCase)) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/DebuggerResumingEventArgs.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/DebuggerResumingEventArgs.cs new file mode 100644 index 000000000..59c8cc902 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/DebuggerResumingEventArgs.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging +{ + internal record DebuggerResumingEventArgs( + DebuggerResumeAction ResumeAction); +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs new file mode 100644 index 000000000..0ad294bd0 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Logging; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using System.Threading; +using SMA = System.Management.Automation; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging +{ + internal class DscBreakpointCapability + { + private string[] dscResourceRootPaths = Array.Empty(); + + private Dictionary breakpointsPerFile = + new Dictionary(); + + public async Task SetLineBreakpointsAsync( + IInternalPowerShellExecutionService executionService, + 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. + var dscCommand = new PSCommand().AddScript( + hashtableString.Length > 0 + ? $"Enable-DscDebug -Breakpoint {hashtableString}" + : "Disable-DscDebug"); + + await executionService.ExecutePSCommandAsync( + dscCommand, + CancellationToken.None) + .ConfigureAwait(false); + + // Verify all the breakpoints and return them + foreach (var breakpoint in breakpoints) + { + breakpoint.Verified = true; + } + + return breakpoints.ToArray(); + } + + public bool IsDscResourcePath(string scriptPath) + { + return dscResourceRootPaths.Any( + dscResourceRootPath => + scriptPath.StartsWith( + dscResourceRootPath, + StringComparison.CurrentCultureIgnoreCase)); + } + + public static Task GetDscCapabilityAsync( + ILogger logger, + IRunspaceInfo currentRunspace, + PsesInternalHost psesHost, + CancellationToken cancellationToken) + { + // DSC support is enabled only for Windows PowerShell. + if ((currentRunspace.PowerShellVersionDetails.Version.Major >= 6) && + (currentRunspace.RunspaceOrigin != RunspaceOrigin.DebuggedRunspace)) + { + return null; + } + + Func getDscBreakpointCapabilityFunc = (pwsh, cancellationToken) => + { + var invocationSettings = new PSInvocationSettings + { + AddToHistory = false, + ErrorActionPreference = ActionPreference.Stop + }; + + PSModuleInfo dscModule = null; + try + { + dscModule = pwsh.AddCommand("Import-Module") + .AddArgument(@"C:\Program Files\DesiredStateConfiguration\1.0.0.0\Modules\PSDesiredStateConfiguration\PSDesiredStateConfiguration.psd1") + .AddParameter("PassThru") + .InvokeAndClear(invocationSettings) + .FirstOrDefault(); + } + catch (RuntimeException e) + { + logger.LogException("Could not load the DSC module!", e); + } + + if (dscModule == null) + { + logger.LogTrace($"Side-by-side DSC module was not found."); + return null; + } + + logger.LogTrace("Side-by-side DSC module found, gathering DSC resource paths..."); + + // The module was loaded, add the breakpoint capability + var capability = new DscBreakpointCapability(); + + pwsh.AddCommand("Microsoft.PowerShell.Utility\\Write-Host") + .AddArgument("Gathering DSC resource paths, this may take a while...") + .InvokeAndClear(invocationSettings); + + Collection resourcePaths = null; + try + { + // Get the list of DSC resource paths + resourcePaths = pwsh.AddCommand("Get-DscResource") + .AddCommand("Select-Object") + .AddParameter("ExpandProperty", "ParentPath") + .InvokeAndClear(invocationSettings); + } + catch (CmdletInvocationException e) + { + logger.LogException("Get-DscResource failed!", e); + } + + if (resourcePaths == null) + { + logger.LogTrace($"No DSC resources found."); + return null; + } + + capability.dscResourceRootPaths = resourcePaths.ToArray(); + + logger.LogTrace($"DSC resources found: {resourcePaths.Count}"); + + return capability; + }; + + return psesHost.ExecuteDelegateAsync( + nameof(getDscBreakpointCapabilityFunc), + ExecutionOptions.Default, + getDscBreakpointCapabilityFunc, + cancellationToken); + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/IPowerShellDebugContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/IPowerShellDebugContext.cs new file mode 100644 index 000000000..bf78d80b1 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/IPowerShellDebugContext.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging +{ + internal interface IPowerShellDebugContext + { + bool IsStopped { get; } + + DebuggerStopEventArgs LastStopEventArgs { get; } + + public event Action DebuggerStopped; + + public event Action DebuggerResuming; + + public event Action BreakpointUpdated; + + void Continue(); + + void StepOver(); + + void StepInto(); + + void StepOut(); + + void BreakExecution(); + + void Abort(); + + Task GetDscBreakpointCapabilityAsync(CancellationToken cancellationToken); + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs new file mode 100644 index 000000000..fb6d11e6d --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Management.Automation; +using System.Threading; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging +{ + /// + /// Handles the state of the PowerShell debugger. + /// + /// + /// + /// Debugging through a PowerShell Host is implemented by registering a handler + /// for the event. + /// Registering that handler causes debug actions in PowerShell like Set-PSBreakpoint + /// and Wait-Debugger to drop into the debugger and trigger the handler. + /// The handler is passed a mutable object + /// and the debugger stop lasts for the duration of the handler call. + /// The handler sets the property + /// when after it returns, the PowerShell debugger uses that as the direction on how to proceed. + /// + /// + /// When we handle the event, + /// we drop into a nested debug prompt and execute things in the debugger with , + /// which enables debugger commands like l, c, s, etc. + /// saves the event args object in its state, + /// and when one of the debugger commands is used, the result returned is used to set + /// on the saved event args object so that when the event handler returns, the PowerShell debugger takes the correct action. + /// + /// + internal class PowerShellDebugContext : IPowerShellDebugContext + { + private readonly ILogger _logger; + + private readonly ILanguageServerFacade _languageServer; + + private readonly PsesInternalHost _psesHost; + + public PowerShellDebugContext( + ILoggerFactory loggerFactory, + ILanguageServerFacade languageServer, + PsesInternalHost psesHost) + { + _logger = loggerFactory.CreateLogger(); + _languageServer = languageServer; + _psesHost = psesHost; + } + + public bool IsStopped { get; private set; } + + public bool IsDebugServerActive { get; set; } + + public DebuggerStopEventArgs LastStopEventArgs { get; private set; } + + public event Action DebuggerStopped; + public event Action DebuggerResuming; + public event Action BreakpointUpdated; + + public Task GetDscBreakpointCapabilityAsync(CancellationToken cancellationToken) + { + return _psesHost.CurrentRunspace.GetDscBreakpointCapabilityAsync(_logger, _psesHost, cancellationToken); + } + + public void EnableDebugMode() + { + // This is required by the PowerShell API so that remote debugging works. + // Without it, a runspace may not have these options set and attempting to set breakpoints remotely can fail. + _psesHost.Runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); + } + + public void Abort() + { + SetDebugResuming(DebuggerResumeAction.Stop); + } + + public void BreakExecution() + { + _psesHost.Runspace.Debugger.SetDebuggerStepMode(enabled: true); + } + + public void Continue() + { + SetDebugResuming(DebuggerResumeAction.Continue); + } + + public void StepInto() + { + SetDebugResuming(DebuggerResumeAction.StepInto); + } + + public void StepOut() + { + SetDebugResuming(DebuggerResumeAction.StepOut); + } + + public void StepOver() + { + SetDebugResuming(DebuggerResumeAction.StepOver); + } + + public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction) + { + _psesHost.SetExit(); + + if (LastStopEventArgs is not null) + { + LastStopEventArgs.ResumeAction = debuggerResumeAction; + } + + // We need to tell whatever is happening right now in the debug prompt to wrap up so we can continue + _psesHost.CancelCurrentTask(); + } + + // This must be called AFTER the new PowerShell has been pushed + public void EnterDebugLoop(CancellationToken loopCancellationToken) + { + RaiseDebuggerStoppedEvent(); + } + + + // This must be called BEFORE the debug PowerShell has been popped + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "This method may acquire an implementation later, at which point it will need instance data")] + public void ExitDebugLoop() + { + } + + public void SetDebuggerStopped(DebuggerStopEventArgs debuggerStopEventArgs) + { + IsStopped = true; + LastStopEventArgs = debuggerStopEventArgs; + } + + public void SetDebuggerResumed() + { + IsStopped = false; + } + + public void ProcessDebuggerResult(DebuggerCommandResults debuggerResult) + { + if (debuggerResult.ResumeAction != null) + { + SetDebugResuming(debuggerResult.ResumeAction.Value); + RaiseDebuggerResumingEvent(new DebuggerResumingEventArgs(debuggerResult.ResumeAction.Value)); + } + } + + public void HandleBreakpointUpdated(BreakpointUpdatedEventArgs breakpointUpdatedEventArgs) + { + BreakpointUpdated?.Invoke(this, breakpointUpdatedEventArgs); + } + + private void RaiseDebuggerStoppedEvent() + { + if (!IsDebugServerActive) + { + _languageServer.SendNotification("powerShell/startDebugger"); + } + + DebuggerStopped?.Invoke(this, LastStopEventArgs); + } + + private void RaiseDebuggerResumingEvent(DebuggerResumingEventArgs debuggerResumingEventArgs) + { + DebuggerResuming?.Invoke(this, debuggerResumingEventArgs); + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/BlockingConcurrentDeque.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/BlockingConcurrentDeque.cs new file mode 100644 index 000000000..71d34d4f3 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/BlockingConcurrentDeque.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution +{ + /// + /// Implements a concurrent deque that supplies: + /// - Non-blocking prepend and append operations + /// - Blocking and non-blocking take calls + /// - The ability to block consumers, so that can also guarantee the state of the consumer + /// + /// The type of item held by this collection. + /// + /// The prepend/append semantics of this class depend on the implementation semantics of + /// and its overloads checking the supplied array in order. + /// This behavior is unlikely to change and ensuring its correctness at our layer is likely to be costly. + /// See https://stackoverflow.com/q/26472251. + /// + internal class BlockingConcurrentDeque : IDisposable + { + private readonly ManualResetEventSlim _blockConsumersEvent; + + private readonly BlockingCollection[] _queues; + + public BlockingConcurrentDeque() + { + // Initialize in the "set" state, meaning unblocked + _blockConsumersEvent = new ManualResetEventSlim(initialState: true); + + _queues = new[] + { + // The high priority section is FIFO so that "prepend" always puts elements first + new BlockingCollection(new ConcurrentStack()), + new BlockingCollection(new ConcurrentQueue()), + }; + } + + public bool IsEmpty => _queues[0].Count == 0 && _queues[1].Count == 0; + + public void Prepend(T item) + { + _queues[0].Add(item); + } + + public void Append(T item) + { + _queues[1].Add(item); + } + + public T Take(CancellationToken cancellationToken) + { + _blockConsumersEvent.Wait(cancellationToken); + BlockingCollection.TakeFromAny(_queues, out T result, cancellationToken); + return result; + } + + public bool TryTake(out T item) + { + if (!_blockConsumersEvent.IsSet) + { + item = default; + return false; + } + + return BlockingCollection.TryTakeFromAny(_queues, out item) >= 0; + } + + public IDisposable BlockConsumers() => PriorityQueueBlockLifetime.StartBlocking(_blockConsumersEvent); + + public void Dispose() + { + ((IDisposable)_blockConsumersEvent).Dispose(); + } + + private class PriorityQueueBlockLifetime : IDisposable + { + public static PriorityQueueBlockLifetime StartBlocking(ManualResetEventSlim blockEvent) + { + blockEvent.Reset(); + return new PriorityQueueBlockLifetime(blockEvent); + } + + private readonly ManualResetEventSlim _blockEvent; + + private PriorityQueueBlockLifetime(ManualResetEventSlim blockEvent) + { + _blockEvent = blockEvent; + } + + public void Dispose() + { + _blockEvent.Set(); + } + } + } +} + diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs new file mode 100644 index 000000000..4965e3415 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution +{ + public enum ExecutionPriority + { + Normal, + Next, + } + + // Some of the fields of this class are not orthogonal, + // so it's possible to construct self-contradictory execution options. + // We should see if it's possible to rework this class to make the options less misconfigurable. + // Generally the executor will do the right thing though; some options just priority over others. + public record ExecutionOptions + { + public static ExecutionOptions Default = new() + { + Priority = ExecutionPriority.Normal, + MustRunInForeground = false, + InterruptCurrentForeground = false, + }; + + public ExecutionPriority Priority { get; init; } + + public bool MustRunInForeground { get; init; } + + public bool InterruptCurrentForeground { get; init; } + } + + public record PowerShellExecutionOptions : ExecutionOptions + { + public static new PowerShellExecutionOptions Default = new() + { + Priority = ExecutionPriority.Normal, + MustRunInForeground = false, + InterruptCurrentForeground = false, + WriteOutputToHost = false, + WriteInputToHost = false, + ThrowOnError = true, + AddToHistory = false, + }; + + public bool WriteOutputToHost { get; init; } + + public bool WriteInputToHost { get; init; } + + public bool ThrowOnError { get; init; } + + public bool AddToHistory { get; init; } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousDelegateTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousDelegateTask.cs new file mode 100644 index 000000000..343f930eb --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousDelegateTask.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using System; +using System.Threading; +using SMA = System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution +{ + internal class SynchronousDelegateTask : SynchronousTask + { + private readonly Action _action; + + private readonly string _representation; + + public SynchronousDelegateTask( + ILogger logger, + string representation, + ExecutionOptions executionOptions, + Action action, + CancellationToken cancellationToken) + : base(logger, cancellationToken) + { + ExecutionOptions = executionOptions; + _representation = representation; + _action = action; + } + + public override ExecutionOptions ExecutionOptions { get; } + + public override object Run(CancellationToken cancellationToken) + { + _action(cancellationToken); + return null; + } + + public override string ToString() + { + return _representation; + } + } + + internal class SynchronousDelegateTask : SynchronousTask + { + private readonly Func _func; + + private readonly string _representation; + + public SynchronousDelegateTask( + ILogger logger, + string representation, + ExecutionOptions executionOptions, + Func func, + CancellationToken cancellationToken) + : base(logger, cancellationToken) + { + _func = func; + _representation = representation; + ExecutionOptions = executionOptions; + } + + public override ExecutionOptions ExecutionOptions { get; } + + public override TResult Run(CancellationToken cancellationToken) + { + return _func(cancellationToken); + } + + public override string ToString() + { + return _representation; + } + } + + internal class SynchronousPSDelegateTask : SynchronousTask + { + private readonly Action _action; + + private readonly string _representation; + + private readonly PsesInternalHost _psesHost; + + public SynchronousPSDelegateTask( + ILogger logger, + PsesInternalHost psesHost, + string representation, + ExecutionOptions executionOptions, + Action action, + CancellationToken cancellationToken) + : base(logger, cancellationToken) + { + _psesHost = psesHost; + _action = action; + _representation = representation; + ExecutionOptions = executionOptions; + } + + public override ExecutionOptions ExecutionOptions { get; } + + public override object Run(CancellationToken cancellationToken) + { + _action(_psesHost.CurrentPowerShell, cancellationToken); + return null; + } + + public override string ToString() + { + return _representation; + } + } + + internal class SynchronousPSDelegateTask : SynchronousTask + { + private readonly Func _func; + + private readonly string _representation; + + private readonly PsesInternalHost _psesHost; + + public SynchronousPSDelegateTask( + ILogger logger, + PsesInternalHost psesHost, + string representation, + ExecutionOptions executionOptions, + Func func, + CancellationToken cancellationToken) + : base(logger, cancellationToken) + { + _psesHost = psesHost; + _func = func; + _representation = representation; + ExecutionOptions = executionOptions; + } + + public override ExecutionOptions ExecutionOptions { get; } + + public override TResult Run(CancellationToken cancellationToken) + { + return _func(_psesHost.CurrentPowerShell, cancellationToken); + } + + public override string ToString() + { + return _representation; + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs new file mode 100644 index 000000000..4511fb1f5 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs @@ -0,0 +1,257 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using System.Management.Automation.Remoting; +using System.Threading; +using SMA = System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution +{ + internal class SynchronousPowerShellTask : SynchronousTask> + { + private readonly ILogger _logger; + + private readonly PsesInternalHost _psesHost; + + private readonly PSCommand _psCommand; + + private SMA.PowerShell _pwsh; + + public SynchronousPowerShellTask( + ILogger logger, + PsesInternalHost psesHost, + PSCommand command, + PowerShellExecutionOptions executionOptions, + CancellationToken cancellationToken) + : base(logger, cancellationToken) + { + _logger = logger; + _psesHost = psesHost; + _psCommand = command; + PowerShellExecutionOptions = executionOptions; + } + + public PowerShellExecutionOptions PowerShellExecutionOptions { get; } + + public override ExecutionOptions ExecutionOptions => PowerShellExecutionOptions; + + public override IReadOnlyList Run(CancellationToken cancellationToken) + { + _pwsh = _psesHost.CurrentPowerShell; + + if (PowerShellExecutionOptions.WriteInputToHost) + { + _psesHost.UI.WriteLine(_psCommand.GetInvocationText()); + } + + return _pwsh.Runspace.Debugger.InBreakpoint + ? ExecuteInDebugger(cancellationToken) + : ExecuteNormally(cancellationToken); + } + + public override string ToString() + { + return _psCommand.GetInvocationText(); + } + + private IReadOnlyList ExecuteNormally(CancellationToken cancellationToken) + { + if (PowerShellExecutionOptions.WriteOutputToHost) + { + _psCommand.AddOutputCommand(); + } + + cancellationToken.Register(CancelNormalExecution); + + Collection result = null; + try + { + var invocationSettings = new PSInvocationSettings + { + AddToHistory = PowerShellExecutionOptions.AddToHistory, + }; + + if (PowerShellExecutionOptions.ThrowOnError) + { + invocationSettings.ErrorActionPreference = ActionPreference.Stop; + } + + result = _pwsh.InvokeCommand(_psCommand, invocationSettings); + cancellationToken.ThrowIfCancellationRequested(); + } + // Test if we've been cancelled. If we're remoting, PSRemotingDataStructureException effectively means the pipeline was stopped. + catch (Exception e) when (cancellationToken.IsCancellationRequested || e is PipelineStoppedException || e is PSRemotingDataStructureException) + { + throw new OperationCanceledException(); + } + // We only catch RuntimeExceptions here in case writing errors to output was requested + // Other errors are bubbled up to the caller + catch (RuntimeException e) + { + Logger.LogWarning($"Runtime exception occurred while executing command:{Environment.NewLine}{Environment.NewLine}{e}"); + + if (PowerShellExecutionOptions.ThrowOnError) + { + throw; + } + + var command = new PSCommand() + .AddOutputCommand() + .AddParameter("InputObject", e.ErrorRecord.AsPSObject()); + + _pwsh.InvokeCommand(command); + } + finally + { + if (_pwsh.HadErrors) + { + _pwsh.Streams.Error.Clear(); + } + } + + return result; + } + + private IReadOnlyList ExecuteInDebugger(CancellationToken cancellationToken) + { + cancellationToken.Register(CancelDebugExecution); + + var outputCollection = new PSDataCollection(); + + // Out-Default doesn't work as needed in the debugger + // Instead we add Out-String to the command and collect results in a PSDataCollection + // and use the event handler to print output to the UI as its added to that collection + if (PowerShellExecutionOptions.WriteOutputToHost) + { + _psCommand.AddDebugOutputCommand(); + + // Use an inline delegate here, since otherwise we need a cast -- allocation < cast + outputCollection.DataAdded += (object sender, DataAddedEventArgs args) => + { + for (int i = args.Index; i < outputCollection.Count; i++) + { + _psesHost.UI.WriteLine(outputCollection[i].ToString()); + } + }; + } + + DebuggerCommandResults debuggerResult = null; + try + { + // In the PowerShell debugger, extra debugger commands are made available, like "l", "s", "c", etc. + // Executing those commands produces a result that needs to be set on the debugger stop event args. + // So we use the Debugger.ProcessCommand() API to properly execute commands in the debugger + // and then call DebugContext.ProcessDebuggerResult() later to handle the command appropriately + debuggerResult = _pwsh.Runspace.Debugger.ProcessCommand(_psCommand, outputCollection); + cancellationToken.ThrowIfCancellationRequested(); + } + // Test if we've been cancelled. If we're remoting, PSRemotingDataStructureException effectively means the pipeline was stopped. + catch (Exception e) when (cancellationToken.IsCancellationRequested || e is PipelineStoppedException || e is PSRemotingDataStructureException) + { + StopDebuggerIfRemoteDebugSessionFailed(); + throw new OperationCanceledException(); + } + // We only catch RuntimeExceptions here in case writing errors to output was requested + // Other errors are bubbled up to the caller + catch (RuntimeException e) + { + Logger.LogWarning($"Runtime exception occurred while executing command:{Environment.NewLine}{Environment.NewLine}{e}"); + + if (PowerShellExecutionOptions.ThrowOnError) + { + throw; + } + + var errorOutputCollection = new PSDataCollection(); + errorOutputCollection.DataAdded += (object sender, DataAddedEventArgs args) => + { + for (int i = args.Index; i < outputCollection.Count; i++) + { + _psesHost.UI.WriteLine(outputCollection[i].ToString()); + } + }; + + var command = new PSCommand() + .AddDebugOutputCommand() + .AddParameter("InputObject", e.ErrorRecord.AsPSObject()); + + _pwsh.Runspace.Debugger.ProcessCommand(command, errorOutputCollection); + } + finally + { + if (_pwsh.HadErrors) + { + _pwsh.Streams.Error.Clear(); + } + } + + _psesHost.DebugContext.ProcessDebuggerResult(debuggerResult); + + // Optimisation to save wasted computation if we're going to throw the output away anyway + if (PowerShellExecutionOptions.WriteOutputToHost) + { + return Array.Empty(); + } + + // If we've been asked for a PSObject, no need to allocate a new collection + if (typeof(TResult) == typeof(PSObject) + && outputCollection is IReadOnlyList resultCollection) + { + return resultCollection; + } + + // Otherwise, convert things over + var results = new List(outputCollection.Count); + foreach (PSObject outputResult in outputCollection) + { + if (LanguagePrimitives.TryConvertTo(outputResult, typeof(TResult), out object result)) + { + results.Add((TResult)result); + } + } + return results; + } + + private void StopDebuggerIfRemoteDebugSessionFailed() + { + // When remoting to Windows PowerShell, + // command cancellation may cancel the remote debug session in a way that the local debug session doesn't detect. + // Instead we have to query the remote directly + if (_pwsh.Runspace.RunspaceIsRemote) + { + var assessDebuggerCommand = new PSCommand().AddScript("$Host.Runspace.Debugger.InBreakpoint"); + + var outputCollection = new PSDataCollection(); + _pwsh.Runspace.Debugger.ProcessCommand(assessDebuggerCommand, outputCollection); + + foreach (PSObject output in outputCollection) + { + if (object.Equals(output?.BaseObject, false)) + { + _psesHost.DebugContext.ProcessDebuggerResult(new DebuggerCommandResults(DebuggerResumeAction.Stop, evaluatedByDebugger: true)); + _logger.LogWarning("Cancelling debug session due to remote command cancellation causing the end of remote debugging session"); + _psesHost.UI.WriteWarningLine("Debug session aborted by command cancellation. This is a known issue in the Windows PowerShell 5.1 remoting system."); + } + } + } + } + + private void CancelNormalExecution() + { + _pwsh.Stop(); + } + + private void CancelDebugExecution() + { + _pwsh.Runspace.Debugger.StopProcessCommand(); + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousTask.cs new file mode 100644 index 000000000..93ca9aac6 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousTask.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using System; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution +{ + internal interface ISynchronousTask + { + bool IsCanceled { get; } + + void ExecuteSynchronously(CancellationToken threadCancellationToken); + + ExecutionOptions ExecutionOptions { get; } + } + + internal abstract class SynchronousTask : ISynchronousTask + { + private readonly TaskCompletionSource _taskCompletionSource; + + private readonly CancellationToken _taskRequesterCancellationToken; + + private bool _executionCanceled; + + private TResult _result; + + private ExceptionDispatchInfo _exceptionInfo; + + protected SynchronousTask( + ILogger logger, + CancellationToken cancellationToken) + { + Logger = logger; + _taskCompletionSource = new TaskCompletionSource(); + _taskRequesterCancellationToken = cancellationToken; + _executionCanceled = false; + } + + protected ILogger Logger { get; } + + public Task Task => _taskCompletionSource.Task; + + // Sometimes we need the result of task run on the same thread, + // which this property allows us to do. + public TResult Result + { + get + { + if (_executionCanceled) + { + throw new OperationCanceledException(); + } + + if (_exceptionInfo is not null) + { + _exceptionInfo.Throw(); + } + + return _result; + } + } + + public bool IsCanceled => _executionCanceled || _taskRequesterCancellationToken.IsCancellationRequested; + + public abstract ExecutionOptions ExecutionOptions { get; } + + public abstract TResult Run(CancellationToken cancellationToken); + + public abstract override string ToString(); + + public void ExecuteSynchronously(CancellationToken executorCancellationToken) + { + if (IsCanceled) + { + return; + } + + using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(_taskRequesterCancellationToken, executorCancellationToken)) + { + if (cancellationSource.IsCancellationRequested) + { + SetCanceled(); + return; + } + + try + { + TResult result = Run(cancellationSource.Token); + SetResult(result); + } + catch (OperationCanceledException) + { + SetCanceled(); + } + catch (Exception e) + { + SetException(e); + } + } + } + + public TResult ExecuteAndGetResult(CancellationToken cancellationToken) + { + ExecuteSynchronously(cancellationToken); + return Result; + } + + private void SetCanceled() + { + _executionCanceled = true; + _taskCompletionSource.SetCanceled(); + } + + private void SetException(Exception e) + { + // We use this to capture the original stack trace so that exceptions will be useful later + _exceptionInfo = ExceptionDispatchInfo.Capture(e); + _taskCompletionSource.SetException(e); + } + + private void SetResult(TResult result) + { + _result = result; + _taskCompletionSource.SetResult(result); + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/EvaluateHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/EvaluateHandler.cs new file mode 100644 index 000000000..fb0f5764a --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/EvaluateHandler.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Handlers +{ + /// + /// Handler for a custom request type for evaluating PowerShell. + /// This is generally for F8 support, to allow execution of a highlighted code snippet in the console as if it were copy-pasted. + /// + internal class EvaluateHandler : IEvaluateHandler + { + private readonly ILogger _logger; + private readonly IInternalPowerShellExecutionService _executionService; + + public EvaluateHandler( + ILoggerFactory factory, + IInternalPowerShellExecutionService executionService) + { + _logger = factory.CreateLogger(); + _executionService = executionService; + } + + public Task Handle(EvaluateRequestArguments request, CancellationToken cancellationToken) + { + // TODO: Understand why we currently handle this asynchronously and why we return a dummy result value + // instead of awaiting the execution and returing a real result of some kind + + // This API is mostly used for F8 execution, so needs to interrupt the command prompt + _executionService.ExecutePSCommandAsync( + new PSCommand().AddScript(request.Expression), + CancellationToken.None, + new PowerShellExecutionOptions { WriteInputToHost = true, WriteOutputToHost = true, AddToHistory = true, ThrowOnError = false, InterruptCurrentForeground = true }) + .HandleErrorsAsync(_logger); + + return Task.FromResult(new EvaluateResponseBody + { + Result = "", + VariablesReference = 0 + }); + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/ExpandAliasHandler.cs similarity index 81% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/ExpandAliasHandler.cs index 08da235c8..44ba1bfe7 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/ExpandAliasHandler.cs @@ -6,9 +6,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services; using MediatR; using OmniSharp.Extensions.JsonRpc; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -28,12 +28,12 @@ internal class ExpandAliasResult internal class ExpandAliasHandler : IExpandAliasHandler { private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; + private readonly IInternalPowerShellExecutionService _executionService; - public ExpandAliasHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) + public ExpandAliasHandler(ILoggerFactory factory, IInternalPowerShellExecutionService executionService) { _logger = factory.CreateLogger(); - _powerShellContextService = powerShellContextService; + _executionService = executionService; } public async Task Handle(ExpandAliasParams request, CancellationToken cancellationToken) @@ -69,8 +69,7 @@ function __Expand-Alias { .AddStatement() .AddCommand("__Expand-Alias") .AddArgument(request.Text); - var result = await _powerShellContextService.ExecuteCommandAsync( - psCommand, cancellationToken: cancellationToken).ConfigureAwait(false); + var result = await _executionService.ExecutePSCommandAsync(psCommand, cancellationToken).ConfigureAwait(false); return new ExpandAliasResult { diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetCommandHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommandHandler.cs similarity index 88% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetCommandHandler.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommandHandler.cs index 91931a3bf..6db8a898a 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetCommandHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommandHandler.cs @@ -6,9 +6,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services; using MediatR; using OmniSharp.Extensions.JsonRpc; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -33,12 +33,12 @@ internal class PSCommandMessage internal class GetCommandHandler : IGetCommandHandler { private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; + private readonly IInternalPowerShellExecutionService _executionService; - public GetCommandHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) + public GetCommandHandler(ILoggerFactory factory, IInternalPowerShellExecutionService executionService) { _logger = factory.CreateLogger(); - _powerShellContextService = powerShellContextService; + _executionService = executionService; } public async Task> Handle(GetCommandParams request, CancellationToken cancellationToken) @@ -53,8 +53,7 @@ public async Task> Handle(GetCommandParams request, Cance .AddCommand("Microsoft.PowerShell.Utility\\Sort-Object") .AddParameter("Property", "Name"); - IEnumerable result = await _powerShellContextService.ExecuteCommandAsync( - psCommand, cancellationToken: cancellationToken).ConfigureAwait(false); + IEnumerable result = await _executionService.ExecutePSCommandAsync(psCommand, cancellationToken).ConfigureAwait(false); var commandList = new List(); if (result != null) diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommentHelpHandler.cs similarity index 100% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommentHelpHandler.cs diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetVersionHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetVersionHandler.cs similarity index 79% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetVersionHandler.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/GetVersionHandler.cs index ec629b66b..f8a6d5521 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetVersionHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetVersionHandler.cs @@ -3,11 +3,13 @@ using System; using System.Management.Automation; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; @@ -20,18 +22,21 @@ internal class GetVersionHandler : IGetVersionHandler private static readonly Version s_desiredPackageManagementVersion = new Version(1, 4, 6); private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; + private IRunspaceContext _runspaceContext; + private readonly IInternalPowerShellExecutionService _executionService; private readonly ILanguageServerFacade _languageServer; private readonly ConfigurationService _configurationService; public GetVersionHandler( ILoggerFactory factory, - PowerShellContextService powerShellContextService, + IRunspaceContext runspaceContext, + IInternalPowerShellExecutionService executionService, ILanguageServerFacade languageServer, ConfigurationService configurationService) { _logger = factory.CreateLogger(); - _powerShellContextService = powerShellContextService; + _runspaceContext = runspaceContext; + _executionService = executionService; _languageServer = languageServer; _configurationService = configurationService; } @@ -77,7 +82,7 @@ private enum PowerShellProcessArchitecture private async Task CheckPackageManagement() { PSCommand getModule = new PSCommand().AddCommand("Get-Module").AddParameter("ListAvailable").AddParameter("Name", "PackageManagement"); - foreach (PSModuleInfo module in await _powerShellContextService.ExecuteCommandAsync(getModule).ConfigureAwait(false)) + foreach (PSModuleInfo module in await _executionService.ExecutePSCommandAsync(getModule, CancellationToken.None).ConfigureAwait(false)) { // The user has a good enough version of PackageManagement if (module.Version >= s_desiredPackageManagementVersion) @@ -87,7 +92,7 @@ private async Task CheckPackageManagement() _logger.LogDebug("Old version of PackageManagement detected."); - if (_powerShellContextService.CurrentRunspace.Runspace.SessionStateProxy.LanguageMode != PSLanguageMode.FullLanguage) + if (_runspaceContext.CurrentRunspace.Runspace.SessionStateProxy.LanguageMode != PSLanguageMode.FullLanguage) { _languageServer.Window.ShowWarning("You have an older version of PackageManagement known to cause issues with the PowerShell extension. Please run the following command in a new Windows PowerShell session and then restart the PowerShell extension: `Install-Module PackageManagement -Force -AllowClobber -MinimumVersion 1.4.6`"); return; @@ -109,19 +114,22 @@ private async Task CheckPackageManagement() Title = "Not now" } } - }).ConfigureAwait(false); + }) + .ConfigureAwait(false); // If the user chose "Not now" ignore it for the rest of the session. if (messageAction?.Title == takeActionText) { - StringBuilder errors = new StringBuilder(); - await _powerShellContextService.ExecuteScriptStringAsync( - "powershell.exe -NoLogo -NoProfile -Command '[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Install-Module -Name PackageManagement -Force -MinimumVersion 1.4.6 -Scope CurrentUser -AllowClobber -Repository PSGallery'", - errors, - writeInputToHost: true, - writeOutputToHost: true, - addToHistory: true).ConfigureAwait(false); + var command = new PSCommand().AddScript("powershell.exe -NoLogo -NoProfile -Command 'Install-Module -Name PackageManagement -Force -MinimumVersion 1.4.6 -Scope CurrentUser -AllowClobber'"); + await _executionService.ExecutePSCommandAsync( + command, + CancellationToken.None, + new PowerShellExecutionOptions { WriteInputToHost = true, WriteOutputToHost = true, AddToHistory = true, ThrowOnError = false }).ConfigureAwait(false); + + // TODO: Error handling here + + /* if (errors.Length == 0) { _logger.LogDebug("PackageManagement is updated."); @@ -141,6 +149,7 @@ await _powerShellContextService.ExecuteScriptStringAsync( Message = "PackageManagement update failed. This might be due to PowerShell Gallery using TLS 1.2. More info can be found at https://aka.ms/psgallerytls" }); } + */ } } } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IEvaluateHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/IEvaluateHandler.cs similarity index 100% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IEvaluateHandler.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/IEvaluateHandler.cs diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetCommentHelpHandler.cs similarity index 100% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetCommentHelpHandler.cs diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetPSHostProcessesHandler.cs similarity index 100% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetPSHostProcessesHandler.cs diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetRunspaceHandler.cs similarity index 100% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetRunspaceHandler.cs diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetVersionHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetVersionHandler.cs similarity index 91% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetVersionHandler.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetVersionHandler.cs index afd07b9a5..d727950b3 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetVersionHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetVersionHandler.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using MediatR; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Handlers +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell { [Serial, Method("powerShell/getVersion")] internal interface IGetVersionHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PSHostProcessAndRunspaceHandlers.cs similarity index 88% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/PSHostProcessAndRunspaceHandlers.cs index 77a47f845..e4f5586bb 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PSHostProcessAndRunspaceHandlers.cs @@ -3,25 +3,24 @@ using System.Collections.Generic; using System.Management.Automation.Runspaces; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services; namespace Microsoft.PowerShell.EditorServices.Handlers { + using Microsoft.PowerShell.EditorServices.Services.PowerShell; using System.Management.Automation; internal class PSHostProcessAndRunspaceHandlers : IGetPSHostProcessesHandler, IGetRunspaceHandler { private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; + private readonly IInternalPowerShellExecutionService _executionService; - public PSHostProcessAndRunspaceHandlers(ILoggerFactory factory, PowerShellContextService powerShellContextService) + public PSHostProcessAndRunspaceHandlers(ILoggerFactory factory, IInternalPowerShellExecutionService executionService) { _logger = factory.CreateLogger(); - _powerShellContextService = powerShellContextService; + _executionService = executionService; } public Task Handle(GetPSHostProcesssesParams request, CancellationToken cancellationToken) @@ -85,10 +84,8 @@ public async Task Handle(GetRunspaceParams request, Cancella else { var psCommand = new PSCommand().AddCommand("Microsoft.PowerShell.Utility\\Get-Runspace"); - var sb = new StringBuilder(); // returns (not deserialized) Runspaces. For simpler code, we use PSObject and rely on dynamic later. - runspaces = await _powerShellContextService.ExecuteCommandAsync( - psCommand, sb, cancellationToken: cancellationToken).ConfigureAwait(false); + runspaces = await _executionService.ExecutePSCommandAsync(psCommand, cancellationToken).ConfigureAwait(false); } var runspaceResponses = new List(); diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ShowHelpHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs similarity index 83% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ShowHelpHandler.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs index 76a69c1a8..5e13a5f8b 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ShowHelpHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs @@ -5,9 +5,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services; using MediatR; using OmniSharp.Extensions.JsonRpc; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -22,12 +23,12 @@ internal class ShowHelpParams : IRequest internal class ShowHelpHandler : IShowHelpHandler { private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; + private readonly IInternalPowerShellExecutionService _executionService; - public ShowHelpHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) + public ShowHelpHandler(ILoggerFactory factory, IInternalPowerShellExecutionService executionService) { _logger = factory.CreateLogger(); - _powerShellContextService = powerShellContextService; + _executionService = executionService; } public async Task Handle(ShowHelpParams request, CancellationToken cancellationToken) @@ -72,8 +73,7 @@ public async Task Handle(ShowHelpParams request, CancellationToken cancell // TODO: Rather than print the help in the console, we should send the string back // to VSCode to display in a help pop-up (or similar) - await _powerShellContextService.ExecuteCommandAsync( - checkHelpPSCommand, sendOutputToHost: true, cancellationToken: cancellationToken).ConfigureAwait(false); + await _executionService.ExecutePSCommandAsync(checkHelpPSCommand, cancellationToken, new PowerShellExecutionOptions { WriteOutputToHost = true, ThrowOnError = false }).ConfigureAwait(false); return Unit.Value; } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/ConsoleColorProxy.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/ConsoleColorProxy.cs new file mode 100644 index 000000000..75b1bb4be --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/ConsoleColorProxy.cs @@ -0,0 +1,151 @@ +// 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.Services.PowerShell.Host +{ + internal class ConsoleColorProxy + { + internal ConsoleColorProxy(EditorServicesConsolePSHostUserInterface hostUserInterface) + { + if (hostUserInterface == null) throw new ArgumentNullException(); + _hostUserInterface = hostUserInterface; + } + + /// + /// The Accent Color for Formatting + /// + public ConsoleColor FormatAccentColor + { + get + { return _hostUserInterface.FormatAccentColor; } + set + { _hostUserInterface.FormatAccentColor = value; } + } + + /// + /// The Accent Color for Error + /// + public ConsoleColor ErrorAccentColor + { + get + { return _hostUserInterface.ErrorAccentColor; } + set + { _hostUserInterface.ErrorAccentColor = value; } + } + + /// + /// 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; } + } + } +} +*/ diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHost.cs new file mode 100644 index 000000000..b145d0299 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHost.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Globalization; +using System.Management.Automation.Host; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host +{ + public class EditorServicesConsolePSHost : PSHost, IHostSupportsInteractiveSession + { + private readonly PsesInternalHost _internalHost; + + internal EditorServicesConsolePSHost( + PsesInternalHost internalHost) + { + _internalHost = internalHost; + } + + public override CultureInfo CurrentCulture => _internalHost.CurrentCulture; + + public override CultureInfo CurrentUICulture => _internalHost.CurrentUICulture; + + public override Guid InstanceId => _internalHost.InstanceId; + + public override string Name => _internalHost.Name; + + public override PSHostUserInterface UI => _internalHost.UI; + + public override Version Version => _internalHost.Version; + + public bool IsRunspacePushed => _internalHost.IsRunspacePushed; + + public System.Management.Automation.Runspaces.Runspace Runspace => _internalHost.Runspace; + + public override void EnterNestedPrompt() => _internalHost.EnterNestedPrompt(); + + public override void ExitNestedPrompt() => _internalHost.ExitNestedPrompt(); + + public override void NotifyBeginApplication() => _internalHost.NotifyBeginApplication(); + + public override void NotifyEndApplication() => _internalHost.NotifyEndApplication(); + + public void PopRunspace() => _internalHost.PopRunspace(); + + public void PushRunspace(System.Management.Automation.Runspaces.Runspace runspace) => _internalHost.PushRunspace(runspace); + + public override void SetShouldExit(int exitCode) => _internalHost.SetShouldExit(exitCode); + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostRawUserInterface.cs similarity index 72% rename from src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs rename to src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostRawUserInterface.cs index 423a00e9c..de69ff6c3 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostRawUserInterface.cs @@ -2,25 +2,21 @@ // Licensed under the MIT License. using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console; using System; using System.Management.Automation; using System.Management.Automation.Host; using System.Threading; -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host { - /// - /// Provides an implementation of the PSHostRawUserInterface class - /// for the ConsoleService and routes its calls to an IConsoleHost - /// implementation. - /// - internal class TerminalPSHostRawUserInterface : PSHostRawUserInterface + internal class EditorServicesConsolePSHostRawUserInterface : PSHostRawUserInterface { #region Private Fields - private readonly PSHostRawUserInterface internalRawUI; - private ILogger Logger; - private KeyInfo? lastKeyDown; + private readonly PSHostRawUserInterface _internalRawUI; + private readonly ILogger _logger; + private KeyInfo? _lastKeyDown; #endregion @@ -32,10 +28,12 @@ internal class TerminalPSHostRawUserInterface : PSHostRawUserInterface /// /// The ILogger implementation to use for this instance. /// The InternalHost instance from the origin runspace. - public TerminalPSHostRawUserInterface(ILogger logger, PSHost internalHost) + public EditorServicesConsolePSHostRawUserInterface( + ILoggerFactory loggerFactory, + PSHostRawUserInterface internalRawUI) { - this.Logger = logger; - this.internalRawUI = internalHost.UI.RawUI; + _logger = loggerFactory.CreateLogger(); + _internalRawUI = internalRawUI; } #endregion @@ -65,8 +63,8 @@ public override ConsoleColor ForegroundColor /// public override Size BufferSize { - get => this.internalRawUI.BufferSize; - set => this.internalRawUI.BufferSize = value; + get => _internalRawUI.BufferSize; + set => _internalRawUI.BufferSize = value; } /// @@ -81,7 +79,7 @@ public override Coordinates CursorPosition ConsoleProxy.GetCursorTop()); } - set => this.internalRawUI.CursorPosition = value; + set => _internalRawUI.CursorPosition = value; } /// @@ -89,8 +87,8 @@ public override Coordinates CursorPosition /// public override int CursorSize { - get => this.internalRawUI.CursorSize; - set => this.internalRawUI.CursorSize = value; + get => _internalRawUI.CursorSize; + set => _internalRawUI.CursorSize = value; } /// @@ -98,8 +96,8 @@ public override int CursorSize /// public override Coordinates WindowPosition { - get => this.internalRawUI.WindowPosition; - set => this.internalRawUI.WindowPosition = value; + get => _internalRawUI.WindowPosition; + set => _internalRawUI.WindowPosition = value; } /// @@ -107,8 +105,8 @@ public override Coordinates WindowPosition /// public override Size WindowSize { - get => this.internalRawUI.WindowSize; - set => this.internalRawUI.WindowSize = value; + get => _internalRawUI.WindowSize; + set => _internalRawUI.WindowSize = value; } /// @@ -116,24 +114,24 @@ public override Size WindowSize /// public override string WindowTitle { - get => this.internalRawUI.WindowTitle; - set => this.internalRawUI.WindowTitle = value; + get => _internalRawUI.WindowTitle; + set => _internalRawUI.WindowTitle = value; } /// /// Gets a boolean that determines whether a keypress is available. /// - public override bool KeyAvailable => this.internalRawUI.KeyAvailable; + public override bool KeyAvailable => _internalRawUI.KeyAvailable; /// /// Gets the maximum physical size of the console window. /// - public override Size MaxPhysicalWindowSize => this.internalRawUI.MaxPhysicalWindowSize; + public override Size MaxPhysicalWindowSize => _internalRawUI.MaxPhysicalWindowSize; /// /// Gets the maximum size of the console window. /// - public override Size MaxWindowSize => this.internalRawUI.MaxWindowSize; + public override Size MaxWindowSize => _internalRawUI.MaxWindowSize; /// /// Reads the current key pressed in the console. @@ -146,10 +144,10 @@ 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) + if (includeUp && _lastKeyDown != null) { - KeyInfo info = this.lastKeyDown.Value; - this.lastKeyDown = null; + KeyInfo info = _lastKeyDown.Value; + _lastKeyDown = null; return new KeyInfo( info.VirtualKeyCode, info.Character, @@ -201,7 +199,7 @@ public override KeyInfo ReadKey(ReadKeyOptions options) /// public override void FlushInputBuffer() { - Logger.LogWarning( + _logger.LogWarning( "PSHostRawUserInterface.FlushInputBuffer was called"); } @@ -212,7 +210,7 @@ public override void FlushInputBuffer() /// A BufferCell array with the requested buffer contents. public override BufferCell[,] GetBufferContents(Rectangle rectangle) { - return this.internalRawUI.GetBufferContents(rectangle); + return _internalRawUI.GetBufferContents(rectangle); } /// @@ -228,7 +226,7 @@ public override void ScrollBufferContents( Rectangle clip, BufferCell fill) { - this.internalRawUI.ScrollBufferContents(source, destination, clip, fill); + _internalRawUI.ScrollBufferContents(source, destination, clip, fill); } /// @@ -250,7 +248,7 @@ public override void SetBufferContents( return; } - this.internalRawUI.SetBufferContents(rectangle, fill); + _internalRawUI.SetBufferContents(rectangle, fill); } /// @@ -262,54 +260,7 @@ public override void SetBufferContents( Coordinates origin, BufferCell[,] contents) { - this.internalRawUI.SetBufferContents(origin, contents); - } - - /// - /// Determines the number of BufferCells a character occupies. - /// - /// - /// The character whose length we want to know. - /// - /// - /// The length in buffer cells according to the original host - /// implementation for the process. - /// - public override int LengthInBufferCells(char source) - { - return this.internalRawUI.LengthInBufferCells(source); - } - /// - /// Determines the number of BufferCells a string occupies. - /// - /// - /// The string whose length we want to know. - /// - /// - /// The length in buffer cells according to the original host - /// implementation for the process. - /// - public override int LengthInBufferCells(string source) - { - return this.internalRawUI.LengthInBufferCells(source); - } - - /// - /// Determines the number of BufferCells a substring of a string occupies. - /// - /// - /// The string whose substring length we want to know. - /// - /// - /// Offset where the substring begins in - /// - /// - /// The length in buffer cells according to the original host - /// implementation for the process. - /// - public override int LengthInBufferCells(string source, int offset) - { - return this.internalRawUI.LengthInBufferCells(source, offset); + _internalRawUI.SetBufferContents(origin, contents); } #endregion @@ -365,7 +316,7 @@ private KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) var result = new KeyInfo((int)key.Key, key.KeyChar, states, isDown); if (isDown) { - this.lastKeyDown = result; + _lastKeyDown = result; } return result; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs new file mode 100644 index 000000000..cab60cfff --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Reflection; +using System.Security; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host +{ + internal class EditorServicesConsolePSHostUserInterface : PSHostUserInterface + { + private readonly ILogger _logger; + + private readonly IReadLineProvider _readLineProvider; + + private readonly PSHostUserInterface _underlyingHostUI; + + private readonly PSHostUserInterface _consoleHostUI; + + public EditorServicesConsolePSHostUserInterface( + ILoggerFactory loggerFactory, + IReadLineProvider readLineProvider, + PSHostUserInterface underlyingHostUI) + { + _logger = loggerFactory.CreateLogger(); + _readLineProvider = readLineProvider; + _underlyingHostUI = underlyingHostUI; + RawUI = new EditorServicesConsolePSHostRawUserInterface(loggerFactory, underlyingHostUI.RawUI); + + _consoleHostUI = GetConsoleHostUI(_underlyingHostUI); + if (_consoleHostUI != null) + { + SetConsoleHostUIToInteractive(_consoleHostUI); + } + } + + public override PSHostRawUserInterface RawUI { get; } + + public override bool SupportsVirtualTerminal => _underlyingHostUI.SupportsVirtualTerminal; + + public override Dictionary Prompt(string caption, string message, Collection descriptions) + { + if (_consoleHostUI != null) + { + return _consoleHostUI.Prompt(caption, message, descriptions); + } + + return _underlyingHostUI.Prompt(caption, message, descriptions); + } + + public override int PromptForChoice(string caption, string message, Collection choices, int defaultChoice) + { + if (_consoleHostUI != null) + { + return _consoleHostUI.PromptForChoice(caption, message, choices, defaultChoice); + } + + return _underlyingHostUI.PromptForChoice(caption, message, choices, defaultChoice); + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) + { + if (_consoleHostUI != null) + { + return _consoleHostUI.PromptForCredential(caption, message, userName, targetName, allowedCredentialTypes, options); + } + + return _underlyingHostUI.PromptForCredential(caption, message, userName, targetName, allowedCredentialTypes, options); + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) + { + if (_consoleHostUI != null) + { + return _consoleHostUI.PromptForCredential(caption, message, userName, targetName); + } + + return _underlyingHostUI.PromptForCredential(caption, message, userName, targetName); + } + + public override string ReadLine() + { + return _readLineProvider.ReadLine.ReadLine(CancellationToken.None); + } + + public override SecureString ReadLineAsSecureString() + { + return _readLineProvider.ReadLine.ReadSecureLine(CancellationToken.None); + } + + public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) + { + _underlyingHostUI.Write(foregroundColor, backgroundColor, value); + } + + public override void Write(string value) + { + _underlyingHostUI.Write(value); + } + + public override void WriteDebugLine(string message) + { + _underlyingHostUI.WriteDebugLine(message); + } + + public override void WriteErrorLine(string value) + { + _underlyingHostUI.WriteErrorLine(value); + } + + public override void WriteLine(string value) + { + _underlyingHostUI.WriteLine(value); + } + + public override void WriteProgress(long sourceId, ProgressRecord record) => _underlyingHostUI.WriteProgress(sourceId, record); + + public override void WriteVerboseLine(string message) + { + _underlyingHostUI.WriteVerboseLine(message); + } + + public override void WriteWarningLine(string message) + { + _underlyingHostUI.WriteWarningLine(message); + } + + private static PSHostUserInterface GetConsoleHostUI(PSHostUserInterface ui) + { + FieldInfo externalUIField = ui.GetType().GetField("_externalUI", BindingFlags.NonPublic | BindingFlags.Instance); + + if (externalUIField == null) + { + return null; + } + + return (PSHostUserInterface)externalUIField.GetValue(ui); + } + + private static void SetConsoleHostUIToInteractive(PSHostUserInterface ui) + { + ui.GetType().GetProperty("ThrowOnReadAndPrompt", BindingFlags.NonPublic | BindingFlags.Instance)?.SetValue(ui, false); + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/HostStartOptions.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/HostStartOptions.cs new file mode 100644 index 000000000..2a1fdfd2f --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/HostStartOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host +{ + internal struct HostStartOptions + { + public bool LoadProfiles { get; set; } + + public string InitialWorkingDirectory { get; set; } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/NullPSHostRawUI.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/NullPSHostRawUI.cs new file mode 100644 index 000000000..cb5c997c1 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/NullPSHostRawUI.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Management.Automation.Host; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host +{ + internal class NullPSHostRawUI : PSHostRawUserInterface + { + private readonly BufferCell[,] _buffer; + + public NullPSHostRawUI() + { + _buffer = new BufferCell[0, 0]; + } + + public override ConsoleColor BackgroundColor { get; set; } + public override Size BufferSize { get; set; } + public override Coordinates CursorPosition { get; set; } + public override int CursorSize { get; set; } + public override ConsoleColor ForegroundColor { get; set; } + + public override bool KeyAvailable => false; + + public override Size MaxPhysicalWindowSize => MaxWindowSize; + + public override Size MaxWindowSize => new Size { Width = _buffer.GetLength(0), Height = _buffer.GetLength(1) }; + + public override Coordinates WindowPosition { get; set; } + public override Size WindowSize { get; set; } + public override string WindowTitle { get; set; } + + public override void FlushInputBuffer() + { + // Do nothing + } + + public override BufferCell[,] GetBufferContents(Rectangle rectangle) => _buffer; + + public override KeyInfo ReadKey(ReadKeyOptions options) => default; + + public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill) + { + // Do nothing + } + + public override void SetBufferContents(Coordinates origin, BufferCell[,] contents) + { + // Do nothing + } + + public override void SetBufferContents(Rectangle rectangle, BufferCell fill) + { + // Do nothing + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/NullPSHostUI.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/NullPSHostUI.cs new file mode 100644 index 000000000..655fd2fe9 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/NullPSHostUI.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Security; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host +{ + internal class NullPSHostUI : PSHostUserInterface + { + public NullPSHostUI() + { + RawUI = new NullPSHostRawUI(); + } + + public override PSHostRawUserInterface RawUI { get; } + + public override Dictionary Prompt(string caption, string message, Collection descriptions) + { + return new Dictionary(); + } + + public override int PromptForChoice(string caption, string message, Collection choices, int defaultChoice) + { + return 0; + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) + { + return new PSCredential(userName: string.Empty, password: new SecureString()); + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) + => PromptForCredential(caption, message, userName, targetName, PSCredentialTypes.Default, PSCredentialUIOptions.Default); + + public override string ReadLine() + { + return string.Empty; + } + + public override SecureString ReadLineAsSecureString() + { + return new SecureString(); + } + + public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) + { + // Do nothing + } + + public override void Write(string value) + { + // Do nothing + } + + public override void WriteDebugLine(string message) + { + // Do nothing + } + + public override void WriteErrorLine(string value) + { + // Do nothing + } + + public override void WriteLine(string value) + { + // Do nothing + } + + public override void WriteProgress(long sourceId, ProgressRecord record) + { + // Do nothing + } + + public override void WriteVerboseLine(string message) + { + // Do nothing + } + + public override void WriteWarningLine(string message) + { + // Do nothing + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs new file mode 100644 index 000000000..fffed9891 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -0,0 +1,949 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Hosting; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Management.Automation.Host; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; +using Microsoft.PowerShell.EditorServices.Utility; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host +{ + using System.Management.Automation; + using System.Management.Automation.Runspaces; + + internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRunspaceContext, IInternalPowerShellExecutionService + { + private const string DefaultPrompt = "PSIC> "; + + private static readonly string s_commandsModulePath = Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), + "../../Commands/PowerShellEditorServices.Commands.psd1")); + + private readonly ILoggerFactory _loggerFactory; + + private readonly ILogger _logger; + + private readonly ILanguageServerFacade _languageServer; + + private readonly HostStartupInfo _hostInfo; + + private readonly BlockingConcurrentDeque _taskQueue; + + private readonly Stack _psFrameStack; + + private readonly Stack _runspaceStack; + + private readonly CancellationContext _cancellationContext; + + private readonly ReadLineProvider _readLineProvider; + + private readonly Thread _pipelineThread; + + private readonly IdempotentLatch _isRunningLatch = new(); + + private readonly TaskCompletionSource _started = new(); + + private readonly TaskCompletionSource _stopped = new(); + + private EngineIntrinsics _mainRunspaceEngineIntrinsics; + + private bool _shouldExit = false; + + private int _shuttingDown = 0; + + private string _localComputerName; + + private ConsoleKeyInfo? _lastKey; + + private bool _skipNextPrompt = false; + + private bool _resettingRunspace = false; + + public PsesInternalHost( + ILoggerFactory loggerFactory, + ILanguageServerFacade languageServer, + HostStartupInfo hostInfo) + { + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + _languageServer = languageServer; + _hostInfo = hostInfo; + + _readLineProvider = new ReadLineProvider(loggerFactory); + _taskQueue = new BlockingConcurrentDeque(); + _psFrameStack = new Stack(); + _runspaceStack = new Stack(); + _cancellationContext = new CancellationContext(); + + _pipelineThread = new Thread(Run) + { + Name = "PSES Pipeline Execution Thread", + }; + + if (VersionUtils.IsWindows) + { + _pipelineThread.SetApartmentState(ApartmentState.STA); + } + + PublicHost = new EditorServicesConsolePSHost(this); + Name = hostInfo.Name; + Version = hostInfo.Version; + + DebugContext = new PowerShellDebugContext(loggerFactory, languageServer, this); + UI = hostInfo.ConsoleReplEnabled + ? new EditorServicesConsolePSHostUserInterface(loggerFactory, _readLineProvider, hostInfo.PSHost.UI) + : new NullPSHostUI(); + } + + public override CultureInfo CurrentCulture => _hostInfo.PSHost.CurrentCulture; + + public override CultureInfo CurrentUICulture => _hostInfo.PSHost.CurrentUICulture; + + public override Guid InstanceId { get; } = Guid.NewGuid(); + + public override string Name { get; } + + public override PSHostUserInterface UI { get; } + + public override Version Version { get; } + + public bool IsRunspacePushed { get; private set; } + + public Runspace Runspace => _runspaceStack.Peek().Runspace; + + public RunspaceInfo CurrentRunspace => CurrentFrame.RunspaceInfo; + + public PowerShell CurrentPowerShell => CurrentFrame.PowerShell; + + public EditorServicesConsolePSHost PublicHost { get; } + + public PowerShellDebugContext DebugContext { get; } + + public bool IsRunning => _isRunningLatch.IsSignaled; + + public string InitialWorkingDirectory { get; private set; } + + public Task Shutdown => _stopped.Task; + + IRunspaceInfo IRunspaceContext.CurrentRunspace => CurrentRunspace; + + private PowerShellContextFrame CurrentFrame => _psFrameStack.Peek(); + + public event Action RunspaceChanged; + + private bool ShouldExitExecutionLoop => _shouldExit || _shuttingDown != 0; + + public override void EnterNestedPrompt() + { + PushPowerShellAndRunLoop(CreateNestedPowerShell(CurrentRunspace), PowerShellFrameType.Nested); + } + + public override void ExitNestedPrompt() + { + SetExit(); + } + + public override void NotifyBeginApplication() + { + // TODO: Work out what to do here + } + + public override void NotifyEndApplication() + { + // TODO: Work out what to do here + } + + public void PopRunspace() + { + IsRunspacePushed = false; + SetExit(); + } + + public void PushRunspace(Runspace runspace) + { + IsRunspacePushed = true; + PushPowerShellAndRunLoop(CreatePowerShellForRunspace(runspace), PowerShellFrameType.Remote); + } + + public override void SetShouldExit(int exitCode) + { + // TODO: Handle exit code if needed + SetExit(); + } + + /// + /// Try to start the PowerShell loop in the host. + /// If the host is already started, this is idempotent. + /// Returns when the host is in a valid initialized state. + /// + /// Options to configure host startup. + /// A token to cancel startup. + /// A task that resolves when the host has finished startup, with the value true if the caller started the host, and false otherwise. + public async Task TryStartAsync(HostStartOptions startOptions, CancellationToken cancellationToken) + { + _logger.LogInformation("Host starting"); + if (!_isRunningLatch.TryEnter()) + { + _logger.LogDebug("Host start requested after already started."); + await _started.Task.ConfigureAwait(false); + return false; + } + + _pipelineThread.Start(); + + if (startOptions.LoadProfiles) + { + await ExecuteDelegateAsync( + "LoadProfiles", + new PowerShellExecutionOptions { MustRunInForeground = true, ThrowOnError = false }, + (pwsh, delegateCancellation) => pwsh.LoadProfiles(_hostInfo.ProfilePaths), + cancellationToken).ConfigureAwait(false); + + _logger.LogInformation("Profiles loaded"); + } + + if (startOptions.InitialWorkingDirectory is not null) + { + await SetInitialWorkingDirectoryAsync(startOptions.InitialWorkingDirectory, CancellationToken.None).ConfigureAwait(false); + } + + await _started.Task.ConfigureAwait(false); + return true; + } + + public Task StopAsync() + { + TriggerShutdown(); + return Shutdown; + } + + public void TriggerShutdown() + { + if (Interlocked.Exchange(ref _shuttingDown, 1) == 0) + { + _cancellationContext.CancelCurrentTaskStack(); + } + } + + public void SetExit() + { + // Can't exit from the top level of PSES + // since if you do, you lose all LSP services + if (_psFrameStack.Count <= 1) + { + return; + } + + _shouldExit = true; + } + + public Task InvokeTaskOnPipelineThreadAsync( + SynchronousTask task) + { + if (task.ExecutionOptions.InterruptCurrentForeground) + { + // When a task must displace the current foreground command, + // we must: + // - block the consumer thread from mutating the queue + // - cancel any running task on the consumer thread + // - place our task on the front of the queue + // - unblock the consumer thread + using (_taskQueue.BlockConsumers()) + { + CancelCurrentTask(); + _taskQueue.Prepend(task); + } + + return task.Task; + } + + switch (task.ExecutionOptions.Priority) + { + case ExecutionPriority.Next: + _taskQueue.Prepend(task); + break; + + case ExecutionPriority.Normal: + _taskQueue.Append(task); + break; + } + + return task.Task; + } + + public void CancelCurrentTask() + { + _cancellationContext.CancelCurrentTask(); + } + + public Task ExecuteDelegateAsync( + string representation, + ExecutionOptions executionOptions, + Func func, + CancellationToken cancellationToken) + { + return InvokeTaskOnPipelineThreadAsync(new SynchronousPSDelegateTask(_logger, this, representation, executionOptions ?? ExecutionOptions.Default, func, cancellationToken)); + } + + public Task ExecuteDelegateAsync( + string representation, + ExecutionOptions executionOptions, + Action action, + CancellationToken cancellationToken) + { + return InvokeTaskOnPipelineThreadAsync(new SynchronousPSDelegateTask(_logger, this, representation, executionOptions ?? ExecutionOptions.Default, action, cancellationToken)); + } + + public Task ExecuteDelegateAsync( + string representation, + ExecutionOptions executionOptions, + Func func, + CancellationToken cancellationToken) + { + return InvokeTaskOnPipelineThreadAsync(new SynchronousDelegateTask(_logger, representation, executionOptions ?? ExecutionOptions.Default, func, cancellationToken)); + } + + public Task ExecuteDelegateAsync( + string representation, + ExecutionOptions executionOptions, + Action action, + CancellationToken cancellationToken) + { + return InvokeTaskOnPipelineThreadAsync(new SynchronousDelegateTask(_logger, representation, executionOptions ?? ExecutionOptions.Default, action, cancellationToken)); + } + + public Task> ExecutePSCommandAsync( + PSCommand psCommand, + CancellationToken cancellationToken, + PowerShellExecutionOptions executionOptions = null) + { + return InvokeTaskOnPipelineThreadAsync(new SynchronousPowerShellTask( + _logger, + this, + psCommand, + executionOptions ?? PowerShellExecutionOptions.Default, + cancellationToken)); + } + + public Task ExecutePSCommandAsync( + PSCommand psCommand, + CancellationToken cancellationToken, + PowerShellExecutionOptions executionOptions = null) => ExecutePSCommandAsync(psCommand, cancellationToken, executionOptions); + + public TResult InvokeDelegate(string representation, ExecutionOptions executionOptions, Func func, CancellationToken cancellationToken) + { + var task = new SynchronousDelegateTask(_logger, representation, executionOptions, func, cancellationToken); + return task.ExecuteAndGetResult(cancellationToken); + } + + public void InvokeDelegate(string representation, ExecutionOptions executionOptions, Action action, CancellationToken cancellationToken) + { + var task = new SynchronousDelegateTask(_logger, representation, executionOptions, action, cancellationToken); + task.ExecuteAndGetResult(cancellationToken); + } + + public IReadOnlyList InvokePSCommand(PSCommand psCommand, PowerShellExecutionOptions executionOptions, CancellationToken cancellationToken) + { + var task = new SynchronousPowerShellTask(_logger, this, psCommand, executionOptions, cancellationToken); + return task.ExecuteAndGetResult(cancellationToken); + } + + public void InvokePSCommand(PSCommand psCommand, PowerShellExecutionOptions executionOptions, CancellationToken cancellationToken) + => InvokePSCommand(psCommand, executionOptions, cancellationToken); + + public TResult InvokePSDelegate(string representation, ExecutionOptions executionOptions, Func func, CancellationToken cancellationToken) + { + var task = new SynchronousPSDelegateTask(_logger, this, representation, executionOptions, func, cancellationToken); + return task.ExecuteAndGetResult(cancellationToken); + } + + public void InvokePSDelegate(string representation, ExecutionOptions executionOptions, Action action, CancellationToken cancellationToken) + { + var task = new SynchronousPSDelegateTask(_logger, this, representation, executionOptions, action, cancellationToken); + task.ExecuteAndGetResult(cancellationToken); + } + + public Task SetInitialWorkingDirectoryAsync(string path, CancellationToken cancellationToken) + { + InitialWorkingDirectory = path; + + return ExecutePSCommandAsync( + new PSCommand().AddCommand("Set-Location").AddParameter("LiteralPath", path), + cancellationToken); + } + + private void Run() + { + try + { + (PowerShell pwsh, RunspaceInfo localRunspaceInfo, EngineIntrinsics engineIntrinsics) = CreateInitialPowerShellSession(); + _mainRunspaceEngineIntrinsics = engineIntrinsics; + _localComputerName = localRunspaceInfo.SessionDetails.ComputerName; + _runspaceStack.Push(new RunspaceFrame(pwsh.Runspace, localRunspaceInfo)); + PushPowerShellAndRunLoop(pwsh, PowerShellFrameType.Normal, localRunspaceInfo); + } + catch (Exception e) + { + _started.TrySetException(e); + _stopped.TrySetException(e); + } + } + + private (PowerShell, RunspaceInfo, EngineIntrinsics) CreateInitialPowerShellSession() + { + (PowerShell pwsh, EngineIntrinsics engineIntrinsics) = CreateInitialPowerShell(_hostInfo, _readLineProvider); + RunspaceInfo localRunspaceInfo = RunspaceInfo.CreateFromLocalPowerShell(_logger, pwsh); + return (pwsh, localRunspaceInfo, engineIntrinsics); + } + + private void PushPowerShellAndRunLoop(PowerShell pwsh, PowerShellFrameType frameType, RunspaceInfo newRunspaceInfo = null) + { + // TODO: Improve runspace origin detection here + if (newRunspaceInfo is null) + { + newRunspaceInfo = GetRunspaceInfoForPowerShell(pwsh, out bool isNewRunspace, out RunspaceFrame oldRunspaceFrame); + + if (isNewRunspace) + { + Runspace newRunspace = pwsh.Runspace; + _runspaceStack.Push(new RunspaceFrame(newRunspace, newRunspaceInfo)); + RunspaceChanged.Invoke(this, new RunspaceChangedEventArgs(RunspaceChangeAction.Enter, oldRunspaceFrame.RunspaceInfo, newRunspaceInfo)); + } + } + + PushPowerShellAndRunLoop(new PowerShellContextFrame(pwsh, newRunspaceInfo, frameType)); + } + + private RunspaceInfo GetRunspaceInfoForPowerShell(PowerShell pwsh, out bool isNewRunspace, out RunspaceFrame oldRunspaceFrame) + { + oldRunspaceFrame = null; + + if (_runspaceStack.Count > 0) + { + // This is more than just an optimization. + // When debugging, we cannot execute PowerShell directly to get this information; + // trying to do so will block on the command that called us, deadlocking execution. + // Instead, since we are reusing the runspace, we reuse that runspace's info as well. + oldRunspaceFrame = _runspaceStack.Peek(); + if (oldRunspaceFrame.Runspace == pwsh.Runspace) + { + isNewRunspace = false; + return oldRunspaceFrame.RunspaceInfo; + } + } + + isNewRunspace = true; + return RunspaceInfo.CreateFromPowerShell(_logger, pwsh, _localComputerName); + } + + private void PushPowerShellAndRunLoop(PowerShellContextFrame frame) + { + PushPowerShell(frame); + + try + { + if (_psFrameStack.Count == 1) + { + RunTopLevelExecutionLoop(); + } + else if ((frame.FrameType & PowerShellFrameType.Debug) != 0) + { + RunDebugExecutionLoop(); + } + else + { + RunExecutionLoop(); + } + } + finally + { + PopPowerShell(); + } + } + + private void PushPowerShell(PowerShellContextFrame frame) + { + if (_psFrameStack.Count > 0) + { + RemoveRunspaceEventHandlers(CurrentFrame.PowerShell.Runspace); + } + + AddRunspaceEventHandlers(frame.PowerShell.Runspace); + + _psFrameStack.Push(frame); + } + + private void PopPowerShell(RunspaceChangeAction runspaceChangeAction = RunspaceChangeAction.Exit) + { + _shouldExit = false; + PowerShellContextFrame frame = _psFrameStack.Pop(); + try + { + // If we're changing runspace, make sure we move the handlers over + RunspaceFrame previousRunspaceFrame = _runspaceStack.Peek(); + if (previousRunspaceFrame.Runspace != CurrentPowerShell.Runspace) + { + _runspaceStack.Pop(); + RunspaceFrame currentRunspaceFrame = _runspaceStack.Peek(); + RemoveRunspaceEventHandlers(previousRunspaceFrame.Runspace); + AddRunspaceEventHandlers(currentRunspaceFrame.Runspace); + RunspaceChanged?.Invoke(this, new RunspaceChangedEventArgs(runspaceChangeAction, previousRunspaceFrame.RunspaceInfo, currentRunspaceFrame.RunspaceInfo)); + } + } + finally + { + frame.Dispose(); + } + } + + private void RunTopLevelExecutionLoop() + { + try + { + // Make sure we execute any startup tasks first + while (_taskQueue.TryTake(out ISynchronousTask task)) + { + task.ExecuteSynchronously(CancellationToken.None); + } + + // Signal that we are ready for outside services to use + _started.TrySetResult(true); + + if (_hostInfo.ConsoleReplEnabled) + { + RunExecutionLoop(); + } + else + { + RunNoPromptExecutionLoop(); + } + } + catch (Exception e) + { + _logger.LogError(e, "PSES pipeline thread loop experienced an unexpected top-level exception"); + _stopped.TrySetException(e); + return; + } + + _logger.LogInformation("PSES pipeline thread loop shutting down"); + _stopped.SetResult(true); + } + + private void RunNoPromptExecutionLoop() + { + while (!ShouldExitExecutionLoop) + { + using (CancellationScope cancellationScope = _cancellationContext.EnterScope(isIdleScope: false)) + { + string taskRepresentation = null; + try + { + ISynchronousTask task = _taskQueue.Take(cancellationScope.CancellationToken); + taskRepresentation = task.ToString(); + task.ExecuteSynchronously(cancellationScope.CancellationToken); + } + catch (OperationCanceledException) + { + // Just continue + } + catch (Exception e) + { + _logger.LogError(e, $"Fatal exception occurred with task '{taskRepresentation ?? ""}'"); + } + } + } + } + + private void RunDebugExecutionLoop() + { + try + { + DebugContext.EnterDebugLoop(CancellationToken.None); + RunExecutionLoop(); + } + finally + { + DebugContext.ExitDebugLoop(); + } + } + + private void RunExecutionLoop() + { + while (!ShouldExitExecutionLoop) + { + using (CancellationScope cancellationScope = _cancellationContext.EnterScope(isIdleScope: false)) + { + DoOneRepl(cancellationScope.CancellationToken); + + while (!ShouldExitExecutionLoop + && !cancellationScope.CancellationToken.IsCancellationRequested + && _taskQueue.TryTake(out ISynchronousTask task)) + { + task.ExecuteSynchronously(cancellationScope.CancellationToken); + } + } + } + } + + private void DoOneRepl(CancellationToken cancellationToken) + { + if (!_hostInfo.ConsoleReplEnabled) + { + return; + } + + // When a task must run in the foreground, we cancel out of the idle loop and return to the top level. + // At that point, we would normally run a REPL, but we need to immediately execute the task. + // So we set _skipNextPrompt to do that. + if (_skipNextPrompt) + { + _skipNextPrompt = false; + return; + } + + try + { + string prompt = GetPrompt(cancellationToken); + UI.Write(prompt); + string userInput = InvokeReadLine(cancellationToken); + + // If the user input was empty it's because: + // - the user provided no input + // - the readline task was canceled + // - CtrlC was sent to readline (which does not propagate a cancellation) + // + // In any event there's nothing to run in PowerShell, so we just loop back to the prompt again. + // However, we must distinguish the last two scenarios, since PSRL will not print a new line in those cases. + if (string.IsNullOrEmpty(userInput)) + { + if (cancellationToken.IsCancellationRequested + || LastKeyWasCtrlC()) + { + UI.WriteLine(); + } + return; + } + + InvokeInput(userInput, cancellationToken); + } + catch (OperationCanceledException) + { + // Do nothing, since we were just cancelled + } + catch (Exception e) + { + UI.WriteErrorLine($"An error occurred while running the REPL loop:{Environment.NewLine}{e}"); + _logger.LogError(e, "An error occurred while running the REPL loop"); + } + } + + private string GetPrompt(CancellationToken cancellationToken) + { + var command = new PSCommand().AddCommand("prompt"); + IReadOnlyList results = InvokePSCommand(command, PowerShellExecutionOptions.Default, cancellationToken); + string prompt = results.Count > 0 ? results[0] : DefaultPrompt; + + if (CurrentRunspace.RunspaceOrigin != RunspaceOrigin.Local) + { + // This is a PowerShell-internal method that we reuse to decorate the prompt string + // with the remote details when remoting, + // so the prompt changes to indicate when you're in a remote session + prompt = Runspace.GetRemotePrompt(prompt); + } + + return prompt; + } + + private string InvokeReadLine(CancellationToken cancellationToken) + { + return _readLineProvider.ReadLine.ReadLine(cancellationToken); + } + + private void InvokeInput(string input, CancellationToken cancellationToken) + { + var command = new PSCommand().AddScript(input, useLocalScope: false); + InvokePSCommand(command, new PowerShellExecutionOptions { AddToHistory = true, ThrowOnError = false, WriteOutputToHost = true }, cancellationToken); + } + + private void AddRunspaceEventHandlers(Runspace runspace) + { + runspace.Debugger.DebuggerStop += OnDebuggerStopped; + runspace.Debugger.BreakpointUpdated += OnBreakpointUpdated; + runspace.StateChanged += OnRunspaceStateChanged; + } + + private void RemoveRunspaceEventHandlers(Runspace runspace) + { + runspace.Debugger.DebuggerStop -= OnDebuggerStopped; + runspace.Debugger.BreakpointUpdated -= OnBreakpointUpdated; + runspace.StateChanged -= OnRunspaceStateChanged; + } + + private static PowerShell CreateNestedPowerShell(RunspaceInfo currentRunspace) + { + if (currentRunspace.RunspaceOrigin != RunspaceOrigin.Local) + { + return CreatePowerShellForRunspace(currentRunspace.Runspace); + } + + // PowerShell.CreateNestedPowerShell() sets IsNested but not IsChild + // This means it throws due to the parent pipeline not running... + // So we must use the RunspaceMode.CurrentRunspace option on PowerShell.Create() instead + var pwsh = PowerShell.Create(RunspaceMode.CurrentRunspace); + pwsh.Runspace.ThreadOptions = PSThreadOptions.UseCurrentThread; + return pwsh; + } + + private static PowerShell CreatePowerShellForRunspace(Runspace runspace) + { + var pwsh = PowerShell.Create(); + pwsh.Runspace = runspace; + return pwsh; + } + + private (PowerShell, EngineIntrinsics) CreateInitialPowerShell( + HostStartupInfo hostStartupInfo, + ReadLineProvider readLineProvider) + { + Runspace runspace = CreateInitialRunspace(hostStartupInfo.InitialSessionState); + PowerShell pwsh = CreatePowerShellForRunspace(runspace); + + var engineIntrinsics = (EngineIntrinsics)runspace.SessionStateProxy.GetVariable("ExecutionContext"); + + if (hostStartupInfo.ConsoleReplEnabled) + { + // If we've been configured to use it, or if we can't load PSReadLine, use the legacy readline + if (hostStartupInfo.UsesLegacyReadLine || !TryLoadPSReadLine(pwsh, engineIntrinsics, out IReadLine readLine)) + { + readLine = new LegacyReadLine(this, ReadKey, OnPowerShellIdle); + } + + readLineProvider.OverrideReadLine(readLine); + System.Console.CancelKeyPress += OnCancelKeyPress; + System.Console.InputEncoding = Encoding.UTF8; + System.Console.OutputEncoding = Encoding.UTF8; + } + + if (VersionUtils.IsWindows) + { + pwsh.SetCorrectExecutionPolicy(_logger); + } + + pwsh.ImportModule(s_commandsModulePath); + + if (hostStartupInfo.AdditionalModules != null && hostStartupInfo.AdditionalModules.Count > 0) + { + foreach (string module in hostStartupInfo.AdditionalModules) + { + pwsh.ImportModule(module); + } + } + + return (pwsh, engineIntrinsics); + } + + private Runspace CreateInitialRunspace(InitialSessionState initialSessionState) + { + Runspace runspace = RunspaceFactory.CreateRunspace(PublicHost, initialSessionState); + + runspace.SetApartmentStateToSta(); + runspace.ThreadOptions = PSThreadOptions.UseCurrentThread; + + runspace.Open(); + + Runspace.DefaultRunspace = runspace; + + return runspace; + } + + private void OnPowerShellIdle(CancellationToken idleCancellationToken) + { + IReadOnlyList eventSubscribers = _mainRunspaceEngineIntrinsics.Events.Subscribers; + + // Go through pending event subscribers and: + // - if we have any subscribers, ensure we process any events + // - if we have any idle events, generate an idle event and process that + bool runPipelineForEventProcessing = false; + foreach (PSEventSubscriber subscriber in eventSubscribers) + { + runPipelineForEventProcessing = true; + + if (string.Equals(subscriber.SourceIdentifier, PSEngineEvent.OnIdle, StringComparison.OrdinalIgnoreCase)) + { + // We control the pipeline thread, so it's not possible for PowerShell to generate events while we're here. + // But we know we're sitting waiting for the prompt, so we generate the idle event ourselves + // and that will flush idle event subscribers in PowerShell so we can service them + _mainRunspaceEngineIntrinsics.Events.GenerateEvent(PSEngineEvent.OnIdle, sender: null, args: null, extraData: null); + break; + } + } + + if (!runPipelineForEventProcessing && _taskQueue.IsEmpty) + { + return; + } + + using (CancellationScope cancellationScope = _cancellationContext.EnterScope(isIdleScope: true, idleCancellationToken)) + { + while (!cancellationScope.CancellationToken.IsCancellationRequested + && _taskQueue.TryTake(out ISynchronousTask task)) + { + if (task.ExecutionOptions.MustRunInForeground) + { + // If we have a task that is queued, but cannot be run under readline + // we place it back at the front of the queue, and cancel the readline task + _taskQueue.Prepend(task); + _skipNextPrompt = true; + _cancellationContext.CancelIdleParentTask(); + return; + } + + // If we're executing a task, we don't need to run an extra pipeline later for events + // TODO: This may not be a PowerShell task, so ideally we can differentiate that here. + // For now it's mostly true and an easy assumption to make. + runPipelineForEventProcessing = false; + task.ExecuteSynchronously(cancellationScope.CancellationToken); + } + } + + // We didn't end up executing anything in the background, + // so we need to run a small artificial pipeline instead + // to force event processing + if (runPipelineForEventProcessing) + { + InvokePSCommand(new PSCommand().AddScript("0", useLocalScope: true), PowerShellExecutionOptions.Default, CancellationToken.None); + } + } + + private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs args) + { + _cancellationContext.CancelCurrentTask(); + } + + private ConsoleKeyInfo ReadKey(bool intercept) + { + // PSRL doesn't tell us when CtrlC was sent. + // So instead we keep track of the last key here. + // This isn't functionally required, + // but helps us determine when the prompt needs a newline added + + _lastKey = ConsoleProxy.SafeReadKey(intercept, CancellationToken.None); + return _lastKey.Value; + } + + private bool LastKeyWasCtrlC() + { + return _lastKey.HasValue + && _lastKey.Value.IsCtrlC(); + } + + private void OnDebuggerStopped(object sender, DebuggerStopEventArgs debuggerStopEventArgs) + { + DebugContext.SetDebuggerStopped(debuggerStopEventArgs); + try + { + CurrentPowerShell.WaitForRemoteOutputIfNeeded(); + PushPowerShellAndRunLoop(CreateNestedPowerShell(CurrentRunspace), PowerShellFrameType.Debug | PowerShellFrameType.Nested); + CurrentPowerShell.ResumeRemoteOutputIfNeeded(); + } + finally + { + DebugContext.SetDebuggerResumed(); + } + } + + private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs breakpointUpdatedEventArgs) + { + DebugContext.HandleBreakpointUpdated(breakpointUpdatedEventArgs); + } + + private void OnRunspaceStateChanged(object sender, RunspaceStateEventArgs runspaceStateEventArgs) + { + if (!ShouldExitExecutionLoop && !_resettingRunspace && !runspaceStateEventArgs.RunspaceStateInfo.IsUsable()) + { + _resettingRunspace = true; + PopOrReinitializeRunspaceAsync().HandleErrorsAsync(_logger); + } + } + + private Task PopOrReinitializeRunspaceAsync() + { + _cancellationContext.CancelCurrentTaskStack(); + RunspaceStateInfo oldRunspaceState = CurrentPowerShell.Runspace.RunspaceStateInfo; + + // Rather than try to lock the PowerShell executor while we alter its state, + // we simply run this on its thread, guaranteeing that no other action can occur + return ExecuteDelegateAsync( + nameof(PopOrReinitializeRunspaceAsync), + new ExecutionOptions { InterruptCurrentForeground = true }, + (cancellationToken) => + { + while (_psFrameStack.Count > 0 + && !_psFrameStack.Peek().PowerShell.Runspace.RunspaceStateInfo.IsUsable()) + { + PopPowerShell(RunspaceChangeAction.Shutdown); + } + + _resettingRunspace = false; + + if (_psFrameStack.Count == 0) + { + // If our main runspace was corrupted, + // we must re-initialize our state. + // TODO: Use runspace.ResetRunspaceState() here instead + (PowerShell pwsh, RunspaceInfo runspaceInfo, EngineIntrinsics engineIntrinsics) = CreateInitialPowerShellSession(); + _mainRunspaceEngineIntrinsics = engineIntrinsics; + PushPowerShell(new PowerShellContextFrame(pwsh, runspaceInfo, PowerShellFrameType.Normal)); + + _logger.LogError($"Top level runspace entered state '{oldRunspaceState.State}' for reason '{oldRunspaceState.Reason}' and was reinitialized." + + " Please report this issue in the PowerShell/vscode-PowerShell GitHub repository with these logs."); + UI.WriteErrorLine("The main runspace encountered an error and has been reinitialized. See the PowerShell extension logs for more details."); + } + else + { + _logger.LogError($"Current runspace entered state '{oldRunspaceState.State}' for reason '{oldRunspaceState.Reason}' and was popped."); + UI.WriteErrorLine($"The current runspace entered state '{oldRunspaceState.State}' for reason '{oldRunspaceState.Reason}'." + + " If this occurred when using Ctrl+C in a Windows PowerShell remoting session, this is expected behavior." + + " The session is now returning to the previous runspace."); + } + }, + CancellationToken.None); + } + + private bool TryLoadPSReadLine(PowerShell pwsh, EngineIntrinsics engineIntrinsics, out IReadLine psrlReadLine) + { + psrlReadLine = null; + try + { + var psrlProxy = PSReadLineProxy.LoadAndCreate(_loggerFactory, pwsh); + psrlReadLine = new PsrlReadLine(psrlProxy, this, engineIntrinsics, ReadKey, OnPowerShellIdle); + return true; + } + catch (Exception e) + { + _logger.LogError(e, "Unable to load PSReadLine. Will fall back to legacy readline implementation."); + return false; + } + } + + private record RunspaceFrame( + Runspace Runspace, + RunspaceInfo RunspaceInfo); + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/IPowerShellExecutionService.cs b/src/PowerShellEditorServices/Services/PowerShell/IPowerShellExecutionService.cs new file mode 100644 index 000000000..31a75728f --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/IPowerShellExecutionService.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using SMA = System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell +{ + public interface IPowerShellExecutionService + { + Task ExecuteDelegateAsync( + string representation, + ExecutionOptions executionOptions, + Func func, + CancellationToken cancellationToken); + + Task ExecuteDelegateAsync( + string representation, + ExecutionOptions executionOptions, + Action action, + CancellationToken cancellationToken); + + Task ExecuteDelegateAsync( + string representation, + ExecutionOptions executionOptions, + Func func, + CancellationToken cancellationToken); + + Task ExecuteDelegateAsync( + string representation, + ExecutionOptions executionOptions, + Action action, + CancellationToken cancellationToken); + + Task> ExecutePSCommandAsync( + PSCommand psCommand, + CancellationToken cancellationToken, + PowerShellExecutionOptions executionOptions = null); + + Task ExecutePSCommandAsync( + PSCommand psCommand, + CancellationToken cancellationToken, + PowerShellExecutionOptions executionOptions = null); + + void CancelCurrentTask(); + } + + internal interface IInternalPowerShellExecutionService : IPowerShellExecutionService + { + event Action RunspaceChanged; + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/IRunspaceCapability.cs b/src/PowerShellEditorServices/Services/PowerShell/Runspace/IRunspaceContext.cs similarity index 57% rename from src/PowerShellEditorServices/Services/PowerShellContext/Session/IRunspaceCapability.cs rename to src/PowerShellEditorServices/Services/PowerShell/Runspace/IRunspaceContext.cs index 12e5e6fcd..c9232d7d5 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/IRunspaceCapability.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Runspace/IRunspaceContext.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace { - internal interface IRunspaceCapability + internal interface IRunspaceContext { - // NOTE: This interface is intentionally empty for now. + IRunspaceInfo CurrentRunspace { get; } } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Runspace/IRunspaceInfo.cs b/src/PowerShellEditorServices/Services/PowerShell/Runspace/IRunspaceInfo.cs new file mode 100644 index 000000000..401d0deab --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Runspace/IRunspaceInfo.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context; +using SMA = System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace +{ + internal interface IRunspaceInfo + { + RunspaceOrigin RunspaceOrigin { get; } + + bool IsOnRemoteMachine { get; } + + PowerShellVersionDetails PowerShellVersionDetails { get; } + + SessionDetails SessionDetails { get; } + + SMA.Runspace Runspace { get; } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs b/src/PowerShellEditorServices/Services/PowerShell/Runspace/RunspaceChangedEventArgs.cs similarity index 86% rename from src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs rename to src/PowerShellEditorServices/Services/PowerShell/Runspace/RunspaceChangedEventArgs.cs index b835b12a8..5d889ba1c 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Runspace/RunspaceChangedEventArgs.cs @@ -3,7 +3,7 @@ using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace { /// /// Defines the set of actions that will cause the runspace to be changed. @@ -31,21 +31,6 @@ internal enum RunspaceChangeAction /// internal 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. /// @@ -54,8 +39,8 @@ internal class RunspaceChangedEventArgs /// The newly active runspace. public RunspaceChangedEventArgs( RunspaceChangeAction changeAction, - RunspaceDetails previousRunspace, - RunspaceDetails newRunspace) + IRunspaceInfo previousRunspace, + IRunspaceInfo newRunspace) { Validate.IsNotNull(nameof(previousRunspace), previousRunspace); @@ -63,5 +48,20 @@ public RunspaceChangedEventArgs( this.PreviousRunspace = previousRunspace; this.NewRunspace = newRunspace; } + + /// + /// Gets the RunspaceChangeAction which caused this event. + /// + public RunspaceChangeAction ChangeAction { get; } + + /// + /// Gets a RunspaceDetails object describing the previous runspace. + /// + public IRunspaceInfo PreviousRunspace { get; } + + /// + /// Gets a RunspaceDetails object describing the new runspace. + /// + public IRunspaceInfo NewRunspace { get; } } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Runspace/RunspaceInfo.cs b/src/PowerShellEditorServices/Services/PowerShell/Runspace/RunspaceInfo.cs new file mode 100644 index 000000000..c9b4d247c --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Runspace/RunspaceInfo.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using System.Threading; +using System; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace +{ + using System.Management.Automation; + using System.Management.Automation.Runspaces; + + internal class RunspaceInfo : IRunspaceInfo + { + public static RunspaceInfo CreateFromLocalPowerShell( + ILogger logger, + PowerShell pwsh) + { + var psVersionDetails = PowerShellVersionDetails.GetVersionDetails(logger, pwsh); + var sessionDetails = SessionDetails.GetFromPowerShell(pwsh); + + return new RunspaceInfo( + pwsh.Runspace, + RunspaceOrigin.Local, + psVersionDetails, + sessionDetails, + isRemote: false); + } + + public static RunspaceInfo CreateFromPowerShell( + ILogger logger, + PowerShell pwsh, + string localComputerName) + { + var psVersionDetails = PowerShellVersionDetails.GetVersionDetails(logger, pwsh); + var sessionDetails = SessionDetails.GetFromPowerShell(pwsh); + + bool isOnLocalMachine = string.Equals(sessionDetails.ComputerName, localComputerName, StringComparison.OrdinalIgnoreCase) + || string.Equals(sessionDetails.ComputerName, "localhost", StringComparison.OrdinalIgnoreCase); + + RunspaceOrigin runspaceOrigin = RunspaceOrigin.Local; + if (pwsh.Runspace.RunspaceIsRemote) + { + runspaceOrigin = pwsh.Runspace.ConnectionInfo is NamedPipeConnectionInfo + ? RunspaceOrigin.EnteredProcess + : RunspaceOrigin.PSSession; + } + + return new RunspaceInfo( + pwsh.Runspace, + runspaceOrigin, + psVersionDetails, + sessionDetails, + isRemote: !isOnLocalMachine); + } + + private DscBreakpointCapability _dscBreakpointCapability; + + public RunspaceInfo( + Runspace runspace, + RunspaceOrigin origin, + PowerShellVersionDetails powerShellVersionDetails, + SessionDetails sessionDetails, + bool isRemote) + { + Runspace = runspace; + RunspaceOrigin = origin; + SessionDetails = sessionDetails; + PowerShellVersionDetails = powerShellVersionDetails; + IsOnRemoteMachine = isRemote; + } + + public RunspaceOrigin RunspaceOrigin { get; } + + public PowerShellVersionDetails PowerShellVersionDetails { get; } + + public SessionDetails SessionDetails { get; } + + public Runspace Runspace { get; } + + public bool IsOnRemoteMachine { get; } + + public async Task GetDscBreakpointCapabilityAsync( + ILogger logger, + PsesInternalHost psesHost, + CancellationToken cancellationToken) + { + if (_dscBreakpointCapability is not null) + { + _dscBreakpointCapability = await DscBreakpointCapability.GetDscCapabilityAsync( + logger, + this, + psesHost, + cancellationToken) + .ConfigureAwait(false); + } + + return _dscBreakpointCapability; + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Runspace/RunspaceOrigin.cs b/src/PowerShellEditorServices/Services/PowerShell/Runspace/RunspaceOrigin.cs new file mode 100644 index 000000000..b91faf020 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Runspace/RunspaceOrigin.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. +// + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace +{ + /// + /// Specifies the context in which the runspace was encountered. + /// + internal enum RunspaceOrigin + { + /// + /// The original runspace in a local session. + /// + Local, + + /// + /// A remote runspace entered through Enter-PSSession. + /// + PSSession, + + /// + /// A runspace in a process that was entered with Enter-PSHostProcess. + /// + EnteredProcess, + + /// + /// A runspace that is being debugged with Debug-Runspace. + /// + DebuggedRunspace + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs b/src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs new file mode 100644 index 000000000..2683e1992 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; +using System.Linq; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace +{ + using System.Management.Automation; + + /// + /// Provides details about the current PowerShell session. + /// + internal class SessionDetails + { + private const string Property_ComputerName = "computerName"; + private const string Property_ProcessId = "processId"; + private const string Property_InstanceId = "instanceId"; + + /// + /// Runs a PowerShell command to gather details about the current session. + /// + /// A data object containing details about the PowerShell session. + public static SessionDetails GetFromPowerShell(PowerShell pwsh) + { + Hashtable detailsObject = pwsh + .AddScript( + $"@{{ '{Property_ComputerName}' = if ([Environment]::MachineName) {{[Environment]::MachineName}} else {{'localhost'}}; '{Property_ProcessId}' = $PID; '{Property_InstanceId}' = $host.InstanceId }}", + useLocalScope: true) + .InvokeAndClear() + .FirstOrDefault(); + + return new SessionDetails( + (int)detailsObject[Property_ProcessId], + (string)detailsObject[Property_ComputerName], + (Guid?)detailsObject[Property_InstanceId]); + } + + /// + /// Creates an instance of SessionDetails using the information + /// contained in the PSObject which was obtained using the + /// PSCommand returned by GetDetailsCommand. + /// + /// + public SessionDetails( + int processId, + string computerName, + Guid? instanceId) + { + ProcessId = processId; + ComputerName = computerName; + InstanceId = instanceId; + } + + /// + /// Gets the process ID of the current process. + /// + public int? ProcessId { get; } + + /// + /// Gets the name of the current computer. + /// + public string ComputerName { get; } + + /// + /// Gets the current PSHost instance ID. + /// + public Guid? InstanceId { get; } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/CancellationContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/CancellationContext.cs new file mode 100644 index 000000000..8503b2065 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/CancellationContext.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility +{ + /// + /// Encapsulates the scoping logic for cancellation tokens. + /// As PowerShell commands nest, this class maintains a stack of cancellation scopes + /// that allow each scope of logic to be cancelled at its own level. + /// Implicitly handles the merging and cleanup of cancellation token sources. + /// + /// + /// The class + /// and the struct + /// are intended to be used with a using block so you can do this: + /// + /// using (CancellationScope cancellationScope = _cancellationContext.EnterScope(_globalCancellationSource.CancellationToken, localCancellationToken)) + /// { + /// ExecuteCommandAsync(command, cancellationScope.CancellationToken); + /// } + /// + /// + internal class CancellationContext + { + private readonly ConcurrentStack _cancellationSourceStack; + + public CancellationContext() + { + _cancellationSourceStack = new ConcurrentStack(); + } + + public CancellationScope EnterScope(bool isIdleScope, CancellationToken cancellationToken) + { + CancellationTokenSource newScopeCancellationSource = _cancellationSourceStack.TryPeek(out CancellationScope parentScope) + ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, parentScope.CancellationToken) + : CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + return EnterScope(isIdleScope, newScopeCancellationSource); + } + + public CancellationScope EnterScope(bool isIdleScope) => EnterScope(isIdleScope, CancellationToken.None); + + public void CancelCurrentTask() + { + if (_cancellationSourceStack.TryPeek(out CancellationScope currentCancellationSource)) + { + currentCancellationSource.Cancel(); + } + } + + public void CancelCurrentTaskStack() + { + foreach (CancellationScope scope in _cancellationSourceStack) + { + scope.Cancel(); + } + } + + /// + /// Cancels the parent task of the idle task. + /// + public void CancelIdleParentTask() + { + foreach (CancellationScope scope in _cancellationSourceStack) + { + scope.Cancel(); + + // Note that this check is done *after* the cancellation because we want to cancel + // not just the idle task, but its parent as well + // because we want to cancel the ReadLine call that the idle handler is running in + // so we can run something else in the foreground + if (!scope.IsIdleScope) + { + break; + } + } + } + + private CancellationScope EnterScope(bool isIdleScope, CancellationTokenSource cancellationFrameSource) + { + var scope = new CancellationScope(_cancellationSourceStack, cancellationFrameSource, isIdleScope); + _cancellationSourceStack.Push(scope); + return scope; + } + } + + internal class CancellationScope : IDisposable + { + private readonly ConcurrentStack _cancellationStack; + + private readonly CancellationTokenSource _cancellationSource; + + internal CancellationScope( + ConcurrentStack cancellationStack, + CancellationTokenSource frameCancellationSource, + bool isIdleScope) + { + _cancellationStack = cancellationStack; + _cancellationSource = frameCancellationSource; + IsIdleScope = isIdleScope; + } + + public CancellationToken CancellationToken => _cancellationSource.Token; + + public void Cancel() => _cancellationSource.Cancel(); + + public bool IsIdleScope { get; } + + public void Dispose() + { + _cancellationStack.TryPop(out CancellationScope _); + _cancellationSource.Cancel(); + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Utilities/CommandHelpers.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs similarity index 82% rename from src/PowerShellEditorServices/Services/PowerShellContext/Utilities/CommandHelpers.cs rename to src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs index 4dc94b55b..15f6845ae 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Utilities/CommandHelpers.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs @@ -5,10 +5,12 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation; +using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility { /// /// Provides utility methods for working with PowerShell commands. @@ -61,10 +63,17 @@ internal static class CommandHelpers /// A CommandInfo object with details about the specified command. public static async Task GetCommandInfoAsync( string commandName, - PowerShellContextService powerShellContext) + IRunspaceInfo currentRunspace, + IInternalPowerShellExecutionService executionService) { + // This mechanism only works in-process + if (currentRunspace.RunspaceOrigin != RunspaceOrigin.Local) + { + return null; + } + Validate.IsNotNull(nameof(commandName), commandName); - Validate.IsNotNull(nameof(powerShellContext), powerShellContext); + Validate.IsNotNull(nameof(executionService), executionService); // If we have a CommandInfo cached, return that. if (s_commandInfoCache.TryGetValue(commandName, out CommandInfo cmdInfo)) @@ -83,15 +92,12 @@ public static async Task GetCommandInfoAsync( return null; } - PSCommand command = new PSCommand(); - command.AddCommand(@"Microsoft.PowerShell.Core\Get-Command"); - command.AddArgument(commandName); - command.AddParameter("ErrorAction", "Ignore"); + PSCommand command = new PSCommand() + .AddCommand(@"Microsoft.PowerShell.Core\Get-Command") + .AddArgument(commandName) + .AddParameter("ErrorAction", "Ignore"); - CommandInfo commandInfo = (await powerShellContext.ExecuteCommandAsync(command, sendOutputToHost: false, sendErrorToHost: false).ConfigureAwait(false)) - .Select(o => o.BaseObject) - .OfType() - .FirstOrDefault(); + CommandInfo commandInfo = (await executionService.ExecutePSCommandAsync(command, CancellationToken.None).ConfigureAwait(false)).FirstOrDefault(); // Only cache CmdletInfos since they're exposed in binaries they are likely to not change throughout the session. if (commandInfo?.CommandType == CommandTypes.Cmdlet) @@ -106,14 +112,14 @@ public static async Task GetCommandInfoAsync( /// Gets the command's "Synopsis" documentation section. /// /// The CommandInfo instance for the command. - /// The PowerShellContext to use for getting command documentation. + /// The PowerShellContext to use for getting command documentation. /// public static async Task GetCommandSynopsisAsync( CommandInfo commandInfo, - PowerShellContextService powerShellContext) + IInternalPowerShellExecutionService executionService) { Validate.IsNotNull(nameof(commandInfo), commandInfo); - Validate.IsNotNull(nameof(powerShellContext), powerShellContext); + Validate.IsNotNull(nameof(executionService), executionService); // A small optimization to not run Get-Help on things like DSC resources. if (commandInfo.CommandType != CommandTypes.Cmdlet && @@ -140,7 +146,7 @@ public static async Task GetCommandSynopsisAsync( .AddParameter("Online", false) .AddParameter("ErrorAction", "Ignore"); - var results = await powerShellContext.ExecuteCommandAsync(command, sendOutputToHost: false, sendErrorToHost: false).ConfigureAwait(false); + IReadOnlyList results = await executionService.ExecutePSCommandAsync(command, CancellationToken.None).ConfigureAwait(false); PSObject helpObject = results.FirstOrDefault(); // Extract the synopsis string from the object diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/ConsoleKeyInfoExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/ConsoleKeyInfoExtensions.cs new file mode 100644 index 000000000..24498772c --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/ConsoleKeyInfoExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility +{ + internal static class ConsoleKeyInfoExtensions + { + public static bool IsCtrlC(this ConsoleKeyInfo keyInfo) + { + if ((int)keyInfo.Key == 3) + { + return true; + } + + return keyInfo.Key == ConsoleKey.C + && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0 + && (keyInfo.Modifiers & ConsoleModifiers.Shift) == 0 + && (keyInfo.Modifiers & ConsoleModifiers.Alt) == 0; + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs new file mode 100644 index 000000000..031a24749 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Management.Automation; +using System.Reflection; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility +{ + internal static class ErrorRecordExtensions + { + private static Action s_setWriteStreamProperty = null; + + [SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "cctor needed for version specific initialization")] + static ErrorRecordExtensions() + { + if (VersionUtils.IsPS7OrGreater) + { + // Used to write ErrorRecords to the Error stream. Using Public and NonPublic because the plan is to make this property + // public in 7.0.1 + PropertyInfo writeStreamProperty = typeof(PSObject).GetProperty("WriteStream", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + Type writeStreamType = typeof(PSObject).Assembly.GetType("System.Management.Automation.WriteStreamType"); + object errorStreamType = Enum.Parse(writeStreamType, "Error"); + + var errorObjectParameter = Expression.Parameter(typeof(PSObject)); + + // Generates a call like: + // $errorPSObject.WriteStream = [System.Management.Automation.WriteStreamType]::Error + // So that error record PSObjects will be rendered in the console properly + // See https://github.com/PowerShell/PowerShell/blob/946341b2ebe6a61f081f4c9143668dc7be1f9119/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs#L2088-L2091 + s_setWriteStreamProperty = Expression.Lambda>( + Expression.Call( + errorObjectParameter, + writeStreamProperty.GetSetMethod(), + Expression.Constant(errorStreamType)), + errorObjectParameter) + .Compile(); + } + } + + public static PSObject AsPSObject(this ErrorRecord errorRecord) + { + var errorObject = PSObject.AsPSObject(errorRecord); + + // Used to write ErrorRecords to the Error stream so they are rendered in the console correctly. + if (s_setWriteStreamProperty != null) + { + s_setWriteStreamProperty(errorObject); + } + else + { + var note = new PSNoteProperty("writeErrorStream", true); + errorObject.Properties.Add(note); + } + + return errorObject; + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs new file mode 100644 index 000000000..5724776d5 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.Reflection; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Hosting; +using Microsoft.PowerShell.EditorServices.Utility; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility +{ + using System.Management.Automation; + + internal static class PowerShellExtensions + { + private static readonly Action s_waitForServicingComplete; + + private static readonly Action s_suspendIncomingData; + + private static readonly Action s_resumeIncomingData; + + static PowerShellExtensions() + { + s_waitForServicingComplete = (Action)Delegate.CreateDelegate( + typeof(Action), + typeof(PowerShell).GetMethod("WaitForServicingComplete", BindingFlags.Instance | BindingFlags.NonPublic)); + + s_suspendIncomingData = (Action)Delegate.CreateDelegate( + typeof(Action), + typeof(PowerShell).GetMethod("SuspendIncomingData", BindingFlags.Instance | BindingFlags.NonPublic)); + + s_resumeIncomingData = (Action)Delegate.CreateDelegate( + typeof(Action), + typeof(PowerShell).GetMethod("ResumeIncomingData", BindingFlags.Instance | BindingFlags.NonPublic)); + } + + public static Collection InvokeAndClear(this PowerShell pwsh, PSInvocationSettings invocationSettings = null) + { + try + { + return pwsh.Invoke(input: null, invocationSettings); + } + finally + { + pwsh.Commands.Clear(); + } + } + + public static void InvokeAndClear(this PowerShell pwsh, PSInvocationSettings invocationSettings = null) + { + try + { + pwsh.Invoke(input: null, invocationSettings); + } + finally + { + pwsh.Commands.Clear(); + } + } + + public static Collection InvokeCommand(this PowerShell pwsh, PSCommand psCommand, PSInvocationSettings invocationSettings = null) + { + pwsh.Commands = psCommand; + return pwsh.InvokeAndClear(invocationSettings); + } + + public static void InvokeCommand(this PowerShell pwsh, PSCommand psCommand, PSInvocationSettings invocationSettings = null) + { + pwsh.Commands = psCommand; + pwsh.InvokeAndClear(invocationSettings); + } + + /// + /// When running a remote session, waits for remote processing and output to complete. + /// + public static void WaitForRemoteOutputIfNeeded(this PowerShell pwsh) + { + if (!pwsh.Runspace.RunspaceIsRemote) + { + return; + } + + // These methods are required when running commands remotely. + // Remote rendering from command output is done asynchronously. + // So to ensure we wait for output to be rendered, + // we need these methods to wait for rendering. + // PowerShell does this in its own implementation: https://github.com/PowerShell/PowerShell/blob/883ca98dd74ea13b3d8c0dd62d301963a40483d6/src/System.Management.Automation/engine/debugger/debugger.cs#L4628-L4652 + s_waitForServicingComplete(pwsh); + s_suspendIncomingData(pwsh); + } + + public static void ResumeRemoteOutputIfNeeded(this PowerShell pwsh) + { + if (!pwsh.Runspace.RunspaceIsRemote) + { + return; + } + + s_resumeIncomingData(pwsh); + } + + public static void SetCorrectExecutionPolicy(this PowerShell pwsh, ILogger logger) + { + // We want to get the list hierarchy of execution policies + // Calling the cmdlet is the simplest way to do that + IReadOnlyList policies = pwsh + .AddCommand("Microsoft.PowerShell.Security\\Get-ExecutionPolicy") + .AddParameter("-List") + .InvokeAndClear(); + + // The policies come out in the following order: + // - MachinePolicy + // - UserPolicy + // - Process + // - CurrentUser + // - LocalMachine + // We want to ignore policy settings, since we'll already have those anyway. + // Then we need to look at the CurrentUser setting, and then the LocalMachine setting. + // + // Get-ExecutionPolicy -List emits PSObjects with Scope and ExecutionPolicy note properties + // set to expected values, so we must sift through those. + + ExecutionPolicy policyToSet = ExecutionPolicy.Bypass; + var currentUserPolicy = (ExecutionPolicy)policies[policies.Count - 2].Members["ExecutionPolicy"].Value; + if (currentUserPolicy != ExecutionPolicy.Undefined) + { + policyToSet = currentUserPolicy; + } + else + { + var localMachinePolicy = (ExecutionPolicy)policies[policies.Count - 1].Members["ExecutionPolicy"].Value; + if (localMachinePolicy != ExecutionPolicy.Undefined) + { + policyToSet = localMachinePolicy; + } + } + + // If there's nothing to do, save ourselves a PowerShell invocation + if (policyToSet == ExecutionPolicy.Bypass) + { + logger.LogTrace("Execution policy already set to Bypass. Skipping execution policy set"); + return; + } + + // Finally set the inherited execution policy + logger.LogTrace("Setting execution policy to {Policy}", policyToSet); + try + { + pwsh.AddCommand("Microsoft.PowerShell.Security\\Set-ExecutionPolicy") + .AddParameter("Scope", ExecutionPolicyScope.Process) + .AddParameter("ExecutionPolicy", policyToSet) + .AddParameter("Force") + .InvokeAndClear(); + } + catch (CmdletInvocationException e) + { + logger.LogError(e, "Error occurred calling 'Set-ExecutionPolicy -Scope Process -ExecutionPolicy {Policy} -Force'", policyToSet); + } + } + + public static void LoadProfiles(this PowerShell pwsh, ProfilePathInfo profilePaths) + { + var profileVariable = new PSObject(); + + pwsh.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.AllUsersAllHosts), profilePaths.AllUsersAllHosts) + .AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.AllUsersCurrentHost), profilePaths.AllUsersCurrentHost) + .AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.CurrentUserAllHosts), profilePaths.CurrentUserAllHosts) + .AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.CurrentUserCurrentHost), profilePaths.CurrentUserCurrentHost); + + pwsh.Runspace.SessionStateProxy.SetVariable("PROFILE", profileVariable); + } + + public static void ImportModule(this PowerShell pwsh, string moduleNameOrPath) + { + pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module") + .AddParameter("-Name", moduleNameOrPath) + .InvokeAndClear(); + } + + + public static string GetErrorString(this PowerShell pwsh) + { + var sb = new StringBuilder(capacity: 1024) + .Append("Execution of the following command(s) completed with errors:") + .AppendLine() + .AppendLine() + .Append(pwsh.Commands.GetInvocationText()); + + sb.AddErrorString(pwsh.Streams.Error[0], errorIndex: 1); + for (int i = 1; i < pwsh.Streams.Error.Count; i++) + { + sb.AppendLine().AppendLine(); + sb.AddErrorString(pwsh.Streams.Error[i], errorIndex: i + 1); + } + + return sb.ToString(); + } + + private static PowerShell AddProfileMemberAndLoadIfExists(this PowerShell pwsh, PSObject profileVariable, string profileName, string profilePath) + { + profileVariable.Members.Add(new PSNoteProperty(profileName, profilePath)); + + if (File.Exists(profilePath)) + { + var psCommand = new PSCommand() + .AddScript(profilePath, useLocalScope: false) + .AddOutputCommand(); + + pwsh.InvokeCommand(psCommand); + } + + return pwsh; + } + + private static StringBuilder AddErrorString(this StringBuilder sb, ErrorRecord error, int errorIndex) + { + sb.Append("Error #").Append(errorIndex).Append(':').AppendLine() + .Append(error).AppendLine() + .Append("ScriptStackTrace:").AppendLine() + .Append(error.ScriptStackTrace ?? "").AppendLine() + .Append("Exception:").AppendLine() + .Append(" ").Append(error.Exception.ToString() ?? ""); + + Exception innerException = error.Exception?.InnerException; + while (innerException != null) + { + sb.Append("InnerException:").AppendLine() + .Append(" ").Append(innerException); + innerException = innerException.InnerException; + } + + return sb; + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs new file mode 100644 index 000000000..0a5076e57 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Linq.Expressions; +using System.Management.Automation; +using System.Reflection; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility +{ + using System.Management.Automation.Runspaces; + + internal static class RunspaceExtensions + { + private static readonly Action s_runspaceApartmentStateSetter; + + private static readonly Func s_getRemotePromptFunc; + + static RunspaceExtensions() + { + // PowerShell ApartmentState APIs aren't available in PSStandard, so we need to use reflection. + MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod(); + Delegate setter = Delegate.CreateDelegate(typeof(Action), firstArgument: null, method: setterInfo); + s_runspaceApartmentStateSetter = (Action)setter; + + MethodInfo getRemotePromptMethod = typeof(HostUtilities).GetMethod("GetRemotePrompt", BindingFlags.NonPublic | BindingFlags.Static); + ParameterExpression runspaceParam = Expression.Parameter(typeof(Runspace)); + ParameterExpression basePromptParam = Expression.Parameter(typeof(string)); + s_getRemotePromptFunc = Expression.Lambda>( + Expression.Call( + getRemotePromptMethod, + new Expression[] + { + Expression.Convert(runspaceParam, typeof(Runspace).Assembly.GetType("System.Management.Automation.RemoteRunspace")), + basePromptParam, + Expression.Constant(false), // configuredSession must be false + }), + new ParameterExpression[] { runspaceParam, basePromptParam }).Compile(); + } + + public static void SetApartmentStateToSta(this Runspace runspace) + { + s_runspaceApartmentStateSetter?.Invoke(runspace, ApartmentState.STA); + } + + /// + /// Augment a given prompt string with a remote decoration. + /// This is an internal method on Runspace in PowerShell that we reuse via reflection. + /// + /// The runspace the prompt is for. + /// The base prompt to decorate. + /// A prompt string decorated with remote connection details. + public static string GetRemotePrompt(this Runspace runspace, string basePrompt) + { + return s_getRemotePromptFunc(runspace, basePrompt); + } + + public static bool IsUsable(this RunspaceStateInfo runspaceStateInfo) + { + switch (runspaceStateInfo.State) + { + case RunspaceState.Broken: + case RunspaceState.Closed: + case RunspaceState.Closing: + case RunspaceState.Disconnecting: + case RunspaceState.Disconnected: + return false; + + default: + return true; + } + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoicePromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoicePromptHandler.cs deleted file mode 100644 index 499a757a1..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoicePromptHandler.cs +++ /dev/null @@ -1,348 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -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.Services.PowerShellContext -{ - /// - /// Indicates the style of prompt to be displayed. - /// - internal 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. - /// - internal 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 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 - ? Array.Empty() - : new int[] { defaultChoice }; - - // Cancel the TaskCompletionSource if the caller cancels the task - cancellationToken.Register(this.CancelPrompt, true); - - // Convert the int[] result to int - return this.WaitForTaskAsync( - this.StartPromptLoopAsync(this.promptCancellationTokenSource.Token) - .ContinueWith( - task => - { - if (task.IsFaulted) - { - throw task.Exception; - } - else if (task.IsCanceled) - { - throw new TaskCanceledException(task); - } - - return ChoicePromptHandler.GetSingleResult(task.GetAwaiter().GetResult()); - })); - } - - /// - /// 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 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 this.WaitForTaskAsync( - this.StartPromptLoopAsync( - this.promptCancellationTokenSource.Token)); - } - - private async Task WaitForTaskAsync(Task taskToWait) - { - _ = await Task.WhenAny(cancelTask.Task, taskToWait).ConfigureAwait(false); - - if (this.cancelTask.Task.IsCanceled) - { - throw new PipelineStoppedException(); - } - - return await taskToWait.ConfigureAwait(false); - } - - 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 ReadInputStringAsync(cancellationToken).ConfigureAwait(false); - 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 static int GetSingleResult(int[] choiceArray) - { - return - choiceArray != null - ? choiceArray.DefaultIfEmpty(-1).First() - : -1; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Console/CollectionFieldDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/CollectionFieldDetails.cs deleted file mode 100644 index 03a3dff31..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/CollectionFieldDetails.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Contains the details of an colleciton input field shown - /// from an InputPromptHandler. This class is meant to be - /// serializable to the user's UI. - /// - internal 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/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs deleted file mode 100644 index 905d81748..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Linq; -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides a standard implementation of ChoicePromptHandler - /// for use in the interactive console (REPL). - /// - internal 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/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs deleted file mode 100644 index 0897cc10f..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides a standard implementation of InputPromptHandler - /// for use in the interactive console (REPL). - /// - internal 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/Services/PowerShellContext/Console/ConsoleReadLine.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleReadLine.cs deleted file mode 100644 index 145cf7360..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleReadLine.cs +++ /dev/null @@ -1,616 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - using System; - using System.Management.Automation; - using System.Management.Automation.Language; - using System.Security; - - internal class ConsoleReadLine - { - #region Private Field - private readonly 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 static Task ReadSecureLineAsync(CancellationToken cancellationToken) - { - SecureString secureString = new SecureString(); - - // TODO: Are these values used? - int initialPromptRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken).ConfigureAwait(false); - int initialPromptCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken).ConfigureAwait(false); - int previousInputLength = 0; - - Console.TreatControlCAsInput = true; - - try - { - while (!cancellationToken.IsCancellationRequested) - { - ConsoleKeyInfo keyInfo = await ReadKeyAsync(cancellationToken).ConfigureAwait(false); - - 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).ConfigureAwait(false); - int col = await ConsoleProxy.GetCursorLeftAsync(cancellationToken).ConfigureAwait(false); - - // 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 Task ReadKeyAsync(CancellationToken cancellationToken) - { - return ConsoleProxy.ReadKeyAsync(intercept: true, cancellationToken); - } - - private Task ReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) - { - return 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) - { - // TODO: Is inputBeforeCompletion used? - 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).ConfigureAwait(false); - int initialCursorRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken).ConfigureAwait(false); - - // TODO: Are these used? - int initialWindowLeft = Console.WindowLeft; - int initialWindowTop = Console.WindowTop; - - int currentCursorIndex = 0; - - Console.TreatControlCAsInput = true; - - try - { - while (!cancellationToken.IsCancellationRequested) - { - ConsoleKeyInfo keyInfo = await ReadKeyAsync(cancellationToken).ConfigureAwait(false); - - // 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, sendOutputToHost: false, sendErrorToHost: false, cancellationToken).ConfigureAwait(false); - - currentCompletion = results.FirstOrDefault(); - } - else - { - using (RunspaceHandle runspaceHandle = await this.powerShellContext.GetRunspaceHandleAsync(cancellationToken) .ConfigureAwait(false)) - 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 = - ConsoleReadLine.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 = - ConsoleReadLine.MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - currentCursorIndex - 1); - } - } - else if (keyInfo.Key == ConsoleKey.Home) - { - currentCompletion = null; - - currentCursorIndex = - ConsoleReadLine.MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - 0); - } - else if (keyInfo.Key == ConsoleKey.RightArrow) - { - currentCompletion = null; - - if (currentCursorIndex < inputLine.Length) - { - currentCursorIndex = - ConsoleReadLine.MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - currentCursorIndex + 1); - } - } - else if (keyInfo.Key == ConsoleKey.End) - { - currentCompletion = null; - - currentCursorIndex = - ConsoleReadLine.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, sendOutputToHost: false, sendErrorToHost: false, cancellationToken).ConfigureAwait(false) - as Collection; - - if (currentHistory != null) - { - historyIndex = currentHistory.Count; - } - } - - if (currentHistory != null && currentHistory.Count > 0 && historyIndex > 0) - { - historyIndex--; - - currentCursorIndex = - ConsoleReadLine.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 = - ConsoleReadLine.InsertInput( - inputLine, - promptStartCol, - promptStartRow, - (string)currentHistory[historyIndex].Properties["CommandLine"].Value, - currentCursorIndex, - insertIndex: 0, - replaceLength: inputLine.Length); - } - else if (historyIndex == currentHistory.Count) - { - currentCursorIndex = - ConsoleReadLine.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 = - ConsoleReadLine.InsertInput( - inputLine, - promptStartCol, - promptStartRow, - string.Empty, - currentCursorIndex, - insertIndex: 0, - replaceLength: inputLine.Length); - } - else if (keyInfo.Key == ConsoleKey.Backspace) - { - currentCompletion = null; - - if (currentCursorIndex > 0) - { - currentCursorIndex = - ConsoleReadLine.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 = - ConsoleReadLine.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 = - ConsoleReadLine.InsertInput( - inputLine, - promptStartCol, - promptStartRow, - keyInfo.KeyChar.ToString(), // TODO: Determine whether this should take culture into account - currentCursorIndex, - finalCursorIndex: currentCursorIndex + 1); - } - } - } - finally - { - Console.TreatControlCAsInput = false; - } - - return null; - } - - // TODO: Is this used? - private static int CalculateIndexFromCursor( - int promptStartCol, - int promptStartRow, - int consoleWidth) - { - return - ((ConsoleProxy.GetCursorTop() - promptStartRow) * consoleWidth) + - ConsoleProxy.GetCursorLeft() - promptStartCol; - } - - private static 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 static 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 - ConsoleReadLine.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 - ConsoleReadLine.MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - finalCursorIndex); - } - else - { - return inputLine.Length; - } - } - - private static int MoveCursorToIndex( - int promptStartCol, - int promptStartRow, - int consoleWidth, - int newCursorIndex) - { - ConsoleReadLine.CalculateCursorFromIndex( - promptStartCol, - promptStartRow, - consoleWidth, - newCursorIndex, - out int newCursorCol, - out int newCursorRow); - - Console.SetCursorPosition(newCursorCol, newCursorRow); - - return newCursorIndex; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Console/CredentialFieldDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/CredentialFieldDetails.cs deleted file mode 100644 index 6fa605f10..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/CredentialFieldDetails.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Management.Automation; -using System.Security; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Contains the details of a PSCredential field shown - /// from an InputPromptHandler. This class is meant to - /// be serializable to the user's UI. - /// - internal 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/Services/PowerShellContext/Console/FieldDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/FieldDetails.cs deleted file mode 100644 index f11a9fbd8..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/FieldDetails.cs +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -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.Services.PowerShellContext -{ - /// - /// Contains the details of an input field shown from an - /// InputPromptHandler. This class is meant to be - /// serializable to the user's UI. - /// - internal 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/Services/PowerShellContext/Console/InputPromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/InputPromptHandler.cs deleted file mode 100644 index 29e8a0ec2..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/InputPromptHandler.cs +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -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.Services.PowerShellContext -{ - /// - /// Provides a base implementation for IPromptHandler classes - /// that present the user a set of fields for which values - /// should be entered. - /// - internal 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); - - _ = await Task.WhenAny(cancelTask.Task, promptTask).ConfigureAwait(false); - - 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).ConfigureAwait(false); - responseValue = secureString; - enteredValue = secureString != null; - } - else - { - responseString = await this.ReadInputStringAsync(cancellationToken).ConfigureAwait(false); - 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/Services/PowerShellContext/Console/PromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/PromptHandler.cs deleted file mode 100644 index 259a0c028..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/PromptHandler.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Defines an abstract base class for prompt handler implementations. - /// - internal 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/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs deleted file mode 100644 index 19e2f7973..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// 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).ConfigureAwait(false); - this.hostOutput.WriteOutput(string.Empty); - - return inputString; - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs deleted file mode 100644 index a9d07c03f..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Security; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// 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).ConfigureAwait(false); - 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 ConsoleReadLine.ReadSecureLineAsync(cancellationToken).ConfigureAwait(false); - this.hostOutput.WriteOutput(string.Empty); - - return secureString; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/EvaluateHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/EvaluateHandler.cs deleted file mode 100644 index ca37a1cc3..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/EvaluateHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services; - -namespace Microsoft.PowerShell.EditorServices.Handlers -{ - internal class EvaluateHandler : IEvaluateHandler - { - private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; - - public EvaluateHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) - { - _logger = factory.CreateLogger(); - _powerShellContextService = powerShellContextService; - } - - public Task Handle(EvaluateRequestArguments request, CancellationToken cancellationToken) - { - _powerShellContextService.ExecuteScriptStringAsync( - request.Expression, - writeInputToHost: true, - writeOutputToHost: true, - addToHistory: true); - - return Task.FromResult(new EvaluateResponseBody - { - Result = "", - VariablesReference = 0 - }); - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs deleted file mode 100644 index f251d3e63..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ /dev/null @@ -1,2851 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -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.Handlers; -using Microsoft.PowerShell.EditorServices.Hosting; -using Microsoft.PowerShell.EditorServices.Logging; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Services -{ - using System.Management.Automation; - - /// - /// 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. - /// - internal class PowerShellContextService : IHostSupportsInteractiveSession - { - // This is a default that can be overriden at runtime by the user or tests. - private static string s_bundledModulePath = Path.GetFullPath(Path.Combine( - Path.GetDirectoryName(typeof(PowerShellContextService).Assembly.Location), - "..", - "..", - "..")); - - private static string s_commandsModulePath => Path.GetFullPath(Path.Combine( - s_bundledModulePath, - "PowerShellEditorServices", - "Commands", - "PowerShellEditorServices.Commands.psd1")); - - private static readonly Action s_runspaceApartmentStateSetter; - private static readonly PropertyInfo s_writeStreamProperty; - private static readonly object s_errorStreamValue; - - [SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "cctor needed for version specific initialization")] - static PowerShellContextService() - { - // PowerShell ApartmentState APIs aren't available in PSStandard, so we need to use reflection. - if (!VersionUtils.IsNetCore || VersionUtils.IsPS7OrGreater) - { - MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod(); - Delegate setter = Delegate.CreateDelegate(typeof(Action), firstArgument: null, method: setterInfo); - s_runspaceApartmentStateSetter = (Action)setter; - } - - if (VersionUtils.IsPS7OrGreater) - { - // Used to write ErrorRecords to the Error stream. Using Public and NonPublic because the plan is to make this property - // public in 7.0.1 - s_writeStreamProperty = typeof(PSObject).GetProperty("WriteStream", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); - Type writeStreamType = typeof(PSObject).Assembly.GetType("System.Management.Automation.WriteStreamType"); - s_errorStreamValue = Enum.Parse(writeStreamType, "Error"); - } - } - - #region Fields - - private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - private readonly SessionStateLock sessionStateLock = new SessionStateLock(); - - private readonly OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade _languageServer; - private readonly bool isPSReadLineEnabled; - private readonly ILogger logger; - - private PowerShell powerShell; - private bool ownsInitialRunspace; - private RunspaceDetails initialRunspace; - private SessionDetails mostRecentSessionDetails; - - private ProfilePathInfo profilePaths; - - private IVersionSpecificOperations versionSpecificOperations; - - private readonly 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; } - - internal 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; } - - /// - /// Tracks the state of the LSP debug server (not the PowerShell debugger). - /// - internal bool IsDebugServerActive { get; set; } - - /// - /// Tracks if the PowerShell session started the debug server itself (true), or if it was - /// started by an LSP notification (false). Essentially, this marks if we're responsible for - /// stopping the debug server (and thus need to send a notification to do so). - /// - internal bool OwnsDebugServerState { get; set; } - - internal DebuggerStopEventArgs CurrentDebuggerStopEventArgs { 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, - OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade languageServer, - bool isPSReadLineEnabled) - { - _languageServer = languageServer; - this.logger = logger; - this.isPSReadLineEnabled = isPSReadLineEnabled; - - RunspaceChanged += PowerShellContext_RunspaceChangedAsync; - ExecutionStatusChanged += PowerShellContext_ExecutionStatusChangedAsync; - } - - [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Checked by Validate call")] - public static PowerShellContextService Create( - ILoggerFactory factory, - OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade languageServer, - HostStartupInfo hostStartupInfo) - { - var logger = factory.CreateLogger(); - - Validate.IsNotNull(nameof(hostStartupInfo), hostStartupInfo); - - // Respect a user provided bundled module path. - if (Directory.Exists(hostStartupInfo.BundledModulePath)) - { - logger.LogDebug($"Using new bundled module path: {hostStartupInfo.BundledModulePath}"); - s_bundledModulePath = hostStartupInfo.BundledModulePath; - } - - bool shouldUsePSReadLine = hostStartupInfo.ConsoleReplEnabled - && !hostStartupInfo.UsesLegacyReadLine; - - var powerShellContext = new PowerShellContextService( - logger, - languageServer, - shouldUsePSReadLine); - - EditorServicesPSHostUserInterface hostUserInterface = - hostStartupInfo.ConsoleReplEnabled - ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, hostStartupInfo.PSHost, logger) - : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); - - EditorServicesPSHost psHost = - new EditorServicesPSHost( - powerShellContext, - hostStartupInfo, - hostUserInterface, - logger); - - Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost, hostStartupInfo.InitialSessionState); - powerShellContext.Initialize(hostStartupInfo.ProfilePaths, initialRunspace, true, hostUserInterface); - powerShellContext.ImportCommandsModuleAsync(); - - // 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 hostStartupInfo.AdditionalModules) - { - var command = - new PSCommand() - .AddCommand("Microsoft.PowerShell.Core\\Import-Module") - .AddParameter("Name", module); - -#pragma warning disable CS4014 - // This call queues the loading on the pipeline thread, so no need to await - powerShellContext.ExecuteCommandAsync( - command, - sendOutputToHost: false, - sendErrorToHost: true); -#pragma warning restore CS4014 - } - - return powerShellContext; - } - - /// - /// Only used in testing. Creates a Runspace given HostStartupInfo instead of a PSHost. - /// - /// - /// TODO: We should use `CreateRunspace` in testing instead of this, if possible. - /// - /// - /// - /// The EditorServicesPSHostUserInterface to use for this instance. - /// An ILogger implementation to use for this instance. - /// - public static Runspace CreateTestRunspace( - HostStartupInfo hostDetails, - PowerShellContextService powerShellContext, - EditorServicesPSHostUserInterface hostUserInterface, - ILogger logger) - { - Validate.IsNotNull(nameof(powerShellContext), powerShellContext); - - var psHost = new EditorServicesPSHost(powerShellContext, hostDetails, hostUserInterface, logger); - powerShellContext.ConsoleWriter = hostUserInterface; - powerShellContext.ConsoleReader = hostUserInterface; - return CreateRunspace(psHost, hostDetails.InitialSessionState); - } - - /// - /// - /// - /// The PSHost that will be used for this Runspace. - /// This will be used when creating runspaces so that we honor the same InitialSessionState. - /// - public static Runspace CreateRunspace(PSHost psHost, InitialSessionState initialSessionState) - { - 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. - /// An IHostOutput implementation. Optional. - public void Initialize( - ProfilePathInfo 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, - connectionString: null); - this.CurrentRunspace = this.initialRunspace; - - // Write out the PowerShell version for tracking purposes - this.logger.LogInformation($"PowerShell Version: {this.LocalPowerShellVersion.Version}, Edition: {this.LocalPowerShellVersion.Edition}"); - - Version powerShellVersion = this.LocalPowerShellVersion.Version; - if (powerShellVersion >= new Version(5, 0)) - { - this.versionSpecificOperations = new PowerShell5Operations(); - } - else - { - // TODO: Also throw for PowerShell 6 - throw new NotSupportedException( - "This computer has an unsupported version of PowerShell installed: " + - powerShellVersion.ToString()); - } - - // 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 (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, s_bundledModulePath, out PSReadLineProxy proxy)) - { - this.PromptContext = new PSReadLinePromptContext( - this, - this.PromptNest, - this.InvocationEventQueue, - proxy); - } - else - { - this.PromptContext = new LegacyReadLineContext(this); - } - - // Finally, restore the runspace's execution policy to the user's policy instead of - // Bypass. - this.RestoreExecutionPolicy(); - } - - /// - /// Imports the PowerShellEditorServices.Commands module into - /// the runspace. This method will be moved somewhere else soon. - /// - /// - public Task ImportCommandsModuleAsync() - { - this.logger.LogTrace($"Importing PowershellEditorServices commands from {s_commandsModulePath}"); - - PSCommand importCommand = new PSCommand() - .AddCommand("Import-Module") - .AddArgument(s_commandsModulePath); - - return this.ExecuteCommandAsync(importCommand, sendOutputToHost: false, sendErrorToHost: 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) - { - this.logger.LogTrace("Configuring Runspace"); - 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) - { - this.logger.LogTrace("Cleaning Up Runspace"); - 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(isReadLine: false, CancellationToken.None); - } - - /// - /// 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(isReadLine: false, cancellationToken); - } - - /// - /// 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 Task> ExecuteCommandAsync( - PSCommand psCommand, - bool sendOutputToHost = false, - bool sendErrorToHost = true, - CancellationToken cancellationToken = default) - { - return this.ExecuteCommandAsync( - psCommand, errorMessages: null, sendOutputToHost, sendErrorToHost, cancellationToken: cancellationToken); - } - - /// - /// 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, - CancellationToken cancellationToken = default) - { - return - this.ExecuteCommandAsync( - psCommand, - errorMessages, - new ExecutionOptions - { - WriteOutputToHost = sendOutputToHost, - WriteErrorsToHost = sendErrorToHost, - AddToHistory = addToHistory - }, - cancellationToken); - } - - /// - /// Executes a PSCommand against the session's runspace and returns - /// a collection of results of the expected type. This function needs help. - /// - /// 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. - /// - [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Checked by Validate call")] - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "PowerShellContext must catch and log all exceptions to be robust")] - public async Task> ExecuteCommandAsync( - PSCommand psCommand, - StringBuilder errorMessages, - ExecutionOptions executionOptions, - CancellationToken cancellationToken = default) - { - Validate.IsNotNull(nameof(psCommand), psCommand); - Validate.IsNotNull(nameof(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(executionOptions.InputString ?? 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))) - { - if (shouldCancelReadLine && PromptNest.IsReadLineBusy()) - { - // If a ReadLine pipeline is running in the debugger then we'll stop responding 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)).ConfigureAwait(false); - } - - Task writeErrorsToConsoleTask = null; - 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).ConfigureAwait(false); - } - - // 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).ConfigureAwait(false); - if (executionOptions.WriteInputToHost) - { - this.WriteOutput( - executionOptions.InputString ?? psCommand.Commands[0].CommandText, - includeNewLine: 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 - { - this.logger.LogTrace($"Executing in debugger: {GetStringForPSCommand(psCommand)}"); - return this.ExecuteCommandInDebugger( - psCommand, - executionOptions.WriteOutputToHost); - } - catch (Exception e) - { - this.logger.LogException("Exception occurred while executing debugger command", e); - } - finally - { - if (!executionOptions.IsReadLine) - { - this.OnSessionStateChanged( - this, - new SessionStateChangedEventArgs( - PowerShellContextState.Ready, - PowerShellExecutionResult.Stopped, - null)); - } - } - } - - var invocationSettings = new PSInvocationSettings() - { - AddToHistory = executionOptions.AddToHistory - }; - - PowerShell shell = this.PromptNest.GetPowerShell(executionOptions.IsReadLine); - - // Due to the following PowerShell bug, we can't just assign shell.Commands to psCommand - // because PowerShell strips out CommandInfo: - // https://github.com/PowerShell/PowerShell/issues/12297 - shell.Commands.Clear(); - foreach (Command command in psCommand.Commands) - { - shell.Commands.AddCommand(command); - } - - // Don't change our SessionState for ReadLine. - if (!executionOptions.IsReadLine) - { - await this.sessionStateLock.AcquireForExecuteCommandAsync().ConfigureAwait(false); - shell.InvocationStateChanged += PowerShell_InvocationStateChanged; - } - - shell.Runspace = executionOptions.ShouldExecuteInOriginalRunspace - ? this.initialRunspace.Runspace - : this.CurrentRunspace.Runspace; - try - { - this.logger.LogDebug($"Invoking: {GetStringForPSCommand(psCommand)}"); - - // 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); - } - - // This is the primary reason that ExecuteCommandAsync takes a CancellationToken - return await Task.Run>( - () => shell.Invoke(input: null, invocationSettings), cancellationToken) - .ConfigureAwait(false); - } - finally - { - if (!executionOptions.IsReadLine) - { - shell.InvocationStateChanged -= PowerShell_InvocationStateChanged; - await this.sessionStateLock.ReleaseForExecuteCommand().ConfigureAwait(false); - } - - // This is the edge case where the debug server is running because it was - // started by PowerShell (and not by an LSP event), and we're no longer in the - // debugger within PowerShell, so since we own the state we need to stop the - // debug server too. - // - // Strangely one would think we could check `!PromptNest.IsInDebugger` but that - // doesn't work, we have to check if the shell is nested instead. Therefore this - // is a bit fragile, and I don't know how it'll work in a remoting scenario. - if (IsDebugServerActive && OwnsDebugServerState && !shell.IsNested) - { - logger.LogDebug("Stopping LSP debugger because PowerShell debugger stopped running!"); - OwnsDebugServerState = false; - _languageServer?.SendNotification("powerShell/stopDebugger"); - } - - 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; - } - } - } - catch (PSRemotingDataStructureException e) - { - this.logger.LogHandledException("PSRemotingDataStructure exception while executing command", e); - errorMessages?.Append(e.Message); - } - catch (PipelineStoppedException e) - { - this.logger.LogHandledException("Pipeline stopped while executing command", e); - errorMessages?.Append(e.Message); - } - catch (RuntimeException e) - { - this.logger.LogHandledException("Runtime exception occurred while executing command", e); - - hadErrors = true; - errorMessages?.Append(e.Message); - - if (executionOptions.WriteErrorsToHost) - { - // Write the error to the host - // We must await this after the runspace handle has been released or we will deadlock - writeErrorsToConsoleTask = this.WriteExceptionToHostAsync(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(cancellationToken).ConfigureAwait(false); - } - - 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(); - if (writeErrorsToConsoleTask != null) - { - await writeErrorsToConsoleTask.ConfigureAwait(false); - } - } - - 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 Task> ExecuteScriptStringAsync( - string scriptString, - StringBuilder errorMessages, - bool writeInputToHost, - bool writeOutputToHost, - bool addToHistory) - { - Validate.IsNotNull(nameof(scriptString), scriptString); - - PSCommand command = null; - if(CurrentRunspace.Runspace.SessionStateProxy.LanguageMode != PSLanguageMode.FullLanguage) - { - try - { - var scriptBlock = ScriptBlock.Create(scriptString); - PowerShell ps = scriptBlock.GetPowerShell(isTrustedInput: false, null); - command = ps.Commands; - } - catch (Exception e) - { - logger.LogException("Exception getting trusted/untrusted PSCommand.", e); - } - } - - // fall back to old behavior - if(command == null) - { - command = new PSCommand().AddScript(scriptString.Trim()); - } - - return this.ExecuteCommandAsync( - command, - 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) - { - Validate.IsNotNull(nameof(script), script); - - PSCommand command = new PSCommand(); - - if (arguments != null) - { - // Add CWD from PowerShell if the script is a file (not a command/inline script) and - // it's not an absolute path. - if (File.Exists(script) && !Path.IsPathRooted(script)) - { - 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"), - sendOutputToHost: false, - sendErrorToHost: false).ConfigureAwait(false)) - .FirstOrDefault() - .ProviderPath; - - this.logger.LogDebug($"Prepending working directory {workingDir} to script path {script}"); - script = Path.Combine(workingDir, script); - } - catch (System.Management.Automation.DriveNotFoundException e) - { - this.logger.LogHandledException("Could not determine current filesystem location", e); - } - } - - 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)) - { - // 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(); - - 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); - } - - - await this.ExecuteCommandAsync( - command, - errorMessages: null, - new ExecutionOptions - { - WriteInputToHost = true, - WriteOutputToHost = true, - WriteErrorsToHost = true, - AddToHistory = true, - }).ConfigureAwait(false); - } - - /// - /// 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 Task InvokeOnPipelineThreadAsync(Action invocationAction) - { - if (this.PromptNest.IsReadLineBusy()) - { - return this.InvocationEventQueue.InvokeOnPipelineThreadAsync(invocationAction); - } - - // 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()); - return Task.CompletedTask; - } - - internal async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) - { - return await PromptContext.InvokeReadLineAsync( - isCommandLine, - cancellationToken).ConfigureAwait(false); - } - - internal static TResult ExecuteScriptAndGetItem( - string scriptToExecute, - Runspace runspace, - TResult defaultValue = default, - bool useLocalScope = false) - { - using (PowerShell pwsh = PowerShell.Create()) - { - pwsh.Runspace = runspace; - IEnumerable results = pwsh.AddScript(scriptToExecute, useLocalScope).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) - { - return; - } - - // Load any of the profile paths that exist - var command = new PSCommand(); - bool hasLoadablePath = false; - foreach (var profilePath in GetLoadableProfilePaths(this.profilePaths)) - { - hasLoadablePath = true; - command.AddCommand(profilePath, false).AddStatement(); - } - - if (!hasLoadablePath) - { - return; - } - - await ExecuteCommandAsync(command, sendOutputToHost: true).ConfigureAwait(false); - - // Gather the session details (particularly the prompt) after - // loading the user's profiles. - await this.GetSessionDetailsInRunspaceAsync().ConfigureAwait(false); - } - - /// - /// 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) - { - this.logger.LogTrace("Execution abort requested..."); - - if (this.SessionState == PowerShellContextState.Aborting - || this.SessionState == PowerShellContextState.Disposed) - { - this.logger.LogTrace($"Execution abort requested when already aborted (SessionState = {this.SessionState})"); - return; - } - - if (shouldAbortDebugSession) - { - this.ExitAllNestedPrompts(); - } - - if (this.PromptNest.IsInDebugger) - { - this.versionSpecificOperations.StopCommandInDebugger(this); - if (shouldAbortDebugSession) - { - this.ResumeDebugger(DebuggerResumeAction.Stop, shouldWaitForExit: false); - } - } - else - { - this.PromptNest.GetPowerShell(isReadLine: false).BeginStop(null, null); - } - - // TODO: - // This lock and state reset are a temporary fix at best. - // We need to investigate how the debugger should be interacting - // with PowerShell in this cancellation scenario. - // - // Currently we try to acquire a lock on the execution status changed event. - // If we can't, it's because a command is executing, so we shouldn't change the status. - // If we can, we own the status and should fire the event. - if (this.sessionStateLock.TryAcquireForDebuggerAbort()) - { - try - { - this.SessionState = PowerShellContextState.Aborting; - this.OnExecutionStatusChanged( - ExecutionStatus.Aborted, - null, - false); - } - finally - { - this.SessionState = PowerShellContextState.Ready; - this.sessionStateLock.ReleaseForDebuggerAbort(); - } - } - } - - /// - /// 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()).ConfigureAwait(false); - 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(); - } - } - - /// - /// Closes the runspace and any other resources being used - /// by this PowerShellContext. - /// - public void Close() - { - logger.LogTrace("Closing PowerShellContextService..."); - 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 Task GetRunspaceHandleAsync(bool isReadLine) - { - return this.GetRunspaceHandleImplAsync(isReadLine, CancellationToken.None); - } - - private Task GetRunspaceHandleImplAsync(bool isReadLine, CancellationToken cancellationToken) - { - return this.PromptNest.GetRunspaceHandleAsync(isReadLine, cancellationToken); - } - - 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.LogHandledException( - $"Caught {exitException.GetType().Name} while exiting {runspaceDetails.Location} runspace", exitException); - } - } - } - - internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) - { - Validate.IsNotNull("runspaceHandle", runspaceHandle); - - if (PromptNest.IsMainThreadBusy() || (runspaceHandle.IsReadLine && PromptNest.IsReadLineBusy())) - { - _ = 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() - { - this.logger.LogTrace("Entering nested prompt"); - - 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 Task SetWorkingDirectoryAsync(string path) - { - return this.SetWorkingDirectoryAsync(path, isPathAlreadyEscaped: 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) - { - Validate.IsNotNull(nameof(path), path); - this.InitialWorkingDirectory = path; - - if (!isPathAlreadyEscaped) - { - path = WildcardEscapePath(path); - } - - await ExecuteCommandAsync( - new PSCommand().AddCommand("Set-Location").AddParameter("Path", path), - errorMessages: null, - sendOutputToHost: false, - sendErrorToHost: false, - addToHistory: false).ConfigureAwait(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) - { - Validate.IsNotNull(nameof(path), path); - - 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) - { - Validate.IsNotNull(nameof(path), 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($"Session state was: {SessionState}, is now: {e.NewSessionState}, result: {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)); - } - - /// - /// TODO: This should somehow check if the server has actually started because we are - /// currently sending this notification before it has initialized, which is not allowed. - /// This might be the cause of our deadlock! - /// - private void PowerShellContext_RunspaceChangedAsync(object sender, RunspaceChangedEventArgs e) - { - _languageServer?.SendNotification( - "powerShell/runspaceChanged", - new MinifiedRunspaceDetails(e.NewRunspace)); - } - - - // TODO: Refactor this, RunspaceDetails, PowerShellVersion, and PowerShellVersionDetails - // It's odd that this is 4 different types. - // P.S. MinifiedRunspaceDetails use to be called RunspaceDetails... as in, there were 2 DIFFERENT - // RunspaceDetails types in this codebase but I've changed it to be minified since the type is - // slightly simpler than the other RunspaceDetails. - internal class MinifiedRunspaceDetails - { - public PowerShellVersion PowerShellVersion { get; set; } - - public RunspaceLocation RunspaceType { get; set; } - - public string ConnectionString { get; set; } - - public MinifiedRunspaceDetails() - { - } - - public MinifiedRunspaceDetails(RunspaceDetails eventArgs) - { - Validate.IsNotNull(nameof(eventArgs), eventArgs); - - this.PowerShellVersion = new PowerShellVersion(eventArgs.PowerShellVersion); - this.RunspaceType = eventArgs.Location; - this.ConnectionString = eventArgs.ConnectionString; - } - } - - /// - /// Event hook on the PowerShell context to listen for changes in script execution status - /// - /// - /// TODO: This should somehow check if the server has actually started because we are - /// currently sending this notification before it has initialized, which is not allowed. - /// - /// the PowerShell context sending the execution event - /// details of the execution status change - private void PowerShellContext_ExecutionStatusChangedAsync(object sender, ExecutionStatusChangedEventArgs e) - { - // The cancelling of the prompt (PSReadLine) causes an ExecutionStatus.Aborted to be sent after every - // actual execution (ExecutionStatus.Running) on the pipeline. We ignore that event since it's counterintuitive to - // the goal of this method which is to send updates when the pipeline is actually running something. - // In the event that we don't know if it was a ReadLine cancel, we default to sending the notification. - var options = e?.ExecutionOptions; - if (options == null || !options.IsReadLine) - { - _languageServer?.SendNotification( - "powerShell/executionStatusChanged", - e); - } - } - - #endregion - - #region Private Methods - - private IEnumerable ExecuteCommandInDebugger(PSCommand psCommand, bool sendOutputToHost) - { - 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 Task WriteExceptionToHostAsync(RuntimeException e) - { - var psObject = PSObject.AsPSObject(e.ErrorRecord); - - // Used to write ErrorRecords to the Error stream so they are rendered in the console correctly. - if (VersionUtils.IsPS7OrGreater) - { - s_writeStreamProperty.SetValue(psObject, s_errorStreamValue); - } - else - { - var note = new PSNoteProperty("writeErrorStream", true); - psObject.Properties.Add(note); - } - - return ExecuteCommandAsync(new PSCommand().AddCommand("Microsoft.PowerShell.Core\\Out-Default").AddParameter("InputObject", psObject)); - } - - 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) - { - PowerShellExecutionResult executionResult = PowerShellExecutionResult.NotFinished; - - PowerShellContextState newState; - 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(); - } - - /// - /// This function restores the execution policy for the process by examining the user's - /// execution policy hierarchy. We do this because the process policy will always be set to - /// Bypass when initializing our runspaces. - /// - internal void RestoreExecutionPolicy() - { - // Execution policy is a Windows-only feature. - if (!VersionUtils.IsWindows) - { - return; - } - - this.logger.LogTrace("Restoring execution policy..."); - - // We want to get the list hierarchy of execution policies - // Calling the cmdlet is the simplest way to do that - IReadOnlyList policies = this.powerShell - .AddCommand("Microsoft.PowerShell.Security\\Get-ExecutionPolicy") - .AddParameter("-List") - .Invoke(); - - this.powerShell.Commands.Clear(); - - // The policies come out in the following order: - // - MachinePolicy - // - UserPolicy - // - Process - // - CurrentUser - // - LocalMachine - // We want to ignore policy settings, since we'll already have those anyway. - // Then we need to look at the CurrentUser setting, and then the LocalMachine setting. - // - // Get-ExecutionPolicy -List emits PSObjects with Scope and ExecutionPolicy note properties - // set to expected values, so we must sift through those. - - ExecutionPolicy policyToSet = ExecutionPolicy.Bypass; - var currentUserPolicy = (ExecutionPolicy)policies[policies.Count - 2].Members["ExecutionPolicy"].Value; - if (currentUserPolicy != ExecutionPolicy.Undefined) - { - policyToSet = currentUserPolicy; - } - else - { - var localMachinePolicy = (ExecutionPolicy)policies[policies.Count - 1].Members["ExecutionPolicy"].Value; - if (localMachinePolicy != ExecutionPolicy.Undefined) - { - policyToSet = localMachinePolicy; - } - } - - // If there's nothing to do, save ourselves a PowerShell invocation - if (policyToSet == ExecutionPolicy.Bypass) - { - this.logger.LogTrace("Execution policy already set to Bypass. Skipping execution policy set"); - return; - } - - // Finally set the inherited execution policy - this.logger.LogTrace($"Setting execution policy to {policyToSet}"); - try - { - this.powerShell - .AddCommand("Microsoft.PowerShell.Security\\Set-ExecutionPolicy") - .AddParameter("Scope", ExecutionPolicyScope.Process) - .AddParameter("ExecutionPolicy", policyToSet) - .AddParameter("Force") - .Invoke(); - } - catch (CmdletInvocationException e) - { - this.logger.LogHandledException( - $"Error occurred calling 'Set-ExecutionPolicy -Scope Process -ExecutionPolicy {policyToSet} -Force'", e); - } - finally - { - this.powerShell.Commands.Clear(); - } - } - - private SessionDetails GetSessionDetails(Func invokeAction) - { - try - { - this.mostRecentSessionDetails = - new SessionDetails( - invokeAction( - SessionDetails.GetDetailsCommand())); - - return this.mostRecentSessionDetails; - } - catch (RuntimeException e) - { - this.logger.LogHandledException("Runtime exception occurred while gathering runspace info", e); - } - 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().ConfigureAwait(false)) - { - 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(ProfilePathInfo 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( - $"Setting $profile variable in runspace. Current user host profile path: {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; - } - } - - private static IEnumerable GetLoadableProfilePaths(ProfilePathInfo profilePaths) - { - if (profilePaths == null) - { - yield break; - } - - foreach (string path in new [] { profilePaths.AllUsersAllHosts, profilePaths.AllUsersCurrentHost, profilePaths.CurrentUserAllHosts, profilePaths.CurrentUserCurrentHost }) - { - if (path != null && File.Exists(path)) - { - yield return path; - } - } - } - - #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; - } - - void availabilityChangedHandler(object runspace, RunspaceAvailabilityEventArgs eventArgs) - { - if (eventArgs.RunspaceAvailability != RunspaceAvailability.Available || - this.versionSpecificOperations.IsDebuggerStopped(this.PromptNest, (Runspace)runspace)) - { - return; - } - - ((Runspace)runspace).AvailabilityChanged -= availabilityChangedHandler; - Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 0); - this.ConsoleReader?.StartCommandLoop(); - } - - this.CurrentRunspace.Runspace.AvailabilityChanged += availabilityChangedHandler; - Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 1); - } - - private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) - { - // We maintain the current stop event args so that we can use it in the DebugServer to fire the "stopped" event - // when the DebugServer is fully started. - CurrentDebuggerStopEventArgs = e; - - // If this event has fired but the LSP debug server is not active, it means that the - // PowerShell debugger has started some other way (most likely an existing PSBreakPoint - // was executed). So not only do we have to start the server, but later we will be - // responsible for stopping it too. - if (!IsDebugServerActive) - { - logger.LogDebug("Starting LSP debugger because PowerShell debugger is running!"); - OwnsDebugServerState = true; - _languageServer?.SendNotification("powerShell/startDebugger"); - } - - // We've hit a breakpoint so go to a new line so that the prompt can be rendered. - this.WriteOutput("", includeNewLine: true); - - 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()); - - // Since we are no longer at a breakpoint, we set this to null. - CurrentDebuggerStopEventArgs = null; - - // 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)) - { - // Since we are no longer at a breakpoint, we set this to null. - CurrentDebuggerStopEventArgs = null; - - 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? - this.logger.LogError($"Unhandled TaskIndex: {taskIndex}"); - } - } - - 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) - { - 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 - - /// - /// Encapsulates the locking semantics hacked together for debugging to work. - /// This allows ExecuteCommandAsync locking to work "re-entrantly", - /// while making sure that a debug abort won't corrupt state. - /// - private class SessionStateLock - { - /// - /// The actual lock to acquire to modify the session state of the PowerShellContextService. - /// - private readonly SemaphoreSlim _sessionStateLock; - - /// - /// A lock used by this class to ensure that count incrementing and session state locking happens atomically. - /// - private readonly SemaphoreSlim _internalLock; - - /// - /// A count of how re-entrant the current execute command lock call is, - /// so we can effectively use it as a two-way semaphore. - /// - private int _executeCommandLockCount; - - public SessionStateLock() - { - _sessionStateLock = AsyncUtils.CreateSimpleLockingSemaphore(); - _internalLock = AsyncUtils.CreateSimpleLockingSemaphore(); - _executeCommandLockCount = 0; - } - - public async Task AcquireForExecuteCommandAsync() - { - // Algorithm here is: - // - Acquire the internal lock to keep operations atomic - // - Increment the number of lock holders - // - If we're the only one, acquire the lock - // - Release the internal lock - - await _internalLock.WaitAsync().ConfigureAwait(false); - try - { - if (_executeCommandLockCount++ == 0) - { - await _sessionStateLock.WaitAsync().ConfigureAwait(false); - } - } - finally - { - _internalLock.Release(); - } - } - - public bool TryAcquireForDebuggerAbort() - { - return _sessionStateLock.Wait(0); - } - - public async Task ReleaseForExecuteCommand() - { - // Algorithm here is the opposite of the acquisition algorithm: - // - Acquire the internal lock to ensure the operation is atomic - // - Decrement the lock holder count - // - If we were the last ones, release the lock - // - Release the internal lock - - await _internalLock.WaitAsync().ConfigureAwait(false); - try - { - if (--_executeCommandLockCount == 0) - { - _sessionStateLock.Release(); - } - } - finally - { - _internalLock.Release(); - } - } - - public void ReleaseForDebuggerAbort() - { - _sessionStateLock.Release(); - } - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs deleted file mode 100644 index 09f795cc4..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - using Microsoft.Extensions.Logging; - using Microsoft.PowerShell.EditorServices.Logging; - using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; - 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 = Array.Empty(); - - 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).ConfigureAwait(false); - - // Verify all the breakpoints and return them - foreach (var breakpoint in breakpoints) - { - breakpoint.Verified = true; - } - - return breakpoints.ToArray(); - } - - 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 - .AddCommand("Microsoft.PowerShell.Utility\\Write-Host") - .AddArgument("Gathering DSC resource paths, this may take a while...") - .Invoke(); - - // Get the list of DSC resource paths - powerShell.Commands.Clear(); - powerShell - .AddCommand("Get-DscResource") - .AddCommand("Select-Object") - .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/Services/PowerShellContext/Session/ExecutionOptions.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionOptions.cs deleted file mode 100644 index 05535d516..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionOptions.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Defines options for the execution of a command. - /// - internal 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; } - - /// - /// If this is set, we will use this string for history and writing to the host - /// instead of grabbing the command from the PSCommand. - /// - public string InputString { get; set; } - - /// - /// If this is set, we will use this string for history and writing to the host - /// instead of grabbing the command from the PSCommand. - /// - public bool UseNewScope { 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/Services/PowerShellContext/Session/ExecutionStatus.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionStatus.cs deleted file mode 100644 index fb88d6013..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionStatus.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Enumerates the possible execution results that can occur after - /// executing a command or script. - /// - internal 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/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs deleted file mode 100644 index bb0cc6cc7..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Contains details about an executed - /// - internal 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/Services/PowerShellContext/Session/ExecutionTarget.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionTarget.cs deleted file mode 100644 index 097b53426..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionTarget.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// 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/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs deleted file mode 100644 index 9cbc1fef7..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Hosting; -using System; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides an implementation of the PSHost class for the - /// ConsoleService and routes its calls to an IConsoleHost - /// implementation. - /// - internal class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession - { - #region Private Fields - - private ILogger Logger; - private HostStartupInfo 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, - HostStartupInfo 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(nameof(hostUserInterface)); - _hostUserInterface = hostUserInterface; - } - - /// - /// The Accent Color for Formatting - /// - public ConsoleColor FormatAccentColor - { - get - { return _hostUserInterface.FormatAccentColor; } - set - { _hostUserInterface.FormatAccentColor = value; } - } - - /// - /// The Accent Color for Error - /// - public ConsoleColor ErrorAccentColor - { - get - { return _hostUserInterface.ErrorAccentColor; } - set - { _hostUserInterface.ErrorAccentColor = value; } - } - - /// - /// 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/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs deleted file mode 100644 index 744cd0c41..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs +++ /dev/null @@ -1,1071 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -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 System.Threading; -using System.Globalization; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Logging; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides an implementation of the PSHostUserInterface class - /// for the ConsoleService and routes its calls to an IConsoleHost - /// implementation. - /// - internal 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 - - /// - /// Returns true if the host supports VT100 output codes. - /// - public override bool SupportsVirtualTerminal => false; - - /// - /// 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(); - Task.Run(() => 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, - PSObject.AsPSObject(keyValuePair.Value ?? string.Empty)); - } - } - - // 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) - { - // PowerShell's ConsoleHost also skips over empty lines: - // https://github.com/PowerShell/PowerShell/blob/8e683972284a5a7f773ea6d027d9aac14d7e7524/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostUserInterface.cs#L1334-L1337 - if (string.IsNullOrEmpty(value)) - { - return; - } - - 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).ConfigureAwait(false) && - this.lastPromptLocation.Y == await ConsoleProxy.GetCursorTopAsync(cancellationToken).ConfigureAwait(false)) - { - 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().AddCommand("prompt"); - - cancellationToken.ThrowIfCancellationRequested(); - string promptString = - (await this.powerShellContext.ExecuteCommandAsync( - promptCommand, false, false, cancellationToken).ConfigureAwait(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).ConfigureAwait(false), - await ConsoleProxy.GetCursorTopAsync(cancellationToken).ConfigureAwait(false)); - } - - private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs) - { - // TODO: What do we display when we don't know why we stopped? - - if (eventArgs.Breakpoints.Count > 0) - { - // The breakpoint classes have nice ToString output so use that - this.WriteOutput( - Environment.NewLine + $"Hit {eventArgs.Breakpoints[0].ToString()}\n", - true, - OutputType.Normal, - ConsoleColor.Blue); - } - } - - internal static ConsoleColor BackgroundColor { get; set; } - - internal ConsoleColor FormatAccentColor { get; set; } = ConsoleColor.Green; - internal ConsoleColor ErrorAccentColor { get; set; } = ConsoleColor.Cyan; - - 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 virtual ConsoleColor ProgressForegroundColor { get; set; } = ConsoleColor.Yellow; - internal virtual ConsoleColor ProgressBackgroundColor { get; set; } = ConsoleColor.DarkCyan; - - private async Task StartReplLoopAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - string commandString = null; - - try - { - await this.WritePromptStringToHostAsync(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - break; - } - - try - { - commandString = await this.ReadCommandLineAsync(cancellationToken).ConfigureAwait(false); - } - 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 - { - // This supplies the newline in the Legacy ReadLine when executing code in the terminal via hitting the ENTER key - // or Ctrl+C. Without this, hitting ENTER with a no input looks like it does nothing (no new prompt is written) - // and also the output would show up on the same line as the code you wanted to execute (the prompt line). - // This is AlSO applied to PSReadLine for the Ctrl+C scenario which appears like it does nothing... - // TODO: This still gives an extra newline when you hit ENTER in the PSReadLine experience. We should figure - // out if there's any way to avoid that... but unfortunately, in both scenarios, we only see that empty - // string is returned. - if (!cancellationToken.IsCancellationRequested) - { - 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/Services/PowerShellContext/Session/Host/IHostInput.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/IHostInput.cs deleted file mode 100644 index 3a99f81c3..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/IHostInput.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides methods for integrating with the host's input system. - /// - internal 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(); - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/IHostOutput.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/IHostOutput.cs deleted file mode 100644 index 732386fd9..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/IHostOutput.cs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides a simplified interface for writing output to a - /// PowerShell host implementation. - /// - internal 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. - /// - internal 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/Services/PowerShellContext/Session/Host/PromptHandlers.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptHandlers.cs deleted file mode 100644 index 4bd29ba29..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptHandlers.cs +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Threading.Tasks; -using System.Threading; -using System.Security; -using Microsoft.Extensions.Logging; -using OmniSharp.Extensions.LanguageServer.Protocol.Server; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - internal class ProtocolChoicePromptHandler : ConsoleChoicePromptHandler - { - private readonly ILanguageServerFacade _languageServer; - private readonly IHostInput _hostInput; - private TaskCompletionSource _readLineTask; - - public ProtocolChoicePromptHandler( - ILanguageServerFacade languageServer, - IHostInput hostInput, - IHostOutput hostOutput, - ILogger logger) - : base(hostOutput, logger) - { - _languageServer = languageServer; - this._hostInput = hostInput; - this.hostOutput = hostOutput; - } - - protected override void ShowPrompt(PromptStyle promptStyle) - { - base.ShowPrompt(promptStyle); - - _languageServer.SendRequest( - "powerShell/showChoicePrompt", - new ShowChoicePromptRequest - { - IsMultiChoice = this.IsMultiChoice, - Caption = this.Caption, - Message = this.Message, - Choices = this.Choices, - DefaultChoices = this.DefaultChoices - }) - .Returning(CancellationToken.None) - .ContinueWith(HandlePromptResponse) - .ConfigureAwait(false); - } - - protected override Task ReadInputStringAsync(CancellationToken cancellationToken) - { - this._readLineTask = new TaskCompletionSource(); - return this._readLineTask.Task; - } - - private void HandlePromptResponse( - Task responseTask) - { - if (responseTask.IsCompleted) - { - ShowChoicePromptResponse response = responseTask.Result; - - if (!response.PromptCancelled) - { - this.hostOutput.WriteOutput( - response.ResponseText, - OutputType.Normal); - - this._readLineTask.TrySetResult(response.ResponseText); - } - else - { - // Cancel the current prompt - this._hostInput.SendControlC(); - } - } - else - { - if (responseTask.IsFaulted) - { - // Log the error - Logger.LogError( - "ShowChoicePrompt request failed with error:\r\n{0}", - responseTask.Exception.ToString()); - } - - // Cancel the current prompt - this._hostInput.SendControlC(); - } - - this._readLineTask = null; - } - } - - internal class ProtocolInputPromptHandler : ConsoleInputPromptHandler - { - private readonly ILanguageServerFacade _languageServer; - private readonly IHostInput hostInput; - private TaskCompletionSource readLineTask; - - public ProtocolInputPromptHandler( - ILanguageServerFacade languageServer, - IHostInput hostInput, - IHostOutput hostOutput, - ILogger logger) - : base(hostOutput, logger) - { - _languageServer = languageServer; - this.hostInput = hostInput; - this.hostOutput = hostOutput; - } - - protected override void ShowFieldPrompt(FieldDetails fieldDetails) - { - base.ShowFieldPrompt(fieldDetails); - - _languageServer.SendRequest( - "powerShell/showInputPrompt", - new ShowInputPromptRequest - { - Name = fieldDetails.Name, - Label = fieldDetails.Label - }).Returning(CancellationToken.None) - .ContinueWith(HandlePromptResponse) - .ConfigureAwait(false); - } - - protected override Task ReadInputStringAsync(CancellationToken cancellationToken) - { - this.readLineTask = new TaskCompletionSource(); - return this.readLineTask.Task; - } - - private void HandlePromptResponse( - Task responseTask) - { - if (responseTask.IsCompleted) - { - ShowInputPromptResponse response = responseTask.Result; - - if (!response.PromptCancelled) - { - this.hostOutput.WriteOutput( - response.ResponseText, - OutputType.Normal); - - this.readLineTask.TrySetResult(response.ResponseText); - } - else - { - // Cancel the current prompt - this.hostInput.SendControlC(); - } - } - else - { - if (responseTask.IsFaulted) - { - // Log the error - Logger.LogError( - "ShowInputPrompt request failed with error:\r\n{0}", - responseTask.Exception.ToString()); - } - - // Cancel the current prompt - this.hostInput.SendControlC(); - } - - this.readLineTask = null; - } - - protected override Task ReadSecureStringAsync(CancellationToken cancellationToken) - { - // TODO: Write a message to the console - throw new NotImplementedException(); - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs deleted file mode 100644 index 89203e5c1..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Extensions.Logging; -using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - internal class ProtocolPSHostUserInterface : EditorServicesPSHostUserInterface - { - #region Private Fields - - private readonly ILanguageServerFacade _languageServer; - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the ConsoleServicePSHostUserInterface - /// class with the given IConsoleHost implementation. - /// - /// - public ProtocolPSHostUserInterface( - ILanguageServerFacade languageServer, - PowerShellContextService powerShellContext, - ILogger logger) - : base ( - powerShellContext, - new SimplePSHostRawUserInterface(logger), - logger) - { - _languageServer = languageServer; - } - - #endregion - - /// - /// Writes output of the given type to the user interface with - /// the given foreground and background colors. Also includes - /// a newline if requested. - /// - /// - /// The output string to be written. - /// - /// - /// If true, a newline should be appended to the output's contents. - /// - /// - /// Specifies the type of output to be written. - /// - /// - /// Specifies the foreground color of the output to be written. - /// - /// - /// Specifies the background color of the output to be written. - /// - public override void WriteOutput( - string outputString, - bool includeNewLine, - OutputType outputType, - ConsoleColor foregroundColor, - ConsoleColor backgroundColor) - { - // TODO: Invoke the "output" notification! - } - - /// - /// 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) - { - // TODO: Send a new message. - } - - protected override Task ReadCommandLineAsync(CancellationToken cancellationToken) - { - // This currently does nothing because the "evaluate" request - // will cancel the current prompt and execute the user's - // script selection. - return new TaskCompletionSource().Task; - } - - protected override InputPromptHandler OnCreateInputPromptHandler() - { - return new ProtocolInputPromptHandler(_languageServer, this, this, this.Logger); - } - - protected override ChoicePromptHandler OnCreateChoicePromptHandler() - { - return new ProtocolChoicePromptHandler(_languageServer, this, this, this.Logger); - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs deleted file mode 100644 index 2ac77b94d..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Management.Automation.Host; -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides an simple implementation of the PSHostRawUserInterface class. - /// - internal 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/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs deleted file mode 100644 index 86d713a98..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Management.Automation.Host; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - using System.Management.Automation; - - /// - /// Provides an EditorServicesPSHostUserInterface implementation - /// that integrates with the user's terminal UI. - /// - internal class TerminalPSHostUserInterface : EditorServicesPSHostUserInterface - { - #region Private Fields - - private readonly PSHostUserInterface internalHostUI; - private readonly PSObject _internalHostPrivateData; - private readonly 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, - PSHost internalHost, - ILogger logger) - : base ( - powerShellContext, - new TerminalPSHostRawUserInterface(logger, internalHost), - logger) - { - internalHostUI = internalHost.UI; - _internalHostPrivateData = internalHost.PrivateData; - _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 (!IsNativeApplicationRunning) - { - // We'll handle Ctrl+C - args.Cancel = true; - SendControlC(); - } - }; - } - - #endregion - - /// - /// Returns true if the host supports VT100 output codes. - /// - public override bool SupportsVirtualTerminal => internalHostUI.SupportsVirtualTerminal; - - /// - /// Gets a value indicating whether writing progress is supported. - /// - internal protected override bool SupportsWriteProgress => true; - - /// - /// Gets and sets the value of progress foreground from the internal host since Progress is handled there. - /// - internal override ConsoleColor ProgressForegroundColor - { - get => (ConsoleColor)_internalHostPrivateData.Properties["ProgressForegroundColor"].Value; - set => _internalHostPrivateData.Properties["ProgressForegroundColor"].Value = value; - } - - /// - /// Gets and sets the value of progress background from the internal host since Progress is handled there. - /// - internal override ConsoleColor ProgressBackgroundColor - { - get => (ConsoleColor)_internalHostPrivateData.Properties["ProgressBackgroundColor"].Value; - set => _internalHostPrivateData.Properties["ProgressBackgroundColor"].Value = value; - } - - /// - /// 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 _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( - _consoleReadLine, - 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( - _consoleReadLine, - 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) - { - 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/Services/PowerShellContext/Session/IPromptContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/IPromptContext.cs deleted file mode 100644 index e0618ed04..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/IPromptContext.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides methods for interacting with implementations of ReadLine. - /// - internal 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/Services/PowerShellContext/Session/IVersionSpecificOperations.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/IVersionSpecificOperations.cs deleted file mode 100644 index cdc5f5507..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/IVersionSpecificOperations.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - 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/Services/PowerShellContext/Session/InvocationEventQueue.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/InvocationEventQueue.cs deleted file mode 100644 index 4d7a87fd0..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/InvocationEventQueue.cs +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -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.Services.PowerShellContext -{ - 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())).ConfigureAwait(false); - - try - { - return await request.Results.ConfigureAwait(false); - } - finally - { - await SetInvocationRequestAsync(request: null).ConfigureAwait(false); - } - } - - /// - /// 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(isReadLine: false, CancellationToken.None)) - { - pwsh.Runspace = _runspace; - invocationAction(pwsh); - } - }); - - await SetInvocationRequestAsync(request).ConfigureAwait(false); - try - { - await request.Task.ConfigureAwait(false); - } - finally - { - await SetInvocationRequestAsync(null).ConfigureAwait(false); - } - } - - private async Task WaitForExistingRequestAsync() - { - InvocationRequest existingRequest; - await _lock.WaitAsync().ConfigureAwait(false); - try - { - existingRequest = _invocationRequest; - if (existingRequest == null || existingRequest.Task.IsCompleted) - { - return; - } - } - finally - { - _lock.Release(); - } - - await existingRequest.Task.ConfigureAwait(false); - } - - private async Task SetInvocationRequestAsync(InvocationRequest request) - { - await WaitForExistingRequestAsync().ConfigureAwait(false); - await _lock.WaitAsync().ConfigureAwait(false); - 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 static 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/Services/PowerShellContext/Session/LegacyReadLineContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/LegacyReadLineContext.cs deleted file mode 100644 index ce77c4343..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/LegacyReadLineContext.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - internal class LegacyReadLineContext : IPromptContext - { - private readonly ConsoleReadLine _legacyReadLine; - - internal LegacyReadLineContext(PowerShellContextService powerShellContext) - { - _legacyReadLine = new ConsoleReadLine(powerShellContext); - } - - public Task AbortReadLineAsync() - { - return Task.FromResult(true); - } - - public Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) - { - return _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/Services/PowerShellContext/Session/OutputType.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/OutputType.cs deleted file mode 100644 index 6e69386ce..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/OutputType.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Enumerates the types of output lines that will be sent - /// to an IConsoleHost implementation. - /// - internal 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/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs deleted file mode 100644 index 3724a7269..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides details about output that has been written to the - /// PowerShell host. - /// - internal 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/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs deleted file mode 100644 index 62f5e21e2..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation.Runspaces; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - using System.IO; - using System.Management.Automation; - - internal class PSReadLinePromptContext : IPromptContext - { - private static readonly Lazy s_lazyInvokeReadLineForEditorServicesCmdletInfo = new Lazy(() => - { - var type = Type.GetType("Microsoft.PowerShell.EditorServices.Commands.InvokeReadLineForEditorServicesCommand, Microsoft.PowerShell.EditorServices.Hosting"); - return new CmdletInfo("__Invoke-ReadLineForEditorServices", type); - }); - - private static ExecutionOptions s_psrlExecutionOptions = new ExecutionOptions - { - WriteErrorsToHost = false, - WriteOutputToHost = false, - InterruptCommandPrompt = false, - AddToHistory = false, - IsReadLine = true, - }; - - private readonly PowerShellContextService _powerShellContext; - - private readonly PromptNest _promptNest; - - private readonly InvocationEventQueue _invocationEventQueue; - - private readonly ConsoleReadLine _consoleReadLine; - - private readonly PSReadLineProxy _readLineProxy; - - private CancellationTokenSource _readLineCancellationSource; - - internal PSReadLinePromptContext( - PowerShellContextService powerShellContext, - PromptNest promptNest, - InvocationEventQueue invocationEventQueue, - PSReadLineProxy readLineProxy) - { - _promptNest = promptNest; - _powerShellContext = powerShellContext; - _invocationEventQueue = invocationEventQueue; - _consoleReadLine = new ConsoleReadLine(powerShellContext); - _readLineProxy = readLineProxy; - - _readLineProxy.OverrideReadKey( - intercept => ConsoleProxy.SafeReadKey( - intercept, - _readLineCancellationSource.Token)); - } - - internal static bool TryGetPSReadLineProxy( - ILogger logger, - Runspace runspace, - string bundledModulePath, - out PSReadLineProxy readLineProxy) - { - readLineProxy = null; - logger.LogTrace("Attempting to load PSReadLine"); - using (var pwsh = PowerShell.Create()) - { - pwsh.Runspace = runspace; - pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module") - .AddParameter("Name", Path.Combine(bundledModulePath, "PSReadLine")) - .Invoke(); - - if (pwsh.HadErrors) - { - logger.LogWarning("PSConsoleReadline type not found: {Reason}", pwsh.Streams.Error[0].ToString()); - return false; - } - - var psReadLineType = Type.GetType("Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2"); - - if (psReadLineType == null) - { - // NOTE: For some reason `Type.GetType(...)` can fail to find the type, - // and in that case, this search through the `AppDomain` for some reason will succeed. - // It's slower, but only happens when needed. - logger.LogTrace("PSConsoleReadline type not found using Type.GetType(), searching all loaded assemblies..."); - psReadLineType = AppDomain.CurrentDomain - .GetAssemblies() - .FirstOrDefault(asm => asm.GetName().Name.Equals("Microsoft.PowerShell.PSReadLine2")) - ?.ExportedTypes - ?.FirstOrDefault(type => type.FullName.Equals("Microsoft.PowerShell.PSConsoleReadLine")); - if (psReadLineType == null) - { - logger.LogWarning("PSConsoleReadLine type not found anywhere!"); - return false; - } - } - - try - { - readLineProxy = new PSReadLineProxy(psReadLineType, logger); - } - catch (InvalidOperationException e) - { - // 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. - logger.LogWarning("PSReadLineProxy unable to be initialized: {Reason}", e); - 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(); - } - - if (!isCommandLine) - { - return await _consoleReadLine.InvokeLegacyReadLineAsync( - isCommandLine: false, - _readLineCancellationSource.Token).ConfigureAwait(false); - } - - var readLineCommand = new PSCommand() - .AddCommand(s_lazyInvokeReadLineForEditorServicesCmdletInfo.Value) - .AddParameter("CancellationToken", _readLineCancellationSource.Token); - - IEnumerable readLineResults = await _powerShellContext.ExecuteCommandAsync( - readLineCommand, - errorMessages: null, - s_psrlExecutionOptions, - // NOTE: This command should always be allowed to complete, as the command itself - // has a linked cancellation token such that PSReadLine will be correctly cancelled. - CancellationToken.None).ConfigureAwait(false); - - string line = readLineResults.FirstOrDefault(); - - return cancellationToken.IsCancellationRequested - ? string.Empty - : line; - } - - public void AbortReadLine() - { - if (_readLineCancellationSource == null) - { - return; - } - - _readLineCancellationSource.Cancel(); - - WaitForReadLineExit(); - } - - public async Task AbortReadLineAsync() { - if (_readLineCancellationSource == null) - { - return; - } - - _readLineCancellationSource.Cancel(); - - await WaitForReadLineExitAsync().ConfigureAwait(false); - } - - public void WaitForReadLineExit() - { - using (_promptNest.GetRunspaceHandle(isReadLine: true, CancellationToken.None)) - { } - } - - public async Task WaitForReadLineExitAsync() { - using (await _promptNest.GetRunspaceHandleAsync(isReadLine: true, CancellationToken.None).ConfigureAwait(false)) - { } - } - - public void AddToHistory(string command) - { - _readLineProxy.AddToHistory(command); - } - - public void ForcePSEventHandling() - { - _readLineProxy.ForcePSEventHandling(); - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLineProxy.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLineProxy.cs deleted file mode 100644 index 69cf90f5f..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLineProxy.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Reflection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - 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/Services/PowerShellContext/Session/PipelineExecutionRequest.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PipelineExecutionRequest.cs deleted file mode 100644 index 21fdaaf01..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PipelineExecutionRequest.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Management.Automation; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - 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).ConfigureAwait(false); - - _ = Task.Run(() => _resultsTask.SetResult(results)); - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShell5Operations.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShell5Operations.cs deleted file mode 100644 index ec40599d2..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShell5Operations.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -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.Services.PowerShellContext -{ - 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/Services/PowerShellContext/Session/PowerShellContextState.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellContextState.cs deleted file mode 100644 index 00849b045..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellContextState.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Enumerates the possible states for a PowerShellContext. - /// - internal 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/Services/PowerShellContext/Session/PowerShellExecutionResult.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellExecutionResult.cs deleted file mode 100644 index 7ce502c1b..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellExecutionResult.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Enumerates the possible execution results that can occur after - /// executing a command or script. - /// - internal 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/Services/PowerShellContext/Session/ProgressDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ProgressDetails.cs deleted file mode 100644 index 86a06b83b..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/ProgressDetails.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Management.Automation; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides details about the progress of a particular activity. - /// - internal 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/Services/PowerShellContext/Session/PromptNest.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNest.cs deleted file mode 100644 index 208353632..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNest.cs +++ /dev/null @@ -1,562 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - using System; - using System.Management.Automation; - - /// - /// Represents the stack of contexts in which PowerShell commands can be invoked. - /// - internal class PromptNest : IDisposable - { - private readonly ConcurrentStack _frameStack; - - private readonly PromptNestFrame _readLineFrame; - - private readonly IVersionSpecificOperations _versionSpecificOperations; - - private readonly object _syncObject = new object(); - - private readonly object _disposeSyncObject = new object(); - - private IHostInput _consoleReader; - - private PowerShellContextService _powerShellContext; - - private bool _isDisposed; - - /// - /// 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(bool isReadLine, CancellationToken cancellationToken) - { - 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(isReadLine: false, cancellationToken); - } - - return GetRunspaceHandleImpl(isReadLine, cancellationToken); - } - - - /// - /// 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(bool isReadLine, CancellationToken cancellationToken) - { - 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(isReadLine: false, cancellationToken).ConfigureAwait(false); - } - - return await GetRunspaceHandleImplAsync(isReadLine, cancellationToken).ConfigureAwait(false); - } - - /// - /// 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).ConfigureAwait(false); - if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) - { - await ReleaseRunspaceHandleImplAsync(isReadLine: false).ConfigureAwait(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).ConfigureAwait(false); - } - finally - { - await currentFrame.WaitForFrameExitAsync(CancellationToken.None).ConfigureAwait(false); - } - } - - /// - /// 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).ConfigureAwait(false); - } - } - - /// - /// 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).ConfigureAwait(false); - } - - /// - /// 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).ConfigureAwait(false); - } - - private AsyncQueue NewHandleQueue() - { - var queue = new AsyncQueue(); - queue.Enqueue(new RunspaceHandle(_powerShellContext)); - return queue; - } - - private RunspaceHandle GetRunspaceHandleImpl(bool isReadLine, CancellationToken cancellationToken) - { - if (isReadLine) - { - return _readLineFrame.Queue.Dequeue(cancellationToken); - } - - return CurrentFrame.Queue.Dequeue(cancellationToken); - } - - private async Task GetRunspaceHandleImplAsync(bool isReadLine, CancellationToken cancellationToken) - { - if (isReadLine) - { - return await _readLineFrame.Queue.DequeueAsync(cancellationToken).ConfigureAwait(false); - } - - return await CurrentFrame.Queue.DequeueAsync(cancellationToken).ConfigureAwait(false); - } - - 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)).ConfigureAwait(false); - return; - } - - await CurrentFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, false)).ConfigureAwait(false); - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrame.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrame.cs deleted file mode 100644 index a23d87d8c..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrame.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - 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).ConfigureAwait(false); - _frameExited.Release(); - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrameType.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrameType.cs deleted file mode 100644 index 34246b576..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrameType.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - [Flags] - internal enum PromptNestFrameType - { - Normal = 0, - - NestedPrompt = 1, - - Debug = 2, - - Remote = 4 - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceDetails.cs deleted file mode 100644 index f5aa8d4b0..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceDetails.cs +++ /dev/null @@ -1,321 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.CSharp.RuntimeBinder; -using System; -using System.Management.Automation.Runspaces; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Specifies the possible types of a runspace. - /// - internal 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. - /// - internal 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. - /// - internal 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, - useLocalScope: true); - - // 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/Services/PowerShellContext/Session/RunspaceHandle.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceHandle.cs deleted file mode 100644 index 24c5f1df5..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceHandle.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Management.Automation.Host; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides a handle to the runspace that is managed by - /// a PowerShellContext. The holder of this handle. - /// - internal 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/Services/PowerShellContext/Session/SessionDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/SessionDetails.cs deleted file mode 100644 index 0f00d2a28..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/SessionDetails.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Management.Automation; -using System.Collections; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides details about the current PowerShell session. - /// - internal 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 }", - useLocalScope: true); - - return infoCommand; - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs deleted file mode 100644 index 422934f1d..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides details about a change in state of a PowerShellContext. - /// - internal 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/Services/PowerShellContext/Session/ThreadController.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ThreadController.cs deleted file mode 100644 index 2a8b83bbd..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/ThreadController.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -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.Services.PowerShellContext -{ - /// - /// 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).ConfigureAwait(false); - return await executionRequest.Results.ConfigureAwait(false); - } - - /// - /// 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 Task TakeExecutionRequestAsync() - { - return 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/Services/Symbols/ScriptDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Services/Symbols/ScriptDocumentSymbolProvider.cs index 89e5f5837..8399b943b 100644 --- a/src/PowerShellEditorServices/Services/Symbols/ScriptDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Services/Symbols/ScriptDocumentSymbolProvider.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Services.TextDocument; diff --git a/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs index 4498c54cf..47b5e40a4 100644 --- a/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs @@ -4,7 +4,9 @@ using System.Diagnostics; using System.Management.Automation; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; namespace Microsoft.PowerShell.EditorServices.Services.Symbols { @@ -36,9 +38,10 @@ internal class SymbolDetails #region Constructors - static internal async Task CreateAsync( + internal static async Task CreateAsync( SymbolReference symbolReference, - PowerShellContextService powerShellContext) + IRunspaceInfo currentRunspace, + IInternalPowerShellExecutionService executionService) { SymbolDetails symbolDetails = new SymbolDetails { @@ -50,14 +53,15 @@ static internal async Task CreateAsync( case SymbolType.Function: CommandInfo commandInfo = await CommandHelpers.GetCommandInfoAsync( symbolReference.SymbolName, - powerShellContext).ConfigureAwait(false); + currentRunspace, + executionService).ConfigureAwait(false); if (commandInfo != null) { symbolDetails.Documentation = await CommandHelpers.GetCommandSynopsisAsync( commandInfo, - powerShellContext).ConfigureAwait(false); + executionService).ConfigureAwait(false); if (commandInfo.CommandType == CommandTypes.Application) { diff --git a/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs index 071945ca7..e110b5ef2 100644 --- a/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs @@ -15,7 +15,9 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.CodeLenses; using Microsoft.PowerShell.EditorServices.Logging; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; @@ -31,7 +33,8 @@ internal class SymbolsService #region Private Fields private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; + private readonly IRunspaceContext _runspaceContext; + private readonly IInternalPowerShellExecutionService _executionService; private readonly WorkspaceService _workspaceService; private readonly ConcurrentDictionary _codeLensProviders; @@ -48,12 +51,14 @@ internal class SymbolsService /// An ILoggerFactory implementation used for writing log messages. public SymbolsService( ILoggerFactory factory, - PowerShellContextService powerShellContextService, + IRunspaceContext runspaceContext, + IInternalPowerShellExecutionService executionService, WorkspaceService workspaceService, ConfigurationService configurationService) { _logger = factory.CreateLogger(); - _powerShellContextService = powerShellContextService; + _runspaceContext = runspaceContext; + _executionService = executionService; _workspaceService = workspaceService; _codeLensProviders = new ConcurrentDictionary(); @@ -320,7 +325,8 @@ public async Task FindSymbolDetailsAtLocationAsync( symbolReference.FilePath = scriptFile.FilePath; SymbolDetails symbolDetails = await SymbolDetails.CreateAsync( symbolReference, - _powerShellContextService).ConfigureAwait(false); + _runspaceContext.CurrentRunspace, + _executionService).ConfigureAwait(false); return symbolDetails; } @@ -335,8 +341,7 @@ public async Task FindSymbolDetailsAtLocationAsync( public async Task FindParameterSetsInFileAsync( ScriptFile file, int lineNumber, - int columnNumber, - PowerShellContextService powerShellContext) + int columnNumber) { SymbolReference foundSymbol = AstOperations.FindCommandAtPosition( @@ -356,7 +361,8 @@ public async Task FindParameterSetsInFileAsync( CommandInfo commandInfo = await CommandHelpers.GetCommandInfoAsync( foundSymbol.SymbolName, - powerShellContext).ConfigureAwait(false); + _runspaceContext.CurrentRunspace, + _executionService).ConfigureAwait(false); if (commandInfo == null) { @@ -472,7 +478,8 @@ public async Task GetDefinitionOfSymbolAsync( CommandInfo cmdInfo = await CommandHelpers.GetCommandInfoAsync( foundSymbol.SymbolName, - _powerShellContextService).ConfigureAwait(false); + _runspaceContext.CurrentRunspace, + _executionService).ConfigureAwait(false); foundDefinition = FindDeclarationForBuiltinCommand( diff --git a/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs index 2251c56b3..f312352f1 100644 --- a/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs @@ -5,14 +5,15 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; using System.Management.Automation; using System.Management.Automation.Language; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; -using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; namespace Microsoft.PowerShell.EditorServices.Services.Symbols { @@ -21,15 +22,26 @@ namespace Microsoft.PowerShell.EditorServices.Services.Symbols /// internal static class AstOperations { - // TODO: When netstandard is upgraded to 2.0, see if - // Delegate.CreateDelegate can be used here instead - private static readonly MethodInfo s_extentCloneWithNewOffset = typeof(PSObject).Assembly - .GetType("System.Management.Automation.Language.InternalScriptPosition") - .GetMethod("CloneWithNewOffset", BindingFlags.Instance | BindingFlags.NonPublic); + private static readonly Func s_clonePositionWithNewOffset; + static AstOperations() + { + Type internalScriptPositionType = typeof(PSObject).GetTypeInfo().Assembly + .GetType("System.Management.Automation.Language.InternalScriptPosition"); + + MethodInfo cloneWithNewOffsetMethod = internalScriptPositionType.GetMethod("CloneWithNewOffset", BindingFlags.Instance | BindingFlags.NonPublic); - private static readonly SemaphoreSlim s_completionHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + ParameterExpression originalPosition = Expression.Parameter(typeof(IScriptPosition)); + ParameterExpression newOffset = Expression.Parameter(typeof(int)); + + var parameters = new ParameterExpression[] { originalPosition, newOffset }; + s_clonePositionWithNewOffset = Expression.Lambda>( + Expression.Call( + Expression.Convert(originalPosition, internalScriptPositionType), + cloneWithNewOffsetMethod, + newOffset), + parameters).Compile(); + } - // TODO: BRING THIS BACK /// /// Gets completions for the symbol found in the Ast at /// the given file offset. @@ -58,83 +70,42 @@ public static async Task GetCompletionsAsync( Ast scriptAst, Token[] currentTokens, int fileOffset, - PowerShellContextService powerShellContext, + IInternalPowerShellExecutionService executionService, ILogger logger, CancellationToken cancellationToken) { - if (!s_completionHandle.Wait(0, CancellationToken.None)) - { - return null; - } - - try - { - IScriptPosition cursorPosition = (IScriptPosition)s_extentCloneWithNewOffset.Invoke( - scriptAst.Extent.StartScriptPosition, - new object[] { fileOffset }); - - logger.LogTrace( - string.Format( - "Getting completions at offset {0} (line: {1}, column: {2})", - fileOffset, - cursorPosition.LineNumber, - cursorPosition.ColumnNumber)); - - if (!powerShellContext.IsAvailable) - { - return null; - } - - var stopwatch = new Stopwatch(); - - // If the current runspace is out of process we can use - // CommandCompletion.CompleteInput because PSReadLine won't be taking up the - // main runspace. - if (powerShellContext.IsCurrentRunspaceOutOfProcess()) + IScriptPosition cursorPosition = s_clonePositionWithNewOffset(scriptAst.Extent.StartScriptPosition, fileOffset); + + logger.LogTrace( + string.Format( + "Getting completions at offset {0} (line: {1}, column: {2})", + fileOffset, + cursorPosition.LineNumber, + cursorPosition.ColumnNumber)); + + var stopwatch = new Stopwatch(); + + CommandCompletion commandCompletion = null; + await executionService.ExecuteDelegateAsync( + representation: "CompleteInput", + new ExecutionOptions { Priority = ExecutionPriority.Next }, + (pwsh, cancellationToken) => { - using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken).ConfigureAwait(false)) - using (System.Management.Automation.PowerShell powerShell = System.Management.Automation.PowerShell.Create()) - { - powerShell.Runspace = runspaceHandle.Runspace; - stopwatch.Start(); - try - { - return CommandCompletion.CompleteInput( - scriptAst, - currentTokens, - cursorPosition, - options: null, - powershell: powerShell); - } - finally - { - stopwatch.Stop(); - logger.LogTrace($"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - } - } - } - - CommandCompletion commandCompletion = null; - await powerShellContext.InvokeOnPipelineThreadAsync( - pwsh => - { - stopwatch.Start(); - commandCompletion = CommandCompletion.CompleteInput( - scriptAst, - currentTokens, - cursorPosition, - options: null, - powershell: pwsh); - }).ConfigureAwait(false); - stopwatch.Stop(); - logger.LogTrace($"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - - return commandCompletion; - } - finally - { - s_completionHandle.Release(); - } + stopwatch.Start(); + commandCompletion = CommandCompletion.CompleteInput( + scriptAst, + currentTokens, + cursorPosition, + options: null, + powershell: pwsh); + }, + cancellationToken) + .ConfigureAwait(false); + + stopwatch.Stop(); + logger.LogTrace($"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + + return commandCompletion; } /// diff --git a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor2.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor2.cs index 545d56ebb..b7949b096 100644 --- a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor2.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor2.cs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Generic; -using System.Management.Automation.Language; - namespace Microsoft.PowerShell.EditorServices.Services.Symbols { // TODO: Restore this when we figure out how to support multiple diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ITemplateHandlers.cs b/src/PowerShellEditorServices/Services/Template/Handlers/ITemplateHandlers.cs similarity index 97% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ITemplateHandlers.cs rename to src/PowerShellEditorServices/Services/Template/Handlers/ITemplateHandlers.cs index 0f4a04742..88ae40789 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ITemplateHandlers.cs +++ b/src/PowerShellEditorServices/Services/Template/Handlers/ITemplateHandlers.cs @@ -4,7 +4,7 @@ using MediatR; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Handlers +namespace Microsoft.PowerShell.EditorServices.Services.Template { [Serial] [Method("powerShell/getProjectTemplates", Direction.ClientToServer)] diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/TemplateHandlers.cs b/src/PowerShellEditorServices/Services/Template/Handlers/TemplateHandlers.cs similarity index 95% rename from src/PowerShellEditorServices/Services/PowerShellContext/Handlers/TemplateHandlers.cs rename to src/PowerShellEditorServices/Services/Template/Handlers/TemplateHandlers.cs index 1270daf84..57f7552ce 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/TemplateHandlers.cs +++ b/src/PowerShellEditorServices/Services/Template/Handlers/TemplateHandlers.cs @@ -6,9 +6,8 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Logging; -using Microsoft.PowerShell.EditorServices.Services; -namespace Microsoft.PowerShell.EditorServices.Handlers +namespace Microsoft.PowerShell.EditorServices.Services.Template { internal class TemplateHandlers : IGetProjectTemplatesHandler, INewProjectFromTemplateHandler { diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/TemplateService.cs b/src/PowerShellEditorServices/Services/Template/TemplateService.cs similarity index 73% rename from src/PowerShellEditorServices/Services/PowerShellContext/TemplateService.cs rename to src/PowerShellEditorServices/Services/Template/TemplateService.cs index 6be0b1679..a3c416190 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/TemplateService.cs +++ b/src/PowerShellEditorServices/Services/Template/TemplateService.cs @@ -2,15 +2,16 @@ // Licensed under the MIT License. using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Handlers; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; -using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; using System; +using System.Collections.Generic; using System.Linq; using System.Management.Automation; +using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Services +namespace Microsoft.PowerShell.EditorServices.Services.Template { /// /// Provides a service for listing PowerShell project templates and creating @@ -21,10 +22,10 @@ internal class TemplateService { #region Private Fields - private readonly ILogger logger; + private readonly ILogger _logger; private bool isPlasterLoaded; private bool? isPlasterInstalled; - private readonly PowerShellContextService powerShellContext; + private readonly IInternalPowerShellExecutionService _executionService; #endregion @@ -35,12 +36,10 @@ internal class TemplateService /// /// The PowerShellContext to use for this service. /// An ILoggerFactory implementation used for writing log messages. - public TemplateService(PowerShellContextService powerShellContext, ILoggerFactory factory) + public TemplateService(IInternalPowerShellExecutionService executionService, ILoggerFactory factory) { - Validate.IsNotNull(nameof(powerShellContext), powerShellContext); - - this.logger = factory.CreateLogger(); - this.powerShellContext = powerShellContext; + _logger = factory.CreateLogger(); + _executionService = executionService; } #endregion @@ -71,24 +70,21 @@ public async Task ImportPlasterIfInstalledAsync() .AddCommand("Select-Object") .AddParameter("First", 1); - this.logger.LogTrace("Checking if Plaster is installed..."); + this._logger.LogTrace("Checking if Plaster is installed..."); - var getResult = - await this.powerShellContext.ExecuteCommandAsync( - psCommand, false, false).ConfigureAwait(false); + PSObject moduleObject = (await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false)).First(); - PSObject moduleObject = getResult.First(); this.isPlasterInstalled = moduleObject != null; string installedQualifier = this.isPlasterInstalled.Value ? string.Empty : "not "; - this.logger.LogTrace($"Plaster is {installedQualifier}installed!"); + this._logger.LogTrace($"Plaster is {installedQualifier}installed!"); // Attempt to load plaster if (this.isPlasterInstalled.Value && this.isPlasterLoaded == false) { - this.logger.LogTrace("Loading Plaster..."); + this._logger.LogTrace("Loading Plaster..."); psCommand = new PSCommand(); psCommand @@ -96,16 +92,14 @@ await this.powerShellContext.ExecuteCommandAsync( .AddParameter("ModuleInfo", (PSModuleInfo)moduleObject.ImmediateBaseObject) .AddParameter("PassThru"); - var importResult = - await this.powerShellContext.ExecuteCommandAsync( - psCommand, false, false).ConfigureAwait(false); + IReadOnlyList importResult = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); this.isPlasterLoaded = importResult.Any(); string loadedQualifier = this.isPlasterInstalled.Value ? "was" : "could not be"; - this.logger.LogTrace($"Plaster {loadedQualifier} loaded successfully!"); + this._logger.LogTrace($"Plaster {loadedQualifier} loaded successfully!"); } } @@ -137,11 +131,11 @@ public async Task GetAvailableTemplatesAsync( psCommand.AddParameter("IncludeModules"); } - var templateObjects = - await this.powerShellContext.ExecuteCommandAsync( - psCommand, false, false).ConfigureAwait(false); + IReadOnlyList templateObjects = await _executionService.ExecutePSCommandAsync( + psCommand, + CancellationToken.None).ConfigureAwait(false); - this.logger.LogTrace($"Found {templateObjects.Count()} Plaster templates"); + this._logger.LogTrace($"Found {templateObjects.Count()} Plaster templates"); return templateObjects @@ -161,7 +155,7 @@ public async Task CreateFromTemplateAsync( string templatePath, string destinationPath) { - this.logger.LogTrace( + this._logger.LogTrace( $"Invoking Plaster...\n\n TemplatePath: {templatePath}\n DestinationPath: {destinationPath}"); PSCommand command = new PSCommand(); @@ -169,19 +163,13 @@ public async Task CreateFromTemplateAsync( command.AddParameter("TemplatePath", templatePath); command.AddParameter("DestinationPath", destinationPath); - var errorString = new System.Text.StringBuilder(); - await this.powerShellContext.ExecuteCommandAsync( + await _executionService.ExecutePSCommandAsync( command, - errorString, - new ExecutionOptions - { - WriteOutputToHost = false, - WriteErrorsToHost = true, - InterruptCommandPrompt = true - }).ConfigureAwait(false); + CancellationToken.None, + new PowerShellExecutionOptions { WriteOutputToHost = true, InterruptCurrentForeground = true, ThrowOnError = false }).ConfigureAwait(false); // If any errors were written out, creation was not successful - return errorString.Length == 0; + return true; } #endregion diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs index 4c77fe934..2581e2a6e 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs @@ -9,7 +9,9 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; @@ -23,10 +25,9 @@ namespace Microsoft.PowerShell.EditorServices.Handlers internal class PsesCompletionHandler : ICompletionHandler, ICompletionResolveHandler { const int DefaultWaitTimeoutMilliseconds = 5000; - private readonly SemaphoreSlim _completionLock = AsyncUtils.CreateSimpleLockingSemaphore(); - private readonly SemaphoreSlim _completionResolveLock = AsyncUtils.CreateSimpleLockingSemaphore(); private readonly ILogger _logger; - private readonly PowerShellContextService _powerShellContextService; + private readonly IRunspaceContext _runspaceContext; + private readonly IInternalPowerShellExecutionService _executionService; private readonly WorkspaceService _workspaceService; private CompletionResults _mostRecentCompletions; private int _mostRecentRequestLine; @@ -38,11 +39,13 @@ internal class PsesCompletionHandler : ICompletionHandler, ICompletionResolveHan public PsesCompletionHandler( ILoggerFactory factory, - PowerShellContextService powerShellContextService, + IRunspaceContext runspaceContext, + IInternalPowerShellExecutionService executionService, WorkspaceService workspaceService) { _logger = factory.CreateLogger(); - _powerShellContextService = powerShellContextService; + _runspaceContext = runspaceContext; + _executionService = executionService; _workspaceService = workspaceService; } @@ -60,47 +63,30 @@ public async Task Handle(CompletionParams request, CancellationT ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri); - try - { - await _completionLock.WaitAsync(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) + if (cancellationToken.IsCancellationRequested) { _logger.LogDebug("Completion request canceled for file: {0}", request.TextDocument.Uri); return Array.Empty(); } - try - { - if (cancellationToken.IsCancellationRequested) - { - _logger.LogDebug("Completion request canceled for file: {0}", request.TextDocument.Uri); - return Array.Empty(); - } - - CompletionResults completionResults = - await GetCompletionsInFileAsync( - scriptFile, - cursorLine, - cursorColumn).ConfigureAwait(false); - - if (completionResults == null) - { - return Array.Empty(); - } + CompletionResults completionResults = + await GetCompletionsInFileAsync( + scriptFile, + cursorLine, + cursorColumn).ConfigureAwait(false); - CompletionItem[] completionItems = new CompletionItem[completionResults.Completions.Length]; - for (int i = 0; i < completionItems.Length; i++) - { - completionItems[i] = CreateCompletionItem(completionResults.Completions[i], completionResults.ReplacedRange, i + 1); - } - - return completionItems; + if (completionResults == null) + { + return Array.Empty(); } - finally + + CompletionItem[] completionItems = new CompletionItem[completionResults.Completions.Length]; + for (int i = 0; i < completionItems.Length; i++) { - _completionLock.Release(); + completionItems[i] = CreateCompletionItem(completionResults.Completions[i], completionResults.ReplacedRange, i + 1); } + + return completionItems; } public static bool CanResolve(CompletionItem value) @@ -123,39 +109,23 @@ public async Task Handle(CompletionItem request, CancellationTok return request; } - try - { - await _completionResolveLock.WaitAsync(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - _logger.LogDebug("CompletionItemResolve request canceled for item: {0}", request.Label); - return request; - } + // Get the documentation for the function + CommandInfo commandInfo = + await CommandHelpers.GetCommandInfoAsync( + request.Label, + _runspaceContext.CurrentRunspace, + _executionService).ConfigureAwait(false); - try + if (commandInfo != null) { - // Get the documentation for the function - CommandInfo commandInfo = - await CommandHelpers.GetCommandInfoAsync( - request.Label, - _powerShellContextService).ConfigureAwait(false); - - if (commandInfo != null) + request = request with { - request = request with - { - Documentation = await CommandHelpers.GetCommandSynopsisAsync(commandInfo, _powerShellContextService).ConfigureAwait(false) - }; - } - - // Send back the updated CompletionItem - return request; - } - finally - { - _completionResolveLock.Release(); + Documentation = await CommandHelpers.GetCommandSynopsisAsync(commandInfo, _executionService).ConfigureAwait(false) + }; } + + // Send back the updated CompletionItem + return request; } public void SetCapability(CompletionCapability capability, ClientCapabilities clientCapabilities) @@ -201,7 +171,7 @@ await AstOperations.GetCompletionsAsync( scriptFile.ScriptAst, scriptFile.ScriptTokens, fileOffset, - _powerShellContextService, + _executionService, _logger, cts.Token).ConfigureAwait(false); } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs index 8f80350d1..c62c9f5db 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs @@ -6,11 +6,9 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Utility; -using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Document; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -using OmniSharp.Extensions.LanguageServer.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.Handlers { diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs index 9b1b2c48d..689b762ca 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; @@ -20,18 +21,18 @@ internal class PsesSignatureHelpHandler : SignatureHelpHandlerBase private readonly ILogger _logger; private readonly SymbolsService _symbolsService; private readonly WorkspaceService _workspaceService; - private readonly PowerShellContextService _powerShellContextService; + private readonly IInternalPowerShellExecutionService _executionService; public PsesSignatureHelpHandler( ILoggerFactory factory, SymbolsService symbolsService, WorkspaceService workspaceService, - PowerShellContextService powerShellContextService) + IInternalPowerShellExecutionService executionService) { _logger = factory.CreateLogger(); _symbolsService = symbolsService; _workspaceService = workspaceService; - _powerShellContextService = powerShellContextService; + _executionService = executionService; } protected override SignatureHelpRegistrationOptions CreateRegistrationOptions(SignatureHelpCapability capability, ClientCapabilities clientCapabilities) => new SignatureHelpRegistrationOptions @@ -55,8 +56,7 @@ public override async Task Handle(SignatureHelpParams request, Ca await _symbolsService.FindParameterSetsInFileAsync( scriptFile, request.Position.Line + 1, - request.Position.Character + 1, - _powerShellContextService).ConfigureAwait(false); + request.Position.Character + 1).ConfigureAwait(false); if (parameterSets == null) { diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs index ed69b6f09..d35de6850 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -55,7 +55,7 @@ public override Task Handle(DidChangeTextDocumentParams notification, Canc // Kick off script diagnostics without blocking the response // TODO: Get all recently edited files in the workspace - _analysisService.RunScriptDiagnostics(new ScriptFile[] { changedFile }); + _analysisService.StartScriptDiagnostics(new ScriptFile[] { changedFile }); return Unit.Task; } @@ -81,7 +81,7 @@ public override Task Handle(DidOpenTextDocumentParams notification, Cancel { // Kick off script diagnostics if we got a PowerShell file without blocking the response // TODO: Get all recently edited files in the workspace - _analysisService.RunScriptDiagnostics(new ScriptFile[] { openedFile }); + _analysisService.StartScriptDiagnostics(new ScriptFile[] { openedFile }); } _logger.LogTrace("Finished opening document."); diff --git a/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs index 8cf04e5de..00095a8a4 100644 --- a/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs +++ b/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs @@ -12,11 +12,13 @@ using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.Configuration; using Newtonsoft.Json.Linq; -using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using OmniSharp.Extensions.LanguageServer.Protocol.Window; using OmniSharp.Extensions.LanguageServer.Protocol.Workspace; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.Extension; + namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -25,10 +27,11 @@ internal class PsesConfigurationHandler : DidChangeConfigurationHandlerBase private readonly ILogger _logger; private readonly WorkspaceService _workspaceService; private readonly ConfigurationService _configurationService; - private readonly PowerShellContextService _powerShellContextService; + private readonly ExtensionService _extensionService; + private readonly PsesInternalHost _psesHost; private readonly ILanguageServerFacade _languageServer; private bool _profilesLoaded; - private bool _consoleReplStarted; + private bool _extensionServiceInitialized; private bool _cwdSet; public PsesConfigurationHandler( @@ -36,14 +39,17 @@ public PsesConfigurationHandler( WorkspaceService workspaceService, AnalysisService analysisService, ConfigurationService configurationService, - PowerShellContextService powerShellContextService, - ILanguageServerFacade languageServer) + ILanguageServerFacade languageServer, + ExtensionService extensionService, + PsesInternalHost psesHost) { _logger = factory.CreateLogger(); _workspaceService = workspaceService; _configurationService = configurationService; - _powerShellContextService = powerShellContextService; _languageServer = languageServer; + _extensionService = extensionService; + _psesHost = psesHost; + ConfigurationUpdated += analysisService.OnConfigurationUpdated; } @@ -70,56 +76,68 @@ public override async Task Handle(DidChangeConfigurationParams request, Ca _workspaceService.WorkspacePath, _logger); + // We need to load the profiles if: + // - Profile loading is configured, AND + // - Profiles haven't been loaded before, OR + // - The profile loading configuration just changed + bool loadProfiles = _configurationService.CurrentSettings.EnableProfileLoading + && (!_profilesLoaded || !profileLoadingPreviouslyEnabled); + + if (!_psesHost.IsRunning) + { + _logger.LogTrace("Starting command loop"); + + if (loadProfiles) + { + _logger.LogTrace("Loading profiles..."); + } + + await _psesHost.TryStartAsync(new HostStartOptions { LoadProfiles = loadProfiles }, CancellationToken.None).ConfigureAwait(false); + + if (loadProfiles) + { + _profilesLoaded = true; + _logger.LogTrace("Loaded!"); + } + } + + // TODO: Load profiles when the host is already running + if (!this._cwdSet) { if (!string.IsNullOrEmpty(_configurationService.CurrentSettings.Cwd) && Directory.Exists(_configurationService.CurrentSettings.Cwd)) { this._logger.LogTrace($"Setting CWD (from config) to {_configurationService.CurrentSettings.Cwd}"); - await _powerShellContextService.SetWorkingDirectoryAsync( + await _psesHost.SetInitialWorkingDirectoryAsync( _configurationService.CurrentSettings.Cwd, - isPathAlreadyEscaped: false).ConfigureAwait(false); + CancellationToken.None).ConfigureAwait(false); - } else if (_workspaceService.WorkspacePath != null + } + else if (_workspaceService.WorkspacePath != null && Directory.Exists(_workspaceService.WorkspacePath)) { this._logger.LogTrace($"Setting CWD (from workspace) to {_workspaceService.WorkspacePath}"); - await _powerShellContextService.SetWorkingDirectoryAsync( + await _psesHost.SetInitialWorkingDirectoryAsync( _workspaceService.WorkspacePath, - isPathAlreadyEscaped: false).ConfigureAwait(false); - } else { + CancellationToken.None).ConfigureAwait(false); + } + else + { this._logger.LogTrace("Tried to set CWD but in bad state"); } this._cwdSet = true; } - // We need to load the profiles if: - // - Profile loading is configured, AND - // - Profiles haven't been loaded before, OR - // - The profile loading configuration just changed - if (_configurationService.CurrentSettings.EnableProfileLoading - && (!this._profilesLoaded || !profileLoadingPreviouslyEnabled)) - { - this._logger.LogTrace("Loading profiles..."); - await _powerShellContextService.LoadHostProfilesAsync().ConfigureAwait(false); - this._profilesLoaded = true; - this._logger.LogTrace("Loaded!"); - } - - // Wait until after profiles are loaded (or not, if that's the - // case) before starting the interactive console. - if (!this._consoleReplStarted) + if (!_extensionServiceInitialized) { - // Start the interactive terminal - this._logger.LogTrace("Starting command loop"); - _powerShellContextService.ConsoleReader.StartCommandLoop(); - this._consoleReplStarted = true; + await _extensionService.InitializeAsync().ConfigureAwait(false); } // Run any events subscribed to configuration updates this._logger.LogTrace("Running configuration update event handlers"); - ConfigurationUpdated(this, _configurationService.CurrentSettings); + ConfigurationUpdated?.Invoke(this, _configurationService.CurrentSettings); // Convert the editor file glob patterns into an array for the Workspace // Both the files.exclude and search.exclude hash tables look like (glob-text, is-enabled): diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/RemoteFileManagerService.cs b/src/PowerShellEditorServices/Services/Workspace/RemoteFileManagerService.cs similarity index 70% rename from src/PowerShellEditorServices/Services/PowerShellContext/RemoteFileManagerService.cs rename to src/PowerShellEditorServices/Services/Workspace/RemoteFileManagerService.cs index 2685067a5..6db4fc4d9 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/RemoteFileManagerService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/RemoteFileManagerService.cs @@ -4,7 +4,11 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Extensions; using Microsoft.PowerShell.EditorServices.Logging; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.Extension; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; @@ -14,7 +18,9 @@ using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Text; +using System.Threading; using System.Threading.Tasks; +using SMA = System.Management.Automation; namespace Microsoft.PowerShell.EditorServices.Services { @@ -29,7 +35,8 @@ internal class RemoteFileManagerService private ILogger logger; private string remoteFilesPath; private string processTempPath; - private PowerShellContextService powerShellContext; + private readonly IRunspaceContext _runspaceContext; + private readonly IInternalPowerShellExecutionService _executionService; private IEditorOperations editorOperations; private Dictionary filesPerComputer = @@ -247,14 +254,14 @@ function New-EditorFile { /// public RemoteFileManagerService( ILoggerFactory factory, - PowerShellContextService powerShellContext, + IRunspaceContext runspaceContext, + IInternalPowerShellExecutionService executionService, EditorOperationsService editorOperations) { - Validate.IsNotNull(nameof(powerShellContext), powerShellContext); - this.logger = factory.CreateLogger(); - this.powerShellContext = powerShellContext; - this.powerShellContext.RunspaceChanged += HandleRunspaceChangedAsync; + _runspaceContext = runspaceContext; + _executionService = executionService; + _executionService.RunspaceChanged += HandleRunspaceChanged; this.editorOperations = editorOperations; @@ -268,8 +275,9 @@ public RemoteFileManagerService( // Delete existing temporary file cache path if it already exists this.TryDeleteTemporaryPath(); + // TODO: Do this somewhere other than the constructor and make it async // Register the psedit function in the current runspace - this.RegisterPSEditFunction(this.powerShellContext.CurrentRunspace); + this.RegisterPSEditFunctionAsync().HandleErrorsAsync(logger); } #endregion @@ -282,7 +290,7 @@ public RemoteFileManagerService( /// /// The remote file path to be opened. /// - /// + /// /// The runspace from which where the remote file will be fetched. /// /// @@ -290,7 +298,7 @@ public RemoteFileManagerService( /// public async Task FetchRemoteFileAsync( string remoteFilePath, - RunspaceDetails runspaceDetails) + IRunspaceInfo runspaceInfo) { string localFilePath = null; @@ -298,8 +306,8 @@ public async Task FetchRemoteFileAsync( { try { - RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); - localFilePath = this.GetMappedPath(remoteFilePath, runspaceDetails); + RemotePathMappings pathMappings = this.GetPathMappings(runspaceInfo); + localFilePath = this.GetMappedPath(remoteFilePath, runspaceInfo); if (!pathMappings.IsRemotePathOpened(remoteFilePath)) { @@ -307,14 +315,22 @@ public async Task FetchRemoteFileAsync( 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"); + PSCommand command = new PSCommand() + .AddCommand("Microsoft.PowerShell.Management\\Get-Content") + .AddParameter("Path", remoteFilePath) + .AddParameter("Raw"); + + if (string.Equals(runspaceInfo.PowerShellVersionDetails.Edition, "Core")) + { + command.AddParameter("AsByteStream"); + } + else + { + command.AddParameter("Encoding", "Byte"); + } byte[] fileContent = - (await this.powerShellContext.ExecuteCommandAsync(command, false, false).ConfigureAwait(false)) + (await this._executionService.ExecutePSCommandAsync(command, CancellationToken.None).ConfigureAwait(false)) .FirstOrDefault(); if (fileContent != null) @@ -354,7 +370,7 @@ public async Task SaveRemoteFileAsync(string localFilePath) string remoteFilePath = this.GetMappedPath( localFilePath, - this.powerShellContext.CurrentRunspace); + _runspaceContext.CurrentRunspace); this.logger.LogTrace( $"Saving remote file {remoteFilePath} (local path: {localFilePath})"); @@ -379,17 +395,15 @@ public async Task SaveRemoteFileAsync(string localFilePath) .AddParameter("RemoteFilePath", remoteFilePath) .AddParameter("Content", localFileContents); - StringBuilder errorMessages = new StringBuilder(); - - await powerShellContext.ExecuteCommandAsync( - saveCommand, - errorMessages, - false, - false).ConfigureAwait(false); - - if (errorMessages.Length > 0) + try { - this.logger.LogError($"Remote file save failed due to an error:\r\n\r\n{errorMessages}"); + await _executionService.ExecutePSCommandAsync( + saveCommand, + CancellationToken.None).ConfigureAwait(false); + } + catch (Exception e) + { + this.logger.LogError(e, "Remote file save failed"); } } @@ -403,11 +417,11 @@ await powerShellContext.ExecuteCommandAsync( /// /// 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) + public string CreateTemporaryFile(string fileName, string fileContents, IRunspaceInfo runspaceInfo) { string temporaryFilePath = Path.Combine(this.processTempPath, fileName); @@ -415,7 +429,7 @@ public string CreateTemporaryFile(string fileName, string fileContents, Runspace { File.WriteAllText(temporaryFilePath, fileContents); - RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); + RemotePathMappings pathMappings = this.GetPathMappings(runspaceInfo); pathMappings.AddOpenedLocalPath(temporaryFilePath); } catch (IOException e) @@ -442,7 +456,7 @@ public string CreateTemporaryFile(string fileName, string fileContents, Runspace /// The mapped file path. public string GetMappedPath( string filePath, - RunspaceDetails runspaceDetails) + IRunspaceInfo runspaceDetails) { RemotePathMappings remotePathMappings = this.GetPathMappings(runspaceDetails); return remotePathMappings.GetMappedPath(filePath); @@ -470,9 +484,9 @@ public bool IsUnderRemoteTempPath(string filePath) private string StoreRemoteFile( string remoteFilePath, byte[] fileContent, - RunspaceDetails runspaceDetails) + IRunspaceInfo runspaceInfo) { - RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); + RemotePathMappings pathMappings = this.GetPathMappings(runspaceInfo); string localFilePath = pathMappings.GetMappedPath(remoteFilePath); RemoteFileManagerService.StoreRemoteFile( @@ -492,187 +506,216 @@ private static void StoreRemoteFile( pathMappings.AddOpenedLocalPath(localFilePath); } - private RemotePathMappings GetPathMappings(RunspaceDetails runspaceDetails) + private RemotePathMappings GetPathMappings(IRunspaceInfo runspaceInfo) { - RemotePathMappings remotePathMappings = null; - string computerName = runspaceDetails.SessionDetails.ComputerName; + string computerName = runspaceInfo.SessionDetails.ComputerName; - if (!this.filesPerComputer.TryGetValue(computerName, out remotePathMappings)) + if (!this.filesPerComputer.TryGetValue(computerName, out RemotePathMappings remotePathMappings)) { - remotePathMappings = new RemotePathMappings(runspaceDetails, this); + remotePathMappings = new RemotePathMappings(runspaceInfo, this); this.filesPerComputer.Add(computerName, remotePathMappings); } return remotePathMappings; } - private async void HandleRunspaceChangedAsync(object sender, RunspaceChangedEventArgs e) + private void HandleRunspaceChanged(object sender, RunspaceChangedEventArgs e) { if (e.ChangeAction == RunspaceChangeAction.Enter) { - this.RegisterPSEditFunction(e.NewRunspace); + this.RegisterPSEditFunction(e.NewRunspace.Runspace); + return; } - else + + // Close any remote files that were opened + if (ShouldTearDownRemoteFiles(e)) { - // 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)) { - RemotePathMappings remotePathMappings; - if (this.filesPerComputer.TryGetValue(e.PreviousRunspace.SessionDetails.ComputerName, out remotePathMappings)) + var fileCloseTasks = new List(); + foreach (string remotePath in remotePathMappings.OpenedPaths) { - foreach (string remotePath in remotePathMappings.OpenedPaths) - { - await (this.editorOperations?.CloseFileAsync(remotePath)).ConfigureAwait(false); - } + fileCloseTasks.Add(this.editorOperations?.CloseFileAsync(remotePath)); } - } - if (e.PreviousRunspace != null) - { - this.RemovePSEditFunction(e.PreviousRunspace); + try + { + Task.WaitAll(fileCloseTasks.ToArray()); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Unable to close all files in closed runspace"); + } } } + + if (e.PreviousRunspace != null) + { + this.RemovePSEditFunction(e.PreviousRunspace); + } + } + + private static bool ShouldTearDownRemoteFiles(RunspaceChangedEventArgs runspaceChangedEvent) + { + if (!runspaceChangedEvent.PreviousRunspace.IsOnRemoteMachine) + { + return false; + } + + if (runspaceChangedEvent.ChangeAction == RunspaceChangeAction.Shutdown) + { + return true; + } + + // Check to see if the runspace we're changing to is on a different machine to the one we left + return !string.Equals( + runspaceChangedEvent.NewRunspace.SessionDetails.ComputerName, + runspaceChangedEvent.PreviousRunspace.SessionDetails.ComputerName, + StringComparison.CurrentCultureIgnoreCase); } private async void HandlePSEventReceivedAsync(object sender, PSEventArgs args) { - if (string.Equals(RemoteSessionOpenFile, args.SourceIdentifier, StringComparison.CurrentCultureIgnoreCase)) + if (!string.Equals(RemoteSessionOpenFile, args.SourceIdentifier, StringComparison.CurrentCultureIgnoreCase)) { - try + return; + } + + try + { + if (args.SourceArgs.Length >= 1) { - 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 (!_runspaceContext.CurrentRunspace.IsOnRemoteMachine) { - string localFilePath = string.Empty; - string remoteFilePath = args.SourceArgs[0] as string; + localFilePath = remoteFilePath; + } + else + { + byte[] fileContent = null; - // Is this a local process runspace? Treat as a local file - if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Local) + if (args.SourceArgs.Length >= 2) { - 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 ?? Array.Empty(); - - if (remoteFilePath != null) + // 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) { - localFilePath = - this.StoreRemoteFile( - remoteFilePath, - fileContent, - this.powerShellContext.CurrentRunspace); + fileContent = sourceObj.BaseObject as byte[]; } else { - await (this.editorOperations?.NewFileAsync()).ConfigureAwait(false); - EditorContext context = await (editorOperations?.GetEditorContextAsync()).ConfigureAwait(false); - context?.CurrentFile.InsertText(Encoding.UTF8.GetString(fileContent, 0, fileContent.Length)); + fileContent = args.SourceArgs[1] as byte[]; } } - bool preview = true; - if (args.SourceArgs.Length >= 3) + // If fileContent is still null after trying to + // unpack the contents, just return an empty byte + // array. + fileContent = fileContent ?? Array.Empty(); + + if (remoteFilePath != null) + { + localFilePath = + this.StoreRemoteFile( + remoteFilePath, + fileContent, + _runspaceContext.CurrentRunspace); + } + else { - bool? previewCheck = args.SourceArgs[2] as bool?; - preview = previewCheck ?? true; + await (this.editorOperations?.NewFileAsync()).ConfigureAwait(false); + EditorContext context = await (editorOperations?.GetEditorContextAsync()).ConfigureAwait(false); + context?.CurrentFile.InsertText(Encoding.UTF8.GetString(fileContent, 0, fileContent.Length)); } + } - // Open the file in the editor - this.editorOperations?.OpenFileAsync(localFilePath, preview); + bool preview = true; + if (args.SourceArgs.Length >= 3) + { + bool? previewCheck = args.SourceArgs[2] as bool?; + preview = previewCheck ?? true; } - } - catch (NullReferenceException e) - { - this.logger.LogException("Could not store null remote file content", e); + + // Open the file in the editor + await (this.editorOperations?.OpenFileAsync(localFilePath, preview)).ConfigureAwait(false); } } + catch (NullReferenceException e) + { + this.logger.LogException("Could not store null remote file content", e); + } + catch (Exception e) + { + this.logger.LogException("Unable to handle remote file update", e); + } } - private void RegisterPSEditFunction(RunspaceDetails runspaceDetails) + private Task RegisterPSEditFunctionAsync() + => _executionService.ExecuteDelegateAsync( + "Register psedit function", + ExecutionOptions.Default, + (pwsh, cancellationToken) => RegisterPSEditFunction(pwsh.Runspace), + CancellationToken.None); + + private void RegisterPSEditFunction(Runspace runspace) { - if (runspaceDetails.Location == RunspaceLocation.Remote && - runspaceDetails.Context == RunspaceContext.Original) + if (!runspace.RunspaceIsRemote) { - try - { - runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived += HandlePSEventReceivedAsync; + return; + } - PSCommand createCommand = new PSCommand(); - createCommand - .AddScript(CreatePSEditFunctionScript) - .AddParameter("PSEditModule", PSEditModule); + runspace.Events.ReceivedEvents.PSEventReceived += HandlePSEventReceivedAsync; - 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); - } + PSCommand createCommand = new PSCommand() + .AddScript(CreatePSEditFunctionScript) + .AddParameter("PSEditModule", PSEditModule); + + var pwsh = SMA.PowerShell.Create(); + pwsh.Runspace = runspace; + try + { + pwsh.InvokeCommand(createCommand, new PSInvocationSettings { AddToHistory = false, ErrorActionPreference = ActionPreference.Stop }); + } + catch (Exception e) + { + this.logger.LogException("Could not create psedit function.", e); + } + finally + { + pwsh.Dispose(); } } - private void RemovePSEditFunction(RunspaceDetails runspaceDetails) + private void RemovePSEditFunction(IRunspaceInfo runspaceInfo) { - if (runspaceDetails.Location == RunspaceLocation.Remote && - runspaceDetails.Context == RunspaceContext.Original) + if (runspaceInfo.RunspaceOrigin != RunspaceOrigin.PSSession) { - try + return; + } + try + { + if (runspaceInfo.Runspace.Events != null) { - if (runspaceDetails.Runspace.Events != null) - { - runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived -= HandlePSEventReceivedAsync; - } + runspaceInfo.Runspace.Events.ReceivedEvents.PSEventReceived -= HandlePSEventReceivedAsync; + } - if (runspaceDetails.Runspace.RunspaceStateInfo.State == RunspaceState.Opened) + if (runspaceInfo.Runspace.RunspaceStateInfo.State == RunspaceState.Opened) + { + using (var powerShell = SMA.PowerShell.Create()) { - using (var powerShell = System.Management.Automation.PowerShell.Create()) - { - powerShell.Runspace = runspaceDetails.Runspace; - powerShell.Commands.AddScript(RemovePSEditFunctionScript); - powerShell.Invoke(); - } + powerShell.Runspace = runspaceInfo.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); - } + } + catch (Exception e) when (e is RemoteException || e is PSInvalidOperationException) + { + this.logger.LogException("Could not remove psedit function.", e); } } @@ -700,7 +743,7 @@ private void TryDeleteTemporaryPath() private class RemotePathMappings { - private RunspaceDetails runspaceDetails; + private IRunspaceInfo runspaceInfo; private RemoteFileManagerService remoteFileManager; private HashSet openedPaths = new HashSet(); private Dictionary pathMappings = new Dictionary(); @@ -711,10 +754,10 @@ public IEnumerable OpenedPaths } public RemotePathMappings( - RunspaceDetails runspaceDetails, + IRunspaceInfo runspaceInfo, RemoteFileManagerService remoteFileManager) { - this.runspaceDetails = runspaceDetails; + this.runspaceInfo = runspaceInfo; this.remoteFileManager = remoteFileManager; } @@ -747,7 +790,7 @@ public string GetMappedPath(string filePath) mappedPath = this.MapRemotePathToLocal( filePath, - runspaceDetails.SessionDetails.ComputerName); + runspaceInfo.SessionDetails.ComputerName); this.AddPathMapping(filePath, mappedPath); } diff --git a/src/PowerShellEditorServices/Utility/AsyncLock.cs b/src/PowerShellEditorServices/Utility/AsyncLock.cs deleted file mode 100644 index 7e39eb6bb..000000000 --- a/src/PowerShellEditorServices/Utility/AsyncLock.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -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. - /// - internal 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/Utility/AsyncQueue.cs b/src/PowerShellEditorServices/Utility/AsyncQueue.cs deleted file mode 100644 index 3c710f753..000000000 --- a/src/PowerShellEditorServices/Utility/AsyncQueue.cs +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -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. - internal 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().ConfigureAwait(false)) - { - 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).ConfigureAwait(false)) - { - 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.ConfigureAwait(false); - } - - /// - /// Dequeues an item from the queue or waits asynchronously - /// until an item is available. - /// - /// - public T Dequeue() - { - return Dequeue(CancellationToken.None); - } - - /// - /// Dequeues an item from the queue or waits asynchronously - /// until an item is available. The wait can be cancelled - /// using the given CancellationToken. - /// - /// - /// A CancellationToken with which a dequeue wait can be cancelled. - /// - /// - public T Dequeue(CancellationToken cancellationToken) - { - TaskCompletionSource requestTask; - using (queueLock.Lock(cancellationToken)) - { - if (this.itemQueue.Count > 0) - { - T item = this.itemQueue.Dequeue(); - this.IsEmpty = this.itemQueue.Count == 0; - - return item; - } - - requestTask = new TaskCompletionSource(); - this.requestQueue.Enqueue(requestTask); - - if (cancellationToken.CanBeCanceled) - { - cancellationToken.Register(() => requestTask.TrySetCanceled()); - } - } - - return requestTask.Task.GetAwaiter().GetResult(); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Utility/AsyncUtils.cs b/src/PowerShellEditorServices/Utility/AsyncUtils.cs index dd16a0afd..2fdff670d 100644 --- a/src/PowerShellEditorServices/Utility/AsyncUtils.cs +++ b/src/PowerShellEditorServices/Utility/AsyncUtils.cs @@ -1,7 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace Microsoft.PowerShell.EditorServices.Utility { @@ -19,5 +23,35 @@ internal static SemaphoreSlim CreateSimpleLockingSemaphore() { return new SemaphoreSlim(initialCount: 1, maxCount: 1); } + + internal static Task HandleErrorsAsync( + this Task task, + ILogger logger, + [CallerMemberName] string callerName = null, + [CallerFilePath] string callerSourceFile = null, + [CallerLineNumber] int callerLineNumber = -1) + { + return task.IsCompleted && !(task.IsFaulted || task.IsCanceled) + ? task + : LogTaskErrors(task, logger, callerName, callerSourceFile, callerLineNumber); + } + + private static async Task LogTaskErrors(Task task, ILogger logger, string callerName, string callerSourceFile, int callerLineNumber) + { + try + { + await task.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + logger.LogDebug($"Task canceled in '{callerName}' in file '{callerSourceFile}' line {callerLineNumber}"); + throw; + } + catch (Exception e) + { + logger.LogError(e, $"Exception thrown running task in '{callerName}' in file '{callerSourceFile}' line {callerLineNumber}"); + throw; + } + } } } diff --git a/src/PowerShellEditorServices/Utility/IdempotentLatch.cs b/src/PowerShellEditorServices/Utility/IdempotentLatch.cs new file mode 100644 index 000000000..31c1a95d0 --- /dev/null +++ b/src/PowerShellEditorServices/Utility/IdempotentLatch.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + internal class IdempotentLatch + { + private int _signaled; + + public IdempotentLatch() + { + _signaled = 0; + } + + public bool IsSignaled => _signaled != 0; + + public bool TryEnter() => Interlocked.Exchange(ref _signaled, 1) == 0; + } +} diff --git a/src/PowerShellEditorServices/Utility/IsExternalInit.cs b/src/PowerShellEditorServices/Utility/IsExternalInit.cs new file mode 100644 index 000000000..7d336fdc2 --- /dev/null +++ b/src/PowerShellEditorServices/Utility/IsExternalInit.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// + /// This type must be defined to use init property accessors, + /// but is not in .NET Standard 2.0. + /// So instead we define the type in our own code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal class IsExternalInit{} +} diff --git a/src/PowerShellEditorServices/Utility/LspDebugUtils.cs b/src/PowerShellEditorServices/Utility/LspDebugUtils.cs index fadf8719e..254bc41a6 100644 --- a/src/PowerShellEditorServices/Utility/LspDebugUtils.cs +++ b/src/PowerShellEditorServices/Utility/LspDebugUtils.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using OmniSharp.Extensions.DebugAdapter.Protocol.Models; diff --git a/src/PowerShellEditorServices/Utility/PSCommandExtensions.cs b/src/PowerShellEditorServices/Utility/PSCommandExtensions.cs index d283d215a..55c8eed20 100644 --- a/src/PowerShellEditorServices/Utility/PSCommandExtensions.cs +++ b/src/PowerShellEditorServices/Utility/PSCommandExtensions.cs @@ -6,6 +6,7 @@ using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Reflection; +using System.Text; namespace Microsoft.PowerShell.EditorServices.Utility { @@ -32,10 +33,74 @@ static PSCommandExtensions() // PowerShell's missing an API for us to AddCommand using a CommandInfo. // An issue was filed here: https://github.com/PowerShell/PowerShell/issues/12295 // This works around this by creating a `Command` and passing it into PSCommand.AddCommand(Command command) - internal static PSCommand AddCommand(this PSCommand command, CommandInfo commandInfo) + public static PSCommand AddCommand(this PSCommand command, CommandInfo commandInfo) { var rsCommand = s_commandCtor(commandInfo); return command.AddCommand(rsCommand); } + + public static PSCommand AddOutputCommand(this PSCommand psCommand) + { + return psCommand.MergePipelineResults() + .AddCommand("Out-Default", useLocalScope: true); + } + + public static PSCommand AddDebugOutputCommand(this PSCommand psCommand) + { + return psCommand.MergePipelineResults() + .AddCommand("Out-String", useLocalScope: true) + .AddParameter("Stream"); + } + + public static PSCommand MergePipelineResults(this PSCommand psCommand) + { + // We need to do merge errors and output before rendering with an Out- cmdlet + Command lastCommand = psCommand.Commands[psCommand.Commands.Count - 1]; + lastCommand.MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output); + lastCommand.MergeMyResults(PipelineResultTypes.Information, PipelineResultTypes.Output); + return psCommand; + } + + /// + /// Get a representation of the PSCommand, for logging purposes. + /// + public static string GetInvocationText(this PSCommand command) + { + Command currentCommand = command.Commands[0]; + var sb = new StringBuilder().AddCommandText(command.Commands[0]); + + for (int i = 1; i < command.Commands.Count; i++) + { + sb.Append(currentCommand.IsEndOfStatement ? "; " : " | "); + currentCommand = command.Commands[i]; + sb.AddCommandText(currentCommand); + } + + return sb.ToString(); + } + + private static StringBuilder AddCommandText(this StringBuilder sb, Command command) + { + sb.Append(command.CommandText); + if (command.Parameters != null) + { + foreach (CommandParameter parameter in command.Parameters) + { + if (parameter.Name != null) + { + sb.Append(" -").Append(parameter.Name); + } + + if (parameter.Value != null) + { + // This isn't going to get PowerShell's string form of the value, + // but it's good enough, and not as complex or expensive + sb.Append(' ').Append(parameter.Value); + } + } + } + + return sb; + } } } diff --git a/src/PowerShellEditorServices/Utility/PathUtils.cs b/src/PowerShellEditorServices/Utility/PathUtils.cs index e7ce11e6d..cdfed6df1 100644 --- a/src/PowerShellEditorServices/Utility/PathUtils.cs +++ b/src/PowerShellEditorServices/Utility/PathUtils.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using System.IO; +using System.Management.Automation; using System.Runtime.InteropServices; +using System.Text; namespace Microsoft.PowerShell.EditorServices.Utility { @@ -35,5 +37,49 @@ public static string NormalizePathSeparators(string path) { return string.IsNullOrWhiteSpace(path) ? path : path.Replace(AlternatePathSeparator, DefaultPathSeparator); } + + public static string WildcardEscape(string path) + { + return WildcardPattern.Escape(path); + } + + /// + /// 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(); + } } } diff --git a/src/PowerShellEditorServices/Utility/StringEscaping.cs b/src/PowerShellEditorServices/Utility/StringEscaping.cs new file mode 100644 index 000000000..5736a9aad --- /dev/null +++ b/src/PowerShellEditorServices/Utility/StringEscaping.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + internal static class StringEscaping + { + public static StringBuilder SingleQuoteAndEscape(string s) + { + return new StringBuilder(s.Length) + .Append("'") + .Append(s.Replace("'", "''")) + .Append("'"); + } + + public static bool PowerShellArgumentNeedsEscaping(string argument) + { + foreach (char c in argument) + { + switch (c) + { + case '\'': + case '"': + case '|': + case '&': + case ';': + case ':': + case char w when char.IsWhiteSpace(w): + return true; + } + } + + return false; + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs index bf8b193cc..95efa48a9 100644 --- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -44,6 +45,13 @@ public async Task InitializeAsync() await _psesProcess.Start().ConfigureAwait(false); var initialized = new TaskCompletionSource(); + + _psesProcess.ProcessExited += (sender, args) => + { + initialized.TrySetException(new ProcessExitedException("Initialization failed due to process failure", args.ExitCode, args.ErrorMessage)); + Started.TrySetException(new ProcessExitedException("Startup failed due to process failure", args.ExitCode, args.ErrorMessage)); + }; + PsesDebugAdapterClient = DebugAdapterClient.Create(options => { options @@ -61,6 +69,11 @@ public async Task InitializeAsync() initialized.SetResult(true); return Task.CompletedTask; }); + + options.OnUnhandledException = (exception) => { + initialized.SetException(exception); + Started.SetException(exception); + }; }); // PSES follows the following flow: @@ -250,7 +263,7 @@ public async Task CanStepPastSystemWindowsForms() string filePath = NewTestFile(string.Join(Environment.NewLine, new [] { "Add-Type -AssemblyName System.Windows.Forms", - "$form = New-Object System.Windows.Forms.Form", + "$global:form = New-Object System.Windows.Forms.Form", "Write-Host $form" })); diff --git a/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs index 6c2d56b0e..b01fff593 100644 --- a/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs +++ b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -45,8 +46,7 @@ public async Task InitializeAsync() var factory = new LoggerFactory(); _psesProcess = new PsesStdioProcess(factory, IsDebugAdapterTests); await _psesProcess.Start().ConfigureAwait(false); - Console.WriteLine("PowerShell Editor Services Server started with PID {0}", ProcessId); - // TIP: Add Breakpoint here and attach debugger using the PID from the above message + Diagnostics = new List(); TelemetryEvents = new List(); DirectoryInfo testdir = diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 32038817d..15de43d4c 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -24,6 +24,8 @@ using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Services.Configuration; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.Template; namespace PowerShellEditorServices.Test.E2E { @@ -39,6 +41,7 @@ public class LanguageServerProtocolMessageTests : IClassFixture private readonly List Diagnostics; private readonly List TelemetryEvents; private readonly string PwshExe; + private readonly LSPTestsFixture _fixture; public LanguageServerProtocolMessageTests(ITestOutputHelper output, LSPTestsFixture data) { @@ -48,6 +51,7 @@ public LanguageServerProtocolMessageTests(ITestOutputHelper output, LSPTestsFixt Diagnostics.Clear(); TelemetryEvents = data.TelemetryEvents; TelemetryEvents.Clear(); + _fixture = data; PwshExe = PsesStdioProcess.PwshExe; } @@ -1197,6 +1201,8 @@ await PsesLanguageClient [Fact] public async Task CanSendEvaluateRequestAsync() { + using var cancellationSource = new CancellationTokenSource(millisecondsDelay: 5000); + EvaluateResponseBody evaluateResponseBody = await PsesLanguageClient .SendRequest( @@ -1205,7 +1211,7 @@ await PsesLanguageClient { Expression = "Get-ChildItem" }) - .Returning(CancellationToken.None).ConfigureAwait(false); + .Returning(cancellationSource.Token).ConfigureAwait(false); // These always gets returned so this test really just makes sure we get _any_ response. Assert.Equal("", evaluateResponseBody.Result); diff --git a/test/PowerShellEditorServices.Test.E2E/Processes/LoggingStream.cs b/test/PowerShellEditorServices.Test.E2E/Processes/LoggingStream.cs new file mode 100644 index 000000000..dc5b2e2a5 --- /dev/null +++ b/test/PowerShellEditorServices.Test.E2E/Processes/LoggingStream.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace PowerShellEditorServices.Test.E2E +{ + internal class LoggingStream : Stream + { + private static readonly string s_banner = new('=', 20); + + private readonly Stream _underlyingStream; + + public LoggingStream(Stream underlyingStream) + { + _underlyingStream = underlyingStream; + } + + public override bool CanRead => _underlyingStream.CanRead; + + public override bool CanSeek => _underlyingStream.CanSeek; + + public override bool CanWrite => _underlyingStream.CanWrite; + + public override long Length => _underlyingStream.Length; + + public override long Position { get => _underlyingStream.Position; set => _underlyingStream.Position = value; } + + public override void Flush() => _underlyingStream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) + { + int actualCount = _underlyingStream.Read(buffer, offset, count); + LogData("READ", buffer, offset, actualCount); + return actualCount; + } + + public override long Seek(long offset, SeekOrigin origin) => _underlyingStream.Seek(offset, origin); + + public override void SetLength(long value) => _underlyingStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) + { + LogData("WRITE", buffer, offset, count); + _underlyingStream.Write(buffer, offset, count); + } + + private static void LogData(string header, byte[] buffer, int offset, int count) + { + Debug.WriteLine($"{header} |{s_banner.Substring(0, Math.Max(s_banner.Length - header.Length - 2, 0))}"); + string data = Encoding.UTF8.GetString(buffer, offset, count); + Debug.WriteLine(data); + Debug.WriteLine(s_banner); + Debug.WriteLine("\n"); + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/Processes/ServerProcess.cs b/test/PowerShellEditorServices.Test.E2E/Processes/ServerProcess.cs index 68e323338..ca7360cb8 100644 --- a/test/PowerShellEditorServices.Test.E2E/Processes/ServerProcess.cs +++ b/test/PowerShellEditorServices.Test.E2E/Processes/ServerProcess.cs @@ -15,6 +15,11 @@ namespace PowerShellEditorServices.Test.E2E public abstract class ServerProcess : IDisposable { private readonly ISubject _exitedSubject; + + private readonly Lazy _inStreamLazy; + + private readonly Lazy _outStreamLazy; + /// /// Create a new . /// @@ -37,6 +42,9 @@ protected ServerProcess(ILoggerFactory loggerFactory) ServerExitCompletion.SetResult(null); // Start out as if the server has already exited. Exited = _exitedSubject = new AsyncSubject(); + + _inStreamLazy = new Lazy(() => new LoggingStream(GetInputStream())); + _outStreamLazy = new Lazy(() => new LoggingStream(GetOutputStream())); } /// @@ -105,13 +113,17 @@ protected virtual void Dispose(bool disposing) /// public Task HasExited => ServerExitCompletion.Task; + protected abstract Stream GetInputStream(); + + protected abstract Stream GetOutputStream(); + /// /// The server's input stream. /// /// /// The connection will write to the server's input stream, and read from its output stream. /// - public abstract Stream InputStream { get; } + public Stream InputStream => _inStreamLazy.Value; /// /// The server's output stream. @@ -119,7 +131,7 @@ protected virtual void Dispose(bool disposing) /// /// The connection will read from the server's output stream, and write to its input stream. /// - public abstract Stream OutputStream { get; } + public Stream OutputStream => _outStreamLazy.Value; /// /// Start or connect to the server. diff --git a/test/PowerShellEditorServices.Test.E2E/Processes/StdioServerProcess.cs b/test/PowerShellEditorServices.Test.E2E/Processes/StdioServerProcess.cs index 06a4a4dbc..8ced05c6d 100644 --- a/test/PowerShellEditorServices.Test.E2E/Processes/StdioServerProcess.cs +++ b/test/PowerShellEditorServices.Test.E2E/Processes/StdioServerProcess.cs @@ -45,6 +45,8 @@ public StdioServerProcess(ILoggerFactory loggerFactory, ProcessStartInfo serverS _serverStartInfo = serverStartInfo; } + public int ProcessId => _serverProcess.Id; + /// /// The process ID of the server process, useful for attaching a debugger. /// @@ -86,12 +88,12 @@ protected override void Dispose(bool disposing) /// /// The server's input stream. /// - public override Stream InputStream => _serverProcess?.StandardInput?.BaseStream; + protected override Stream GetInputStream() => _serverProcess?.StandardInput?.BaseStream; /// /// The server's output stream. /// - public override Stream OutputStream => _serverProcess?.StandardOutput?.BaseStream; + protected override Stream GetOutputStream() => _serverProcess?.StandardOutput?.BaseStream; /// /// Start or connect to the server. @@ -104,6 +106,7 @@ public override Task Start() _serverStartInfo.UseShellExecute = false; _serverStartInfo.RedirectStandardInput = true; _serverStartInfo.RedirectStandardOutput = true; + _serverStartInfo.RedirectStandardError = true; Process serverProcess = _serverProcess = new Process { @@ -125,17 +128,19 @@ public override Task Start() /// /// Stop or disconnect from the server. /// - public override async Task Stop() + public override Task Stop() { Process serverProcess = Interlocked.Exchange(ref _serverProcess, null); + ServerExitCompletion.TrySetResult(null); if (serverProcess != null && !serverProcess.HasExited) { serverProcess.Kill(); } - - await ServerExitCompletion.Task.ConfigureAwait(false); + return ServerExitCompletion.Task; } + public event EventHandler ProcessExited; + /// /// Called when the server process has exited. /// @@ -149,9 +154,49 @@ void ServerProcess_Exit(object sender, EventArgs args) { Log.LogDebug("Server process has exited."); + var serverProcess = (Process)sender; + + int exitCode = serverProcess.ExitCode; + string errorMsg = serverProcess.StandardError.ReadToEnd(); + OnExited(); - ServerExitCompletion.TrySetResult(null); + ProcessExited?.Invoke(this, new ProcessExitedArgs(exitCode, errorMsg)); + if (exitCode != 0) + { + ServerExitCompletion.TrySetException(new ProcessExitedException("Stdio server process exited unexpectedly", exitCode, errorMsg)); + } + else + { + ServerExitCompletion.TrySetResult(null); + } ServerStartCompletion = new TaskCompletionSource(); } } + + public class ProcessExitedException : Exception + { + public ProcessExitedException(string message, int exitCode, string errorMessage) + : base(message) + { + ExitCode = exitCode; + ErrorMessage = errorMessage; + } + + public int ExitCode { get; init; } + + public string ErrorMessage { get; init; } + } + + public class ProcessExitedArgs : EventArgs + { + public ProcessExitedArgs(int exitCode, string errorMessage) + { + ExitCode = exitCode; + ErrorMessage = errorMessage; + } + + public int ExitCode { get; init; } + + public string ErrorMessage { get; init; } + } } diff --git a/test/PowerShellEditorServices.Test.E2E/xunit.runner.json b/test/PowerShellEditorServices.Test.E2E/xunit.runner.json index 3f3645a0a..2719fd14a 100644 --- a/test/PowerShellEditorServices.Test.E2E/xunit.runner.json +++ b/test/PowerShellEditorServices.Test.E2E/xunit.runner.json @@ -2,5 +2,7 @@ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", "appDomain": "denied", "parallelizeTestCollections": false, - "methodDisplay": "method" + "methodDisplay": "method", + "diagnosticMessages": true, + "longRunningTestSeconds": 60 } diff --git a/test/PowerShellEditorServices.Test/Console/ChoicePromptHandlerTests.cs b/test/PowerShellEditorServices.Test/Console/ChoicePromptHandlerTests.cs index a07303472..819be34f0 100644 --- a/test/PowerShellEditorServices.Test/Console/ChoicePromptHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Console/ChoicePromptHandlerTests.cs @@ -4,11 +4,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Xunit; namespace Microsoft.PowerShell.EditorServices.Test.Console { + /* public class ChoicePromptHandlerTests { private readonly ChoiceDetails[] Choices = @@ -121,5 +121,6 @@ protected override void ShowPrompt(PromptStyle promptStyle) this.TimesPrompted++; } } + */ } diff --git a/test/PowerShellEditorServices.Test/Console/InputPromptHandlerTests.cs b/test/PowerShellEditorServices.Test/Console/InputPromptHandlerTests.cs index abb0acabc..2aadb3658 100644 --- a/test/PowerShellEditorServices.Test/Console/InputPromptHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Console/InputPromptHandlerTests.cs @@ -8,7 +8,6 @@ using System; using System.Threading; using System.Security; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.PowerShell.EditorServices.Test.Console diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 42386192c..a01ddc20d 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -3,34 +3,36 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Management.Automation; using System.Threading; using System.Threading.Tasks; +using MediatR; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Test.Shared; -using Microsoft.PowerShell.EditorServices.Utility; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Progress; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; using Xunit; namespace Microsoft.PowerShell.EditorServices.Test.Debugging { + /* public class DebugServiceTests : IDisposable { private WorkspaceService workspace; private DebugService debugService; private ScriptFile debugScriptFile; private ScriptFile variableScriptFile; - private PowerShellContextService powerShellContext; - - private AsyncQueue debuggerStoppedQueue = - new AsyncQueue(); - private AsyncQueue sessionStateQueue = - new AsyncQueue(); private ScriptFile GetDebugScript(string fileName) { @@ -44,6 +46,8 @@ private ScriptFile GetDebugScript(string fileName) public DebugServiceTests() { + var loggerFactory = new NullLoggerFactory(); + var logger = NullLogger.Instance; this.powerShellContext = PowerShellContextFactory.Create(logger); @@ -1067,4 +1071,5 @@ await this.powerShellContext.ExecuteCommandAsync( .AddParameter("Script", scriptFile.FilePath)).ConfigureAwait(false); } } + */ } diff --git a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs index c7b5ac37c..68a6db95a 100644 --- a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.PowerShell.EditorServices.Handlers; using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Test.Shared; @@ -31,7 +32,7 @@ public class LanguageServiceTests : IDisposable private readonly WorkspaceService workspace; private readonly SymbolsService symbolsService; private readonly PsesCompletionHandler completionHandler; - private readonly PowerShellContextService powerShellContext; + private readonly PsesInternalHost _psesHost; private static readonly string s_baseSharedScriptPath = Path.Combine( Path.GetDirectoryName(VersionUtils.IsWindows @@ -44,16 +45,16 @@ public class LanguageServiceTests : IDisposable public LanguageServiceTests() { - var logger = NullLogger.Instance; - powerShellContext = PowerShellContextFactory.Create(logger); + _psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance); - symbolsService = new SymbolsService(NullLoggerFactory.Instance, powerShellContext, workspace, new ConfigurationService()); - completionHandler = new PsesCompletionHandler(NullLoggerFactory.Instance, powerShellContext, workspace); + symbolsService = new SymbolsService(NullLoggerFactory.Instance, _psesHost, _psesHost, workspace, new ConfigurationService()); + completionHandler = new PsesCompletionHandler(NullLoggerFactory.Instance, _psesHost, _psesHost, workspace); } public void Dispose() { - this.powerShellContext.Close(); + _psesHost.StopAsync().GetAwaiter().GetResult(); } [Trait("Category", "Completions")] @@ -463,14 +464,12 @@ await this.completionHandler.GetCompletionsInFileAsync( scriptRegion.StartColumnNumber).ConfigureAwait(false); } - private async Task GetParamSetSignatures(ScriptRegion scriptRegion) + private Task GetParamSetSignatures(ScriptRegion scriptRegion) { - return - await this.symbolsService.FindParameterSetsInFileAsync( + return this.symbolsService.FindParameterSetsInFileAsync( GetScriptFile(scriptRegion), scriptRegion.StartLineNumber, - scriptRegion.StartColumnNumber, - powerShellContext).ConfigureAwait(false); + scriptRegion.StartColumnNumber); } private async Task GetDefinition(ScriptRegion scriptRegion) diff --git a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs b/test/PowerShellEditorServices.Test/PsesHostFactory.cs similarity index 60% rename from test/PowerShellEditorServices.Test/PowerShellContextFactory.cs rename to test/PowerShellEditorServices.Test/PsesHostFactory.cs index bd5f1a27a..dfe74b836 100644 --- a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs +++ b/test/PowerShellEditorServices.Test/PsesHostFactory.cs @@ -3,21 +3,25 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; using System.IO; +using System.Management.Automation; +using System.Management.Automation.Host; using System.Management.Automation.Runspaces; +using System.Security; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.PowerShell.EditorServices.Hosting; -using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Test { - internal static class PowerShellContextFactory + internal static class PsesHostFactory { // NOTE: These paths are arbitrarily chosen just to verify that the profile paths // can be set to whatever they need to be for the given host. @@ -38,10 +42,8 @@ internal static class PowerShellContextFactory public static System.Management.Automation.Runspaces.Runspace InitialRunspace; - public static PowerShellContextService Create(ILogger logger) + public static PsesInternalHost Create(ILoggerFactory loggerFactory) { - PowerShellContextService powerShellContext = new PowerShellContextService(logger, null, isPSReadLineEnabled: false); - // We intentionally use `CreateDefault2()` as it loads `Microsoft.PowerShell.Core` only, // which is a more minimal and therefore safer state. var initialSessionState = InitialSessionState.CreateDefault2(); @@ -60,66 +62,63 @@ public static PowerShellContextService Create(ILogger logger) "PowerShell Editor Services Test Host", "Test.PowerShellEditorServices", new Version("1.0.0"), - null, + psHost: new NullPSHost(), TestProfilePaths, - new List(), - new List(), + featureFlags: Array.Empty(), + additionalModules: Array.Empty(), initialSessionState, - null, - 0, + logPath: null, + (int)LogLevel.None, consoleReplEnabled: false, usesLegacyReadLine: false, bundledModulePath: BundledModulePath); - InitialRunspace = PowerShellContextService.CreateTestRunspace( - testHostDetails, - powerShellContext, - new TestPSHostUserInterface(powerShellContext, logger), - logger); + var psesHost = new PsesInternalHost(loggerFactory, null, testHostDetails); - powerShellContext.Initialize( - TestProfilePaths, - InitialRunspace, - ownsInitialRunspace: true, - consoleHost: null); + psesHost.TryStartAsync(new HostStartOptions { LoadProfiles = true }, CancellationToken.None).GetAwaiter().GetResult(); - return powerShellContext; + return psesHost; } } - internal class TestPSHostUserInterface : EditorServicesPSHostUserInterface + internal class NullPSHost : PSHost { - public TestPSHostUserInterface( - PowerShellContextService powerShellContext, - ILogger logger) - : base( - powerShellContext, - new SimplePSHostRawUserInterface(logger), - NullLogger.Instance) - { - } + public override CultureInfo CurrentCulture => CultureInfo.CurrentCulture; + + public override CultureInfo CurrentUICulture => CultureInfo.CurrentUICulture; - public override void WriteOutput(string outputString, bool includeNewLine, OutputType outputType, ConsoleColor foregroundColor, ConsoleColor backgroundColor) + public override Guid InstanceId { get; } = Guid.NewGuid(); + + public override string Name => nameof(NullPSHost); + + public override PSHostUserInterface UI { get; } = new NullPSHostUI(); + + public override Version Version { get; } = new Version(1, 0, 0); + + public override void EnterNestedPrompt() { + // Do nothing } - protected override ChoicePromptHandler OnCreateChoicePromptHandler() + public override void ExitNestedPrompt() { - throw new NotImplementedException(); + // Do nothing } - protected override InputPromptHandler OnCreateInputPromptHandler() + public override void NotifyBeginApplication() { - throw new NotImplementedException(); + // Do nothing } - protected override Task ReadCommandLineAsync(CancellationToken cancellationToken) + public override void NotifyEndApplication() { - return Task.FromResult("USER COMMAND"); + // Do nothing } - protected override void UpdateProgress(long sourceId, ProgressDetails progressDetails) + public override void SetShouldExit(int exitCode) { + // Do nothing } } } + diff --git a/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs b/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs index 510bff2c6..495166e09 100644 --- a/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs @@ -4,6 +4,7 @@ using Xunit; using System.IO; using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Test.Session { @@ -28,7 +29,7 @@ public class PathEscapingTests [InlineData("C:\\&nimals\\утка\\qu*ck?.ps1", "C:\\&nimals\\утка\\qu`*ck`?.ps1")] public void CorrectlyWildcardEscapesPaths_NoSpaces(string unescapedPath, string escapedPath) { - string extensionEscapedPath = PowerShellContextService.WildcardEscapePath(unescapedPath); + string extensionEscapedPath = PathUtils.WildcardEscapePath(unescapedPath); Assert.Equal(escapedPath, extensionEscapedPath); } @@ -49,7 +50,7 @@ public void CorrectlyWildcardEscapesPaths_NoSpaces(string unescapedPath, string [InlineData("C:\\&nimals\\утка\\qu*ck?.ps1", "C:\\&nimals\\утка\\qu`*ck`?.ps1")] public void CorrectlyWildcardEscapesPaths_Spaces(string unescapedPath, string escapedPath) { - string extensionEscapedPath = PowerShellContextService.WildcardEscapePath(unescapedPath, escapeSpaces: true); + string extensionEscapedPath = PathUtils.WildcardEscapePath(unescapedPath, escapeSpaces: true); Assert.Equal(escapedPath, extensionEscapedPath); } @@ -71,7 +72,7 @@ public void CorrectlyWildcardEscapesPaths_Spaces(string unescapedPath, string es [InlineData("C:\\&nimals\\утка\\qu*ck?.ps1", "'C:\\&nimals\\утка\\qu*ck?.ps1'")] public void CorrectlyQuoteEscapesPaths(string unquotedPath, string expectedQuotedPath) { - string extensionQuotedPath = PowerShellContextService.QuoteEscapeString(unquotedPath); + string extensionQuotedPath = StringEscaping.SingleQuoteAndEscape(unquotedPath).ToString(); Assert.Equal(expectedQuotedPath, extensionQuotedPath); } @@ -93,31 +94,10 @@ public void CorrectlyQuoteEscapesPaths(string unquotedPath, string expectedQuote [InlineData("C:\\&nimals\\утка\\qu*ck?.ps1", "'C:\\&nimals\\утка\\qu`*ck`?.ps1'")] public void CorrectlyFullyEscapesPaths(string unescapedPath, string escapedPath) { - string extensionEscapedPath = PowerShellContextService.FullyPowerShellEscapePath(unescapedPath); + string extensionEscapedPath = StringEscaping.SingleQuoteAndEscape(PathUtils.WildcardEscapePath(unescapedPath)).ToString(); Assert.Equal(escapedPath, extensionEscapedPath); } - [Trait("Category", "PathEscaping")] - [Theory] - [InlineData("DebugTest.ps1", "DebugTest.ps1")] - [InlineData("../../DebugTest.ps1", "../../DebugTest.ps1")] - [InlineData("C:\\Users\\me\\Documents\\DebugTest.ps1", "C:\\Users\\me\\Documents\\DebugTest.ps1")] - [InlineData("/home/me/Documents/weird&folder/script.ps1", "/home/me/Documents/weird&folder/script.ps1")] - [InlineData("./path/with` some/spaces", "./path/with some/spaces")] - [InlineData("C:\\path\\with`[some`]brackets\\file.ps1", "C:\\path\\with[some]brackets\\file.ps1")] - [InlineData("C:\\look\\an`*\\here.ps1", "C:\\look\\an*\\here.ps1")] - [InlineData("/Users/me/Documents/`?here.ps1", "/Users/me/Documents/?here.ps1")] - [InlineData("/Brackets` `[and` s`]paces/path.ps1", "/Brackets [and s]paces/path.ps1")] - [InlineData("/CJK` chars/脚本/hello.ps1", "/CJK chars/脚本/hello.ps1")] - [InlineData("/CJK` chars/脚本/`[hello`].ps1", "/CJK chars/脚本/[hello].ps1")] - [InlineData("C:\\Animal` s\\утка\\quack.ps1", "C:\\Animal s\\утка\\quack.ps1")] - [InlineData("C:\\&nimals\\утка\\qu`*ck`?.ps1", "C:\\&nimals\\утка\\qu*ck?.ps1")] - public void CorrectlyUnescapesPaths(string escapedPath, string expectedUnescapedPath) - { - string extensionUnescapedPath = PowerShellContextService.UnescapeWildcardEscapedPath(escapedPath); - Assert.Equal(expectedUnescapedPath, extensionUnescapedPath); - } - [Trait("Category", "PathEscaping")] [Theory] [InlineData("NormalScript.ps1")] @@ -126,7 +106,7 @@ public void CorrectlyUnescapesPaths(string escapedPath, string expectedUnescaped public void CanDotSourcePath(string rawFileName) { string fullPath = Path.Combine(ScriptAssetPath, rawFileName); - string quotedPath = PowerShellContextService.QuoteEscapeString(fullPath); + string quotedPath = StringEscaping.SingleQuoteAndEscape(fullPath).ToString(); var psCommand = new System.Management.Automation.PSCommand().AddScript($". {quotedPath}"); diff --git a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs index fc15c7854..d65a186ac 100644 --- a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs @@ -9,13 +9,13 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Utility; using Xunit; namespace Microsoft.PowerShell.EditorServices.Test.Console { + /* public class PowerShellContextTests : IDisposable { // Borrowed from `VersionUtils` which can't be used here due to an initialization problem. @@ -174,4 +174,5 @@ private void OnSessionStateChanged(object sender, SessionStateChangedEventArgs e #endregion } + */ } diff --git a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs index 4e716a1d2..91b6a1e14 100644 --- a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs +++ b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs @@ -16,7 +16,7 @@ public class ScriptFileChangeTests { #if CoreCLR - private static readonly Version PowerShellVersion = new Version(6, 2); + private static readonly Version PowerShellVersion = new Version(7, 2); #else private static readonly Version PowerShellVersion = new Version(5, 1); #endif diff --git a/test/PowerShellEditorServices.Test/Utility/AsyncLockTests.cs b/test/PowerShellEditorServices.Test/Utility/AsyncLockTests.cs deleted file mode 100644 index 99b8bb05c..000000000 --- a/test/PowerShellEditorServices.Test/Utility/AsyncLockTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.PowerShell.EditorServices.Test.Utility -{ - public class AsyncLockTests - { - [Fact] - public async Task AsyncLockSynchronizesAccess() - { - AsyncLock asyncLock = new AsyncLock(); - - Task lockOne = asyncLock.LockAsync(); - Task lockTwo = asyncLock.LockAsync(); - - Assert.Equal(TaskStatus.RanToCompletion, lockOne.Status); - Assert.Equal(TaskStatus.WaitingForActivation, lockTwo.Status); - lockOne.Result.Dispose(); - - await lockTwo.ConfigureAwait(false); - Assert.Equal(TaskStatus.RanToCompletion, lockTwo.Status); - } - - [Fact] - public void AsyncLockCancelsWhenRequested() - { - CancellationTokenSource cts = new CancellationTokenSource(); - AsyncLock asyncLock = new AsyncLock(); - - Task lockOne = asyncLock.LockAsync(); - Task lockTwo = asyncLock.LockAsync(cts.Token); - - // Cancel the second lock before the first is released - cts.Cancel(); - lockOne.Result.Dispose(); - - Assert.Equal(TaskStatus.RanToCompletion, lockOne.Status); - Assert.Equal(TaskStatus.Canceled, lockTwo.Status); - } - } -} diff --git a/test/PowerShellEditorServices.Test/Utility/AsyncQueueTests.cs b/test/PowerShellEditorServices.Test/Utility/AsyncQueueTests.cs deleted file mode 100644 index 70c31f444..000000000 --- a/test/PowerShellEditorServices.Test/Utility/AsyncQueueTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerShell.EditorServices.Utility; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.PowerShell.EditorServices.Test.Utility -{ - public class AsyncQueueTests - { - [Fact] - public async Task AsyncQueueSynchronizesAccess() - { - ConcurrentBag outputItems = new ConcurrentBag(); - AsyncQueue inputQueue = new AsyncQueue(Enumerable.Range(0, 100)); - CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - - try - { - // Start 5 consumers - await Task.WhenAll( - Task.Run(() => ConsumeItemsAsync(inputQueue, outputItems, cancellationTokenSource.Token)), - Task.Run(() => ConsumeItemsAsync(inputQueue, outputItems, cancellationTokenSource.Token)), - Task.Run(() => ConsumeItemsAsync(inputQueue, outputItems, cancellationTokenSource.Token)), - Task.Run(() => ConsumeItemsAsync(inputQueue, outputItems, cancellationTokenSource.Token)), - Task.Run(() => ConsumeItemsAsync(inputQueue, outputItems, cancellationTokenSource.Token)), - Task.Run( - async () => - { - // Wait for a bit and then add more items to the queue - await Task.Delay(250).ConfigureAwait(false); - - foreach (var i in Enumerable.Range(100, 200)) - { - await inputQueue.EnqueueAsync(i).ConfigureAwait(false); - } - - // Cancel the waiters - cancellationTokenSource.Cancel(); - })).ConfigureAwait(false); - } - catch (TaskCanceledException) - { - // Do nothing, this is expected. - } - - // At this point, numbers 0 through 299 should be in the outputItems - IEnumerable expectedItems = Enumerable.Range(0, 300); - Assert.Empty(expectedItems.Except(outputItems)); - } - - [Fact] - public async Task AsyncQueueSkipsCancelledTasks() - { - AsyncQueue inputQueue = new AsyncQueue(); - - // Queue up a couple of tasks to wait for input - CancellationTokenSource cancellationSource = new CancellationTokenSource(); - Task taskOne = inputQueue.DequeueAsync(cancellationSource.Token); - Task taskTwo = inputQueue.DequeueAsync(); - - // Cancel the first task and then enqueue a number - cancellationSource.Cancel(); - await inputQueue.EnqueueAsync(1).ConfigureAwait(false); - - // Wait for things to propegate. - await Task.Delay(1000).ConfigureAwait(false); - - // Did the second task get the number? - Assert.Equal(TaskStatus.Canceled, taskOne.Status); - Assert.Equal(TaskStatus.RanToCompletion, taskTwo.Status); - Assert.Equal(1, taskTwo.Result); - } - - private static async Task ConsumeItemsAsync( - AsyncQueue inputQueue, - ConcurrentBag outputItems, - CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - int consumedItem = await inputQueue.DequeueAsync(cancellationToken).ConfigureAwait(false); - outputItems.Add(consumedItem); - } - } - } -}