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

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.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 15
  • Comments: 20 (6 by maintainers)

Commits related to this issue

Most upvoted comments

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.

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.

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.

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);
}

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?