diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psd1 b/module/PowerShellEditorServices/PowerShellEditorServices.psd1 index 77772c481..4c5887f4f 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psd1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psd1 @@ -76,11 +76,7 @@ Copyright = '(c) 2017 Microsoft. All rights reserved.' FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = @( - 'Start-EditorServices', - '__Invoke-ReadLineForEditorServices', - '__Invoke-ReadLineConstructor' -) +CmdletsToExport = @('Start-EditorServices') # Variables to export from this module VariablesToExport = @() diff --git a/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineConstructorCommand.cs b/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineConstructorCommand.cs index 48d28d16d..d27708c39 100644 --- a/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineConstructorCommand.cs +++ b/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineConstructorCommand.cs @@ -12,7 +12,6 @@ namespace Microsoft.PowerShell.EditorServices.Commands /// /// The Start-EditorServices command, the conventional entrypoint for PowerShell Editor Services. /// - [Cmdlet("__Invoke", "ReadLineConstructor")] public sealed class InvokeReadLineConstructorCommand : PSCmdlet { protected override void EndProcessing() diff --git a/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineForEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineForEditorServicesCommand.cs index 7590f769d..20aa66d44 100644 --- a/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineForEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineForEditorServicesCommand.cs @@ -14,7 +14,6 @@ namespace Microsoft.PowerShell.EditorServices.Commands /// /// The Start-EditorServices command, the conventional entrypoint for PowerShell Editor Services. /// - [Cmdlet("__Invoke", "ReadLineForEditorServices")] public sealed class InvokeReadLineForEditorServicesCommand : PSCmdlet { private delegate string ReadLineInvoker( diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 73b20546b..3e74221a5 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Handlers; using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Server; @@ -28,6 +29,12 @@ internal class PsesDebugServer : IDisposable /// 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; @@ -80,8 +87,10 @@ public async Task StartAsync() // 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 - var command = new PSCommand().AddCommand("__Invoke-ReadLineConstructor"); _powerShellContextService .ExecuteCommandAsync(command, sendOutputToHost: true, sendErrorToHost: true) .GetAwaiter() diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index 3af5094c4..4f9639209 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -740,7 +740,15 @@ public async Task> ExecuteCommandAsync( PowerShell shell = this.PromptNest.GetPowerShell(executionOptions.IsReadLine); - shell.Commands = psCommand; + + // 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) diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs index 3812a3250..93a098f2e 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs @@ -3,18 +3,18 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; +using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; +using System.Management.Automation.Runspaces; using System.Threading; using System.Threading.Tasks; -using System; -using System.Management.Automation.Runspaces; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { - using System.Collections.Generic; using System.Management.Automation; - using Microsoft.Extensions.Logging; internal class PSReadLinePromptContext : IPromptContext { @@ -35,6 +35,12 @@ internal class PSReadLinePromptContext : IPromptContext return [Microsoft.PowerShell.PSConsoleReadLine] }"; + 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, @@ -129,7 +135,7 @@ public async Task InvokeReadLineAsync(bool isCommandLine, CancellationTo } var readLineCommand = new PSCommand() - .AddCommand("__Invoke-ReadLineForEditorServices") + .AddCommand(s_lazyInvokeReadLineForEditorServicesCmdletInfo.Value) .AddParameter("CancellationToken", _readLineCancellationSource.Token); IEnumerable readLineResults = await _powerShellContext.ExecuteCommandAsync( diff --git a/src/PowerShellEditorServices/Utility/PSCommandExtensions.cs b/src/PowerShellEditorServices/Utility/PSCommandExtensions.cs new file mode 100644 index 000000000..4f0140f56 --- /dev/null +++ b/src/PowerShellEditorServices/Utility/PSCommandExtensions.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Linq.Expressions; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Reflection; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + internal static class PSCommandExtensions + { + private static readonly Func s_commandCtor; + + static PSCommandExtensions() + { + var ctor = typeof(Command).GetConstructor( + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, + binder: null, + new[] { typeof(CommandInfo) }, + modifiers: null); + + ParameterExpression commandInfo = Expression.Parameter(typeof(CommandInfo), nameof(commandInfo)); + + s_commandCtor = Expression.Lambda>( + Expression.New(ctor, commandInfo), + new[] { commandInfo }) + .Compile(); + } + + // 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) + { + var rsCommand = s_commandCtor(commandInfo); + return command.AddCommand(rsCommand); + } + } +}