Repository navigation
Console.ReadKey throws an InvalidOperationException when other console-attached process dies #88697
Description
Description
The Console.ReadKey function can throw an InvalidOperationException when "application does not have a console or when console input has been redirected". That makes sense. However, there is another case where it throws an InvalidOperationException, which happens when there are multiple processes attached to the same console, both waiting for input, and one dies or is killed.
Here is the code in question, from ConsolePal.Windows.cs (here)
while (true)
{
r = Interop.Kernel32.ReadConsoleInput(InputHandle, out ir, 1, out int numEventsRead);
if (!r || numEventsRead == 0)
{
// This will fail when stdin is redirected from a file or pipe.
// We could theoretically call Console.Read here, but I
// think we might do some things incorrectly then.
throw new InvalidOperationException(SR.InvalidOperation_ConsoleReadKeyOnFile);
}And the same problem exists in KeyAvailable, here:
bool r = Interop.Kernel32.PeekConsoleInput(InputHandle, out Interop.INPUT_RECORD ir, 1, out int numEventsRead);
if (!r)
{
int errorCode = Marshal.GetLastPInvokeError();
if (errorCode == Interop.Errors.ERROR_INVALID_HANDLE)
throw new InvalidOperationException(SR.InvalidOperation_ConsoleKeyAvailableOnFile);
throw Win32Marshal.GetExceptionForWin32Error(errorCode, "stdin");
}This can happen infrequently and effectively at random, so it is not desirable for every single app that uses Console.ReadKey to have to know that it needs to handle this exception itself. Ideally, these APIs could recognize this situation (0 records returned) and simply re-issue the read, instead of blowing up.
Q: But isn't it unusual for multiple processes to be reading input from the same console at the same time?
A: Yes, but it does happen. A common way I've encountered this situation is with build systems: you've got some build coordinator process, spawning bajillions of other console processes (doing build things), and the user hits ctrl+c to cancel it. Due to race conditions between processes being spawned and getting attached to the console, you can end up with some stragglers still attached to the console after the main build coordinator has exited and returned control to the shell process. In this case the user can mash on ctrl+c a bit more, to get back to their shell / get the console back to a usable state, but due to this bug, if their shell is managed code (like PowerShell), then there is a chance their shell will die. :'(
Reproduction Steps
In pwsh:
dotnet new console --name ConsoleReadkeyRepro
cd .\ConsoleReadkeyRepro
Set-Content -Path .\Program.cs -Value @"
using System.Diagnostics;
Console.WriteLine("Hello, World!");
if( (null == args) || (args.Length == 0) )
{
Console.WriteLine( "(parent process)" );
Thread spawner = new Thread( () => {
ProcessStartInfo psi = new ProcessStartInfo( "dotnet", "run --no-build child" );
psi.UseShellExecute = false;
Console.WriteLine( "Spawning child:" );
using( Process? p = Process.Start( psi ) )
{
Thread.Sleep( 4000 );
Console.WriteLine( "killing child" );
p?.Kill();
}
});
spawner.Start();
Thread.Sleep( 2000 );
Console.WriteLine( "parent now also asking for a key..." );
try
{
var ki = Console.ReadKey();
}
catch( Exception e )
{
Console.WriteLine( "unexpected exception: {0}", e );
return -1;
}
}
else
{
Console.WriteLine( "(child process)" );
while( true )
{
Console.WriteLine( "feed me..." );
var ki = Console.ReadKey( true );
}
}
return 0;
"@
dotnet build
dotnet run --no-buildExpected behavior
Console.ReadKey and Console.KeyAvailable should not throw just because some random other process attached to the same console died.
Actual behavior
Console.ReadKey and Console.KeyAvailable throw an InvalidOperationException just because some random other process attached to the same console died.
Regression?
I do not believe this is a regression.
Known Workarounds
Every single caller of these APIs could wrap them with try/catch to manually deal with the InvalidOperationException. Or not run attached to a console that runs any other programs. (This is obviously unfortunate and impractical.)
Configuration
Windows 11, dotnet version 7.0.304.
Other information
No response
Activity
Work around InvalidOperationExceptions from Console API
adamsitnik commentedon Aug 1, 2023
Hi @jazzdelightsme
Thank you for a detailed bug report. Does the error code allow us to differentiate such scenario from the two other scenarios where we want to throw? If that is the case, then I am supportive of introducing such a change.
jazzdelightsme commentedon Aug 1, 2023
Looking at the source for ReadConsoleInput, I see that it sets the last Win32 error on failure, but that doesn't matter: this particular case is actually a success case (
ristrue; it's just thatnumEventsReadis 0 ("we successfully read 0 input records")). So mainly, we should just honorr.So the real question is: "if stdin is redirected from a file or pipe, will
ReadConsoleInputever successfully read 0 input records?"rand we should be good.IsInputRedirectedCoreto detect that. Like so:This second option seems safe enough that I don't know if it's even worth trying to chase down the answer to the question.
Now, for
KeyAvailable... when I was fiddling around with this stuff, I thought I remembered hitting a crash with that API, too, but... it's already just properly honoring the return code!So... I may have just been hallucinating. Or maybe it stuck in my head because I saw this code and saw that it was doing it correctly; whereas the other location wasn't.
Indeed, modifying my repro to use
KeyAvailablein the parent process, like this:I wasn't able to hit a crash. So I think that's my bad; I think just
ReadKeyneeds the fix.adamsitnik commentedon Aug 4, 2023
@jazzdelightsme Thank you for investigating it further. The proposed solution with
ReadConsoleInputsounds very reasonable to me. Would you like to send a PR with a fix?BTW it's interesting that the official docs say that it's impossible to read
0characters on success:jazzdelightsme commentedon Aug 15, 2023
@zadjii-msft any comment on this? Is it a doc bug, or a code bug?
zadjii-msft commentedon Aug 15, 2023
Bear with me as I collect some notes
ApiDispatchers::ServerGetConsoleInputlooks like the guy that handlesReadConsoleInputhttps://github.com/microsoft/terminal/blob/b556594793006ba49934d9e5291cbf235e18b4b3/src/server/ApiDispatchers.cpp#L127-L140
That calls into
ApiRoutines::GetConsoleInputImplhttps://github.com/microsoft/terminal/blob/b556594793006ba49934d9e5291cbf235e18b4b3/src/host/directio.cpp#L73-L78
That always passes
WaitForDataastrue, in the read from the input buffer.Doc comment on that method:
oh but then I think we start getting into the realm of the console read waits. Those are nasty to try and track down. My gut says it's not impossible that there's a case where a client disconnecting might try to complete the other waits that were pending. Tracking that down would be trickier. I also have no idea which is right, certainly not off the top of my head.
jazzdelightsme commentedon Aug 18, 2023
Thanks, @zadjii-msft! Okay, do you want an Issue in Terminal for this? If we're probably not going to do anything about it, and we don't really need to have it for reference, maybe this Issue is enough...
@adamsitnik, yes, I can create a PR. I've been a bit bogged down, bear with me...
zadjii-msft commentedon Aug 21, 2023
FWIW, in microsoft/terminal#15859, we did decide that this was a Conhostv2 bug that regressed some time since conhost v1. We tossed it onto our backlog☺️
Console.ReadKey should not throw when read returns 0 records
jazzdelightsme commentedon Feb 20, 2024
@adamsitnik sorry for the long delay on this; I've finally put together a PR.
I'm having some trouble testing it... I ran the System.Console tests, but I hardcoded an exception, just to verify that the tests were covering this code, but it didn't cause a problem, so either I'm holding it wrong (highly likely, since this is a large and complex project, and I don't have experience with it), or this code isn't covered by a test (not totally unreasonable, since this is in a PAL layer and it is in code that explicitly wouldn't be hit when console IO is redirected, which one would imagine would be the case in a unit testing scenario...). Would there be an easy way for me to run my repro code against a private System.Console library?
Console.ReadKey should not throw when read returns 0 records (#98684)
Console.ReadKey should not throw when read returns 0 records (dotnet#…
Console.ReadKey should not throw when read returns 0 records (dotnet#…