Get application exit CancellationToken

Get application exit CancellationToken

Intro

Currently, our applications use more and more asynchronous methods. Asynchronous methods usually have a CancellationToken parameter to cancel our asynchronous operation in time. So how do we get the CancellationToken, we only need to use a CancellationTokenSource to cancel it when the application exits.

Cancel C exit

For a simple Ctrl + C exit, we can handle it through the Console.CancelKeyPress event, as follows:

public static class ConsoleHelper
{
    static ConsoleHelper()
    {
        Console.CancelKeyPress + = (sender, args) =>
        {
            CancellationTokenSource.Cancel(false);
        };
    }

    public static CancellationToken GetExitToken()
    {
        return CancellationTokenSource.Token;
    }

    private static readonly CancellationTokenSource CancellationTokenSource = new();
}

If we want to register the exit event, we can use this CancellationToken to Register

Let's test it. The test code is as follows:

var exitToken = ConsoleHelper.GetExitToken();
exitToken.Register(() => Console.WriteLine(@"Console exiting"));
Console.ReadLine();

e147baeaa764828aa0da66ec9c724cf9.png

Let’s slightly modify the test code and add a three-second wait in the callback.

var exitToken = ConsoleHelper.GetExitToken();
exitToken.Register(() =>
{
    Console.WriteLine(@"Console exiting");
    Thread. Sleep(3000);
    Console.WriteLine(@"Console exited");
});
Console.WriteLine("starting");
Console.ReadLine();

df001097ee2c0d6e7385f7e3f98295f6.png

From the output, we can see that our Console exited was not printed.

If you pay attention, you will find that the event parameter ConsoleCancelEventArgs of CancelKeyPress has an attribute of Cancel, and the default value is false

If we set it to true, the end process will be canceled. ASP.NET Core also uses this to achieve graceful shutdown.

Let's modify the exitToken and set it to true when the event is processed

args.Cancel = true;

Modify the example:

var exitToken = ConsoleHelper. GetExitToken();
exitToken.Register(() =>
{
    Console.WriteLine(@"Console exiting");
    Thread. Sleep(3000);
    Console.WriteLine(@"Console exited");
});
Console.WriteLine("starting");
Console. ReadLine();
Console.WriteLine("exiting");
Console.ReadLine();

e572c98e000261de8c545ca4d88e7573.png

Let's remove the setting of Cancel and try again

80aadb79e99059612b6abc715ca9bc11.png

I don’t know if you can see the difference. When it is set to true, it blocks to the last Console.ReadLine(). When it is not set, the process terminates. You can adjust it according to your own needs. need to be adjusted

Process exits

Previously we only processed the Console.CancelKeyPress event. The actual process may be forcibly terminated externally. These situations will basically not be captured and require additional settings.

We can find the method of registering the application exit event in the code of ConsoleLifetime in the Hosting part of dotnet core

Some time ago, I also saw Brother Shitou’s test of the exit of various applications they tested. Here I borrow the test results of Brother Shitou:

1ff54d2a7bafc6079c6912d647e6ffae.png

If you are interested, you can refer to: https://newlifex.com/blood/elegant_exit, and also searched for their application exit registration, most of them are the same

There is a SIGQUIT that is registered in ConsoleLifetime, but Shitou didn’t register it in their library, so I submitted a PR to them

https://github.com/NewLifeX/X/pull/128

All registration methods used are as follows:

static void InvokeExitHandler(object? sender, EventArgs? args);

// https://github.com/NewLifeX/X/blob/e65dfa0998ec393804f3f793f333c237110d890e/NewLife.Core/Model/Host.cs#L61
// https://github.com/dotnet/runtime/blob/940b332ad04e58862febe019788a5b21e266ea10/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.notnetcoreapp.cs
AppDomain.CurrentDomain.ProcessExit += InvokeExitHandler;
Console.CancelKeyPress + = InvokeExitHandler;
#if NETCOREAPP
System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += ctx => InvokeExitHandler(ctx, null);
#endif
#if NET6_0_OR_GREATER
// https://github.com/dotnet/runtime/blob/940b332ad04e58862febe019788a5b21e266ea10/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.netcoreapp.cs
PosixSignalRegistration.Create(PosixSignal.SIGINT, ctx => InvokeExitHandler(ctx, null));
PosixSignalRegistration.Create(PosixSignal.SIGQUIT, ctx => InvokeExitHandler(ctx, null));
PosixSignalRegistration.Create(PosixSignal.SIGTERM, ctx => InvokeExitHandler(ctx, null));
#endif

https://github.com/WeihanLi/WeihanLi.Common/blob/578c5ba80bad9b8073ae6dec3403f884a7ab4e84/src/WeihanLi.Common/Helpers/InvokeHelper.cs#L12

ExitToken

The implementation code is as follows:

private static readonly object _exitLock = new();
private static volatile bool _exited;
private static readonly Lazy<CancellationTokenSource> LazyCancellationTokenSource = new();

public static CancellationToken GetExitToken() => LazyCancellationTokenSource.Value.Token;

private static void InvokeExitHandler(object? sender, EventArgs? args)
{
    if (_exited) return;
    lock (_exitLock)
    {
        if (_exited) return;
        Debug.WriteLine("exiting...");
        if (LazyCancellationTokenSource.IsValueCreated)
        {
            LazyCancellationTokenSource.Value.Cancel(false);
            LazyCancellationTokenSource.Value.Dispose();
        }
        Debug.WriteLine("exited");
        _exited = true;
    }
}

CancellationTokenSource uses Lazy for lazy initialization and does not create it when it is not needed. Use _exited and _exitLock to avoid exit. The method is executed multiple times. If CancellationTokenSource is not created, there is no need to process it. If it is created, trigger CancellationToken and Dispose after triggering

Sample

The usage is similar to the previous ConsoleHelper.GetExitToken()

var exitToken = InvokeHelper.GetExitToken();
exitToken.Register(() =>
{
    Console.WriteLine(@"Exiting");
    Thread.Sleep(3000);
    Console.WriteLine(@"Exited");
});

while(!exitToken.IsCancellationRequested)
{
    System.Console.WriteLine(DateTimeOffset.Now);
    await Task.Delay(1000);
}

The output is as follows:

77e8b8b9bdcc1415af21434115f343ac.png

References

  • https://newlifex.com/blood/elegant_exit

  • https://github.com/NewLifeX/X/blob/e65dfa0998ec393804f3f793f333c237110d890e/NewLife.Core/Model/Host.cs#L61

  • https://github.com/dotnet/runtime/blob/940b332ad04e58862febe019788a5b21e266ea10/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.notnetcoreapp.cs

  • https://github.com/dotnet/runtime/blob/940b332ad04e58862febe019788a5b21e266ea10/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.netcoreapp.cs

  • https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html

  • https://github.com/NewLifeX/X/pull/128

  • https://github.com/WeihanLi/WeihanLi.Common/blob/dev/src/WeihanLi.Common/Helpers/InvokeHelper.cs

syntaxbug.com © 2021 All Rights Reserved.