Cancelling read key with CancelIoEx also interrupts the next read

YourEyeG 1 Reputation point
2021-02-01T20:02:45.967+00:00

(reposted from stackoverflow and Microsoft developer community)

Following https://stackoverflow.com/questions/9479573/how-to-interrupt-console-readline/9634490 to figure out to interrupt a Console.ReadlLine (and Console.ReadKey), I encountered the following behavior (I don't know if it's a bug in Windows, the .NET framework, or whatnot):
When I use CancelIoEx to cancel a ReadKey (or ReadLine), it cancels the read successfully (i.e., interrupts the call and the thread continues), but it seems the read action is still happening in the console (and will be ignored when done).

Am I doing something wrong? Is it a bug in Windows/.NET? Should I perform another action to stop the actual read?

How to reproduce:
1- I'm using a console project with .NET Framework 4.7.2 (this would reproduce on .NET5.0 as well).
2- I'm running the following code:

class Program
{
    const int STD_INPUT_HANDLE = -10;
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr GetStdHandle(int nStdHandle);
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool CancelIoEx(IntPtr handle, IntPtr lpOverlapped);

    private static void InterruptReadLine()
    {
        var handle = GetStdHandle(STD_INPUT_HANDLE);
        bool success = CancelIoEx(handle, IntPtr.Zero);
    }

    private static ConsoleKey? ReadHiddenKey()
    {
        ConsoleKey? result = null;
        try
        {
            result = Console.ReadKey(true).Key;
        }
        // This is interrupted on Dispose.
        catch (InvalidOperationException) { }
        catch (OperationCanceledException) { }
        return result;
    }

    static void Main(string[] args)
    {
        Task task = Task.Delay(2000).ContinueWith(_ =>
        {
            Console.WriteLine("Interrupting...");
            InterruptReadLine();
        });

        Console.WriteLine("Read key1");
        var key1 = ReadHiddenKey();
        Console.WriteLine("Read key2");
        var key2 = ReadHiddenKey();
        Console.WriteLine($"Keys: {key1}, {key2}");
    }
}

3- Wait 2 seconds, then click a key (the key is ignored) and then another key (which will be written). The output for 'a' then 'b':

Read key1
Interrupting...
Read key2
Keys: , B

4- I would expect the first actual read (for key2) would not be ignored (i.e., the output would say Keys: , A and I will not have a chance to click 'b').
5- Note that if I change something in the console (change its size, or minimize/maximize) it then behaves as expected: After 2 seconds, it interrupts, I change the size, click a and its done. No need for an extra key input.

Note that when experimenting with it I encountered the following behavior as well:

  1. Changing the try in ReadHiddenKey to
        var str = Console.ReadLine();
        result = ConsoleKey.Enter;
        Console.WriteLine("Read: " + str);
    
  2. Adding Console.ReadKey(); before the second ReadHiddenKey.

The result for when writing abc then enter and then 1234 and enter is:

Read key1
Interrupting...
Read key2
abc
1234
Read: 234
Keys: , Enter

Proving that the behavior is that the first Console.ReadLine() throws an exception, but continues as usual (thus the first line I write is "swallowed").

.NET CLI
.NET CLI
A cross-platform toolchain for developing, building, running, and publishing .NET applications.
322 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,234 questions
Visual Studio Debugging
Visual Studio Debugging
Visual Studio: A family of Microsoft suites of integrated development tools for building applications for Windows, the web and mobile devices.Debugging: The act or process of detecting, locating, and correcting logical or syntactical errors in a program or malfunctions in hardware. In hardware contexts, the term troubleshoot is the term more frequently used, especially if the problem is major.
938 questions
0 comments No comments
{count} votes

2 answers

Sort by: Most helpful
  1. Karen Payne MVP 35,036 Reputation points
    2021-02-01T20:23:24.687+00:00

    I would go with a different approach.

    using System;
    using System.Threading.Tasks;
    using static System.Console;
    using static ConsoleReadLineTimeOut.ConsoleHelpers;
    
    namespace ConsoleReadLineTimeOut
    {
        class Program
        {
            static void Main(string[] args)
            {
                WriteLine("First name");
                var firstName = ReadLineWaitFiveSecond();
                WriteLine(string.IsNullOrWhiteSpace(firstName) ? "No first name" : $"First name: {firstName}");
    
                WriteLine("Last name");
                var lastName = ReadLineWaitFiveSecond();
                WriteLine(string.IsNullOrWhiteSpace(lastName) ? "No last name" : $"Last name: {lastName}");
    
                WriteLine("Done");
                ReadLineWaitTenSecond();
    
            }
        }
    
        public static class ConsoleHelpers
        {
            public static string ConsoleReadLineWithTimeout(TimeSpan? timeout = null)
            {
    
                var timeSpan = timeout ?? TimeSpan.FromSeconds(1);
                var task = Task.Factory.StartNew(ReadLine);
                var result = (Task.WaitAny(new Task[] { task }, timeSpan) == 0) ? task.Result : string.Empty;
    
                return result;
    
            }
    
            public static string ReadLineWaitFiveSecond() => ConsoleReadLineWithTimeout(TimeSpan.FromSeconds(5));
            public static string ReadLineWaitTenSecond() => ConsoleReadLineWithTimeout(TimeSpan.FromSeconds(10));
        }
    }
    

  2. Timon Yang-MSFT 9,571 Reputation points
    2021-02-02T06:28:01.967+00:00

    I can reproduce your error, but I don't know what the cause is.

    When debugging step by step, everything is as expected, but when running directly, this problem occurs.

    I will continue to explore this issue. Before finding the real solution, the following is only a temporary solution, using SendKeys to simulate the first input after interruption.

                Console.WriteLine("Read key2");  
                if(key1 == null) SendKeys.SendWait("{ENTER}");  
                var key2 = ReadHiddenKey();  
    

    If the response is helpful, please click "Accept Answer" and upvote it.
    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.