Search code examples
c#console-application.net-6.0cancellationcancellation-token

Cancelling the token source does not work and Main function gets stuck in Console app


I wrote the following C# code in .NET 6 in a simple console application. It captures "Interrupt" signal (i.e. Ctrl+C) and signals cancellation token source to cancel its token.

Then in Main method, I want the thread to exit. Why does the following code not work as expected i.e. the Main method does not exit and I don't see the message "Exiting..."

namespace ConsoleApp1
{
    public class Program
    {
        private static CancellationTokenSource cancellationTokenSource =
            new CancellationTokenSource();

        public static void Main(string[] args)
        {
            Console.CancelKeyPress += new ConsoleCancelEventHandler(InterruptHandler);
            Task.Delay(Timeout.Infinite, cancellationTokenSource.Token).Wait();
            Console.WriteLine("Exiting...");
        }

        private static void InterruptHandler(object? sender, ConsoleCancelEventArgs e)
        {
            cancellationTokenSource.Cancel();
        }
    }
}

Then I changed the main method to this, still same thing happened.

public static void Main(string[] args)
{
    Console.CancelKeyPress += new ConsoleCancelEventHandler(InterruptHandler);
    while (cancellationTokenSource.IsCancellationRequested == false)
        Thread.Sleep(1000);

    Console.WriteLine("Exiting...");
}

Solution

  • As I found out, Ctrl+C terminates the process, so your app just ends after pressing the combination. In order to change that behaviour you need to set ConsoleCancelEventArgs.Cancel property to true.

    This will change behaviour, but it won't be exactly as you expect, as cancelling task is just interrupting it by throwing exception, so you'd need to add handling of that exception, please see below example:

    var cts = new CancellationTokenSource();
    
    Console.CancelKeyPress += Console_CancelKeyPress;
    
    try
    {
        //await Task.Delay(10 * 1000, cts.Token);
        Task.Delay(60 * 1000, cts.Token).Wait();
    
        Console.WriteLine("After waiting");
    }
    // This will be thrown when you use .Wait() method
    catch(AggregateException aggEx)
    {
        var tcEx = aggEx.InnerExceptions.FirstOrDefault(x => x is TaskCanceledException);
        if (tcEx != null)
        {
            Console.WriteLine(tcEx.ToString());
        }
    }
    // This will be thrown when you await the task
    catch (TaskCanceledException cacnelledEx)
    {
        Console.WriteLine(cacnelledEx.ToString());
    }
    
    void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
    {
        Console.WriteLine("Cancelling operation");
        cts.Cancel();
        e.Cancel = true;
    }
    

    UPDATE As suggested, you could simplify the code by replacing Wait() method with GetAwaiter().GetResult(), which would throw TaskCancelledException instead of AggregateException:

    try
    {
        //await Task.Delay(10 * 1000, cts.Token);
        Task.Delay(60 * 1000, cts.Token).GetAwaiter().GetResult();
    
        Console.WriteLine("After waiting");
    }
    catch (TaskCanceledException cacnelledEx)
    {
        Console.WriteLine(cacnelledEx.ToString());
    }