Why is this .NET 6.0 async code not returning

Viewed 147

The following code never returns for me and I don't understand why. It gets stuck on var s = await and never continues. What exactly am I doing wrong? I am running this from VS2022 and it is compiled for .NET 6.0.

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
cts.Token.Register(() => Console.WriteLine("cancelled"));
Console.WriteLine(DateTime.Now);
var s = await Console.In.ReadLineAsync().WaitAsync(cts.Token);
Console.WriteLine(DateTime.Now);
Environment.Exit(0);
3 Answers

Reading from the console with a timeout is surprisingly hard and async in Console.In is all pretend :)

You may notice that Console.In on Windows is System.IO.SyncTextReader and

ReadLineAsync, and even .NET7's ReadLineAsync(CancellationToken cancellationToken), are not that cancellable (from source):

public override Task<string?> ReadLineAsync()
{
   return Task.FromResult(ReadLine());
}

public override ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
{
   return cancellationToken.IsCancellationRequested ?
      ValueTask.FromCanceled<string?>(cancellationToken) :
      new ValueTask<string?>(ReadLine());
}

This is documented on MSDN:

"Read operations on the standard input stream execute synchronously. That is, they block until the specified read operation has completed. This is true even if an asynchronous method, such as ReadLineAsync, is called on the TextReader object returned by the In property.

So you cannot use Console.In. Looking at the source code, it does appear that it creates a synchronous TextReader.

In theory it should be possible to open an asynchronous reader using the STDIN handle, will update....

I see that there are solid answers for the questions of Why is this .NET 6.0 async code not returning and what am I doing wrong (nothing). That being the case, I had to wonder what it would take to fulfill on the original outcome. In the process of reproducing the original issue I came up with a method that conceivably might be useful even though it doesn't directly answer the question as asked.

I would also point out that even if the original code had succeeded, the user only has 2 seconds to complete the entire line. There's no recognition of typing effort and if they miss the deadline to ENTER by even 1 tick then it's all gone. So this method offers the added benefit of restarting the timeout interval every time a key is pressed.

async Task<string> ReadLineWithTimeout(TimeSpan timeout)
{
    List<char> currentLine = new List<char>();
    var wdtStartOrRestart = DateTime.Now;

    while (true)
    {
        while (Console.KeyAvailable)
        {
            wdtStartOrRestart = DateTime.Now;
            ConsoleKeyInfo keyInfo = Console.ReadKey(intercept: true);
            switch (keyInfo.Key)
            {
                case ConsoleKey.Enter:
                    Console.WriteLine();
                    goto breakFromSwitch;
                case ConsoleKey.Backspace:
                    Console.Write("\b \b");
                    var removeIndex = currentLine.Count - 1;
                    if(removeIndex != -1)
                    {
                        currentLine.RemoveAt(removeIndex);
                    }
                    break;
                case ConsoleKey.LeftArrow:
                case ConsoleKey.RightArrow:
                    // Handling these is more than we're taking on right now.
                    break;
                default:
                    Console.Write(keyInfo.KeyChar);
                    currentLine.Add(keyInfo.KeyChar);
                    break;
            }
        }
        await Task.Delay(1);
        if(DateTime.Now.Subtract(wdtStartOrRestart) > timeout)
        {
            throw new TaskCanceledException();
        }
    }
    breakFromSwitch:
        return string.Join(String.Empty, currentLine);
} 

Net Core 6 Test

var timeout = TimeSpan.FromSeconds(5);

while (true)
{
    Console.WriteLine();
    Console.WriteLine(DateTime.Now);
    try
    {
        Console.WriteLine(await ReadLineWithTimeout(timeout) + "(ECHO)");
    }
    catch
    {
        Console.WriteLine("Cancelled");
    }
    Console.WriteLine(DateTime.Now);
}

console output

Related