Skip to content

Move PSReadLine logic to cmdlets #1255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ 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')
CmdletsToExport = @(
'Start-EditorServices',
'__Invoke-ReadLineForEditorServices',
'__Invoke-ReadLineConstructor'
)

# Variables to export from this module
VariablesToExport = @()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using System;
using System.Management.Automation;
using System.Runtime.CompilerServices;

namespace Microsoft.PowerShell.EditorServices.Commands
{
/// <summary>
/// The Start-EditorServices command, the conventional entrypoint for PowerShell Editor Services.
/// </summary>
[Cmdlet("__Invoke", "ReadLineConstructor")]
public sealed class InvokeReadLineConstructorCommand : PSCmdlet
{
protected override void EndProcessing()
{
Type type = Type.GetType("Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2");
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Threading;

namespace Microsoft.PowerShell.EditorServices.Commands
{
/// <summary>
/// The Start-EditorServices command, the conventional entrypoint for PowerShell Editor Services.
/// </summary>
[Cmdlet("__Invoke", "ReadLineForEditorServices")]
public sealed class InvokeReadLineForEditorServicesCommand : PSCmdlet
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can drop the Cmdlet decoration and make this internal. When you go to invoke it, use:

ps.AddCommand(
    new CmdletInfo(
        "any-hyphenseparatedname",
        typeof(InvokeReadLineForEditorServicesCommand)))

(and probably staticly store the CmdletInfo)

Edit: hmm I guess the direct CommandInfo route isn't accessible from PSCommand directly for some reason. Either PSCommand.AddCommand needs an overload for CommandInfo, or the Runspaces.Command ctor that takes it needs to be made public.

I'd honestly still recommend going this route, and use reflection to access the Command.ctor(CommandInfo) overload. Then add an internal extension method PSCommand.AddCommand(CommandInfo) so we can use this as necessary.

I really dislike adding global state, though I understand if other folks aren't against it enough to think this approach is warranted.

Copy link
Member Author

@TylerLeonhardt TylerLeonhardt Apr 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to pass on this one for now...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't think I explained that well. That part in particular should be pretty easy, this is what I mean:

internal static PSCommand AddCommand(this PSCommand command, CommandInfo commandInfo)
{
    var rsCommand = (Command)typeof(Command)
        .GetConstructor(
            BindingFlags.Instance | BindingFlags.NonPublic,
            binder: null,
            new[] { typeof(CommandInfo) },
            modifiers: null)
        .Invoke(new object[] { commandInfo });

    return command.AddCommand(rsCommand);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually can't do this without breaking the contract between the PSES.Host proj and PSES proj.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I can put the command in PSES...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually then that loads it in a different ALC... ok there's a bit too much going on here. I'm gonna defer this.

{
private delegate string ReadLineInvoker(
Runspace runspace,
EngineIntrinsics engineIntrinsics,
CancellationToken cancellationToken);

private static Lazy<ReadLineInvoker> s_readLine = new Lazy<ReadLineInvoker>(() =>
{
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));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

});

/// <summary>
/// The ID to give to the host's profile.
/// </summary>
[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);
}
}
}
4 changes: 3 additions & 1 deletion src/PowerShellEditorServices/Server/PsesDebugServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System;
using System.IO;
using System.Management.Automation;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -80,8 +81,9 @@ public async Task StartAsync()
if (_usePSReadLine && _useTempSession && Interlocked.Exchange(ref s_hasRunPsrlStaticCtor, 1) == 0)
{
// This must be run synchronously to ensure debugging works
var command = new PSCommand().AddCommand("__Invoke-ReadLineConstructor");
_powerShellContextService
.ExecuteScriptStringAsync("[System.Runtime.CompilerServices.RuntimeHelpers]::RunClassConstructor([Microsoft.PowerShell.PSConsoleReadLine].TypeHandle)")
.ExecuteCommandAsync<object>(command, sendOutputToHost: true, sendErrorToHost: true)
.GetAwaiter()
.GetResult();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,6 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext

internal class PSReadLinePromptContext : IPromptContext
{
private const string ReadLineScript = @"
[System.Diagnostics.DebuggerHidden()]
[System.Diagnostics.DebuggerStepThrough()]
param()
return [Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null]::ReadLine(
$Host.Runspace,
$ExecutionContext,
$args[0])";

private const string ReadLineInitScript = @"
[System.Diagnostics.DebuggerHidden()]
[System.Diagnostics.DebuggerStepThrough()]
Expand All @@ -41,7 +32,7 @@ internal class PSReadLinePromptContext : IPromptContext
}

Import-Module -ModuleInfo $module
return [Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null]
return [Microsoft.PowerShell.PSConsoleReadLine]
}";

private static ExecutionOptions s_psrlExecutionOptions = new ExecutionOptions
Expand Down Expand Up @@ -138,8 +129,8 @@ public async Task<string> InvokeReadLineAsync(bool isCommandLine, CancellationTo
}

var readLineCommand = new PSCommand()
.AddScript(ReadLineScript)
.AddArgument(_readLineCancellationSource.Token);
.AddCommand("__Invoke-ReadLineForEditorServices")
.AddParameter("CancellationToken", _readLineCancellationSource.Token);

IEnumerable<string> readLineResults = await _powerShellContext.ExecuteCommandAsync<string>(
readLineCommand,
Expand Down