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.
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.
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.
In .NET Core 3.0, there was some incredible work done on the Console API which silently:
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.
- @SeeminglyScience From a recent PR
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.
Because of this less than ideal implementation, imperfections show up… like how you can _see_ the typing when you paste:
Having a proper ReadKeyAsync within .NET can help this experience greatly.
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 checkKeyAvailable
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
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:
@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:
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:
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().
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.
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:
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:
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 ofReadKey
today, only it accepts aCancellationToken
.When that
CancellationToken
is canceled, theReadKeyAsync
is cancelled so that anotherReadKey
/ReadKeyAsync
can be run on another thread, for example, and not be blocked.Please let me know if there’s any additional context I can give! I’m happy to supply it.