Skip to content

Console.ReadKey throws an InvalidOperationException when other console-attached process dies #88697

@jazzdelightsme

Description

@jazzdelightsme

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-build

Expected 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.Consolein-prThere is an active PR which will close this issue when it is merged

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions