Runtime: Implement an async version of the blocking System.Console.ReadKey

Created on 3 Nov 2017  Â·  20Comments  Â·  Source: dotnet/runtime

In the PowerShell extension for Visual Studio Code, we have an async REPL loop where we'd love to be able to await Console.ReadKeyAsync() rather than blocking on Console.ReadKey().

The current implementation is problematic because we need cancellation support. We have tried to work around the blocking ReadKey() call by using KeyAvailable() but there is a known issue with KeyAvailable() causing the characters to be echo'd to the screen on Linux which is not desirable when you're asking for a user's password.

area-System.Console

Most helpful comment

Hi all, I wanted to revive this old thread with some context - especially since there was a ton of great work done in .NET Core 3.0 in the Console API space that have made our life a lot easier.

I work on PowerShell Editor Services which is the backend to the PowerShell extension for vscode.

image

We do a lot of weird things with the Console API in order to offer an “Integrated Console" experience in vscode. Here’s where we are at with the current Console APIs.

Good news!

In .NET Core 3.0, there was some incredible work done on the Console API which silently:

  • Fixed an awful bug related to sub-processes (sudo, ssh, etc) using native prompts
  • Removed the need for @SeeminglyScience’s UnixConsoleEcho which was used to disable input echo so that we could leverage Console APIs like CursorLeft and CursorTop in other threads while a ReadKey was happening:

So for a long time debugging was mostly broken on *nix because you can't throw ReadKey into another thread and use Console.CursorLeft/Top. If you tried to call either of those when ReadKey was pending, the calls will block until ReadKey returns.
To get around that we wrote the less than ideal implementation of ReadKeyAsync that checks KeyAvailable until it returns true. That worked, but since a read wasn't actually taking place, echo was still turned on. With no API to disable echo other than Console.ReadKey(true), I had to rip the native code that corefx uses out and into it's own library for PSES to consume.

Context

Initially, that implementation's sole purpose was to disable echo. However, recently we added support for PSReadLine which replaces the default PowerShell prompt with its own, and uses Console.ReadKey under the hood… or a delate that you can set via Reflection that PSReadLine will use instead of Console.ReadKey.

This is what we do. We have our “less than ideal” implementation as mentioned above.

What’s missing...

Because of this less than ideal implementation, imperfections show up… like how you can _see_ the typing when you paste:

typey

Having a proper ReadKeyAsync within .NET can help this experience greatly.

What would ReadKeyAsync look like?

Ideally, all we need is a ReadKeyAsync that has the same behavior of ReadKey today, only it accepts a CancellationToken.

When that CancellationToken is canceled, the ReadKeyAsync is cancelled so that another ReadKey/ReadKeyAsync can be run on another thread, for example, and not be blocked.

public static Task<ConsoleKeyInfo> ReadKeyAsync(CancellationToken ctn)

Please let me know if there’s any additional context I can give! I’m happy to supply it.

All 20 comments

we have an async REPL loop where we'd love to be able to await Console.ReadKeyAsync() rather than blocking on Console.ReadKey().

How would ReadKeyAsync be implemented? It wouldn't just be blocking a different thread?

This could go hand in hand with the new async main introduced in C# 7.1. Not knowing enough here but could this not be implimented using an i/o completion port or something similar (I know it would need to be different on linux)

Thanks.

Hey everyone, I just wanted to ping on this thread that this would be amazing for us working on the PowerShell extension for VSCode!

Checkout https://github.com/PowerShell/vscode-powershell/issues/987 for context.

Hi all, I wanted to revive this old thread with some context - especially since there was a ton of great work done in .NET Core 3.0 in the Console API space that have made our life a lot easier.

I work on PowerShell Editor Services which is the backend to the PowerShell extension for vscode.

image

We do a lot of weird things with the Console API in order to offer an “Integrated Console" experience in vscode. Here’s where we are at with the current Console APIs.

Good news!

In .NET Core 3.0, there was some incredible work done on the Console API which silently:

  • Fixed an awful bug related to sub-processes (sudo, ssh, etc) using native prompts
  • Removed the need for @SeeminglyScience’s UnixConsoleEcho which was used to disable input echo so that we could leverage Console APIs like CursorLeft and CursorTop in other threads while a ReadKey was happening:

So for a long time debugging was mostly broken on *nix because you can't throw ReadKey into another thread and use Console.CursorLeft/Top. If you tried to call either of those when ReadKey was pending, the calls will block until ReadKey returns.
To get around that we wrote the less than ideal implementation of ReadKeyAsync that checks KeyAvailable until it returns true. That worked, but since a read wasn't actually taking place, echo was still turned on. With no API to disable echo other than Console.ReadKey(true), I had to rip the native code that corefx uses out and into it's own library for PSES to consume.

Context

Initially, that implementation's sole purpose was to disable echo. However, recently we added support for PSReadLine which replaces the default PowerShell prompt with its own, and uses Console.ReadKey under the hood… or a delate that you can set via Reflection that PSReadLine will use instead of Console.ReadKey.

This is what we do. We have our “less than ideal” implementation as mentioned above.

What’s missing...

Because of this less than ideal implementation, imperfections show up… like how you can _see_ the typing when you paste:

typey

Having a proper ReadKeyAsync within .NET can help this experience greatly.

What would ReadKeyAsync look like?

Ideally, all we need is a ReadKeyAsync that has the same behavior of ReadKey today, only it accepts a CancellationToken.

When that CancellationToken is canceled, the ReadKeyAsync is cancelled so that another ReadKey/ReadKeyAsync can be run on another thread, for example, and not be blocked.

public static Task<ConsoleKeyInfo> ReadKeyAsync(CancellationToken ctn)

Please let me know if there’s any additional context I can give! I’m happy to supply it.

Also, I’m happy to reopen this in a new issue if it makes sense. I know replying to old issues sometimes get lost in the incoming new issues.

@tmds also adding you to this since your name pops up in blame in the c implementation of the console apis

Fixed an awful bug related to sub-processes (sudo, ssh, etc) using native prompts

It's not clear for me what the bug is and what fixed it.

Removed the need for @SeeminglyScience’s UnixConsoleEcho which was used to disable input echo

Echoing is now off unless a child process may need it to be on.

Console APIs like CursorLeft and CursorTop in other threads while a ReadKey was happening

We maintain a cached cursor position. Note that if there is no cached cursor position, ReadKey can still cause CursorLeft/Top to block.

To understand the use-case, why are you calling CursorLeft/Top from a different thread than ReadKey?

like how you can see the typing when you paste

You mean, the pasted block doesn't show up as a while, but it comes char by char?
Could you check KeyAvailable and continue reading the characters, and then write them out together?

@tmds

Fixed an awful bug related to sub-processes (sudo, ssh, etc) using native prompts

It's not clear for me what the bug is and what fixed it.

The bug was ours, as part of our workarounds we were setting termios attributes ourselves. In some circumstances they weren't getting set back properly so native sub-processes would unexpectedly be using our settings. The "fix" was the change in how corefx changes those attributes, eliminating the need for our implementation.

Removed the need for @SeeminglyScience’s UnixConsoleEcho which was used to disable input echo

Echoing is now off unless a child process may need it to be on.

Yeah, that's the "fix" I'm referring to above.

Console APIs like CursorLeft and CursorTop in other threads while a ReadKey was happening

We maintain a cached cursor position. Note that if there is no cached cursor position, ReadKey can still cause CursorLeft/Top to block.

To understand the use-case, why are you calling CursorLeft/Top from a different thread than ReadKey?

TL;DR: Events that are processed between key presses could write to console requiring adjusting position.

That is sort of a complicated question to answer, I'm going to try to keep it concise but I apologize if it runs along a bit.

First it's important to note that PowerShell is mostly designed around being in a single thread. Most of it's state is based on thread static storage. Access to this thread is crucial for most PowerShell related tasks.

PSReadLine (the library that handles REPL for PowerShell) is always running whenever a command is not. It needs access to that thread for state based completion, custom prompt text, as well as evaluating the input content. Unfortunately, PowerShell's event system also needs access to that thread to process script based event handlers. So the choice is to either call ReadKey in the main thread and block all events from processing, or call it in another thread so it can allow the engine to process those events.

Before PSRL calls out to the PowerShell engine for event processing, it checks they cursor position so that it can re-adjust position if an event handler writes to the console. That's the first place it blocks, but even removing that wouldn't help much. Several parts of PowerShell's host application will also check cursor position before and after command invocations (which processing an event counts as). Additionally since the event handler is user code, any of them could check cursor position as well. All while ReadKey is still pending in another thread.

like how you can see the typing when you paste

You mean, the pasted block doesn't show up as a while, but it comes char by char?
Could you check KeyAvailable and continue reading the characters, and then write them out together?

Yeah that's what we currently do.

@SeeminglyScience thanks for responding - I must have missed the notification from @tmds's response. Sorry for the delay. @SeeminglyScience explained it perfectly.

Is there any more information that we can provide @tmds / @stephentoub?

If something like this made its way into 3.1, then PowerShell 7 would be able to pick it up and take advantage of it.

No one wants to risk breaking console for 3.x, so improvements will be 5.0.

For new APIs (e.g. ReadKeyAsync), you need to follow: https://github.com/dotnet/corefx/blob/master/Documentation/project-docs/api-review-process.md. The top comment here is out-of-date and the author isn't active, so maybe we should close this, and create a new issue.

For other issues (like: _you can see the typing when you paste_) please create separate issues.

(I'm also not clear on how you propose for such a method to be implemented on both Windows and Unix, and implemented in a way that doesn't block a thread.)

Thanks for the clarification and speedy response. It makes total sense. Let me take a look at the API review process.

(@stephentoub yeah this is definitely one of those situations where I was hoping that you folks know of some crazy magic that could accomplish this but I'll do some digging.)

For other issues (like: you can see the typing when you paste) please create separate issues.

Just to clarify, this is because the PowerShell extension for vscode on non-Windows uses the following fake ReadKeyAsync implementation

https://github.com/PowerShell/PowerShellEditorServices/blob/master/src/PowerShellEditorServices/Services/PowerShellContext/Console/UnixConsoleOperations.cs#L86-L119

Which uses System.Console.KeyAvailable:

private bool IsKeyAvailable(CancellationToken cancellationToken)
        {
            s_stdInHandle.Wait(cancellationToken);
            try
            {
                return System.Console.KeyAvailable;
            }
            finally
            {
                s_stdInHandle.Release();
            }
        }

where the cancellationToken is cancelled every Xms so that it's:

  • not sitting there spinning keeping the machine awake
  • cancelled so other events can take over if need be

@SeeminglyScience please correct me if I'm wrong.

where the cancellationToken is cancelled every Xms so that it's:

In the actual code KeyAvailable isn't called directly, but instead a separate method is called that polls KeyAvailable with a frequency determined by how recently a key was pressed. The actual wait happens in that method, not through the cancellation token. (I know you were trying to simplify the example, but that's the important bit in this scenario).

The very simplified version of the code that is running in a right click paste scenario would be sorta like this:

private ConsoleKeyInfo ReadKey(CancellationToken token)
{
    while (!Console.KeyAvailable)
    {
        token.ThrowIfCancellationRequested();
        Thread.Sleep(30);
    }

    return Console.ReadKey(true);
}

Thanks @SeeminglyScience. As a result, this is what pasting looks like:

paste-in-console

We can close this. Based on more confirmation by @stephentoub offline, this isn't feasible.

Based on more confirmation by @stephentoub offline, this isn't feasible.

On Linux, the thread that waits for an event on standard input could be shared with others. Similar to how the poll threads are shared between Sockets.
I don't know about Windows though.

Thanks, Tom.

I considered that, but have some concerns:

  • It represents a huge amount of infrastructure / churn to address this corner case. We'd need to reuse / repurpose / extend the sockets infrastructure on epoll/kqueues to enable this (to avoid duplicating a ton of code and behavior), but we would need to do so in a way that didn't destabilize or negatively impact sockets perf in any way.
  • To do basic reading would entail spinning up an additional thread and the relevant coordination between threads, which could impact startup time, working set, etc. for even simple utilities.
  • We would need to maintain existing semantics here, in particular that we currently synchronize all Console-related activity, and folks rely on that, e.g. rely on being able to lock on Console.In to synchronize with other reading activities (we've had complaints in the past when we've made changes that broke that and had to revert).
  • We would need to ensure that everything continued to work correctly whether there were active child processes or not; it's an additional layer of complexity on an already complex set of interactions, and for which we don't have great automated testing.
  • We would need to ensure that everything continued to work correctly regardless of the file type of the stdin file descriptor; epoll is finicky about certain file types, e.g. if stdin were redirected from a disk file.

That said, if we did do the work to make our epoll/kqueues support more general-purpose, and validate that it has no negative impact on sockets, it could be used for positive gains elsewhere, e.g. using it to improve how we do waiting and cancellation in anonymous pipes, using it to aid FileStream when it's wrapped around a SafeFileHandle for something other than a disk file, etc.

So, if you have the cycles to prototype it and prove it out, it'd be interesting to see the results.

I don't see why this is not feasible. I imagine a dedicated thread that polls the device 20x a second and puts available keys into a thread-safe queue, to be read asynchronously by ReadKeyAsync().

Was this page helpful?
0 / 5 - 0 ratings

Related issues

EgorBo picture EgorBo  Â·  3Comments

omariom picture omariom  Â·  3Comments

bencz picture bencz  Â·  3Comments

btecu picture btecu  Â·  3Comments

Timovzl picture Timovzl  Â·  3Comments