-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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