question

YourEyeG-8345 avatar image
0 Votes"
YourEyeG-8345 asked ·

Cancelling read key with CancelIoEx also interrupts the next read

(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").


dotnet-csharpvs-debuggingdotnet-console
10 |1000 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

karenpayneoregon avatar image
0 Votes"
karenpayneoregon answered ·

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));
     }
 }
· 1 ·
10 |1000 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Interesting concept, but this code is just an example on how to reproduce the issue.
My real code, is something that writes text in X seconds. If before that X seconds a SPACE is clicked, then the full text is written immediately.
The console program then requires additional input from the user.

I can't just read the line on another thread: This means that if X seconds have passed, the readline would still "swallow" the next input. I need to actually abort the readline.

0 Votes 0 ·
TimonYang-MSFT avatar image
0 Votes"
TimonYang-MSFT answered ·

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.

· 1 ·
10 |1000 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Yeah, not only when debugging, but also when changing something in the console.
SendKeys is something of Windows Forms, I don't feel comfortable referencing it in a console application.

My current workaround (without simulating inputs) is resizing the console:

         if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
         {
             int height = Console.WindowHeight;
             int width = Console.WindowWidth;
             Console.SetWindowSize(width, height + 1);
             Console.SetWindowSize(width, height);
         }


0 Votes 0 ·