1

I have a process with some computations in c#. I usually run it either in console on windows or in docker container on Linux. But I am seeking for a general answer which I would like to be applicable also to other types of apps such as windows service or WPF app.

I want my process to react to the application shutdown. I want to save all the computation, leave the hard drive in a consistent state, and dispose of some resources before the process ends.

I used to do it like this:

class Program
{
    // longRunningTasks can be cancelled by CancellationToken from processExitTokenSource source 
    static Task longRunningTasks;
    static readonly CancellationTokenSource processExitTokenSource = new();

    static async Task doTheMainJob(CancellationToken cancellationToken)
    {
        // the long running computations I am interested in
    }

    static void CurrentDomain_ProcessExit(object? sender, EventArgs e)
    {
        Console.WriteLine("I expect this line to be written to console when ctrl+c is pressed or when process is to be closed by other means.");
        processExitTokenSource.Cancel();
        // this waits for task to end
        longRunningTasks.Wait();
        // aplication should exit after this line is reached
    }

    static async Task Main()
    {
        AppDomain.CurrentDomain.ProcessExit += new EventHandler(CurrentDomain_ProcessExit);
        var tasks = new List<Task>();
        tasks.Add(doTheMainJob(cancellationToken))
        if (Settings.AllowMonitoringThroughHttp) {
            tasks.Add(WebServer.RunAsync(cancellationToken));
        }
        longRunningTasks = Task.WhenAll(tasks.ToArray());
        await longRunningTasks;
    }
}

It always worked. Now I wanted to be modern and I added simple web server Microsoft.AspNetCore.Mvc server to monitor the process.

class WebServer
{
    public static async Task RunAsync(CancellationToken cancellationToken)
    {
        var builder = WebApplication.CreateBuilder(new string[0]);
        builder.Services.AddControllers();
        builder.Services.AddEndpointsApiExplorer();

        var app = builder.Build();
        app.MapControllers();
        await app.RunAsync(cancellationToken);
    }
}

The server listens to some port and replies to http request flawlessly. However it seems to somehow override the ctrl+c behavior and CurrentDomain_ProcessExit method is never called. Hence processExitTokenSource is never Canceled and my computations are never canceled.

What hapens? And how do I configure the web server to behave normally? I want to cancel it myself by cancelation token.

ogererla
  • 21
  • 2

1 Answers1

1

This question answers how to disable Ctrl+C key.

Here we do not want to disable the key but restore its behavior to the default. The closes approximation of the original behavior I could get was by using Console.CancelKeyPress which starts a task which calls Environment.Exit(0).

Environment.Exit(0) cannot be called from the same thread as it would cause dead lock. AppDomain.CurrentDomain.ProcessExit event would wait for WebApplication to exit and WebApplication would wait for Environment.Exit(0) to end.

public class NoopConsoleLifetime : IHostLifetime, IDisposable
    {
        private readonly ILogger<NoopConsoleLifetime> _logger;

        public NoopConsoleLifetime(ILogger<NoopConsoleLifetime> logger)
        {
            _logger = logger;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("StopAsync was called");
            return Task.CompletedTask;
        }

        public Task WaitForStartAsync(CancellationToken cancellationToken)
        {
            // without this ctrl+c just kills the application
            Console.CancelKeyPress += OnCancelKeyPressed;
            return Task.CompletedTask;
        }

        private void OnCancelKeyPressed(object? sender, ConsoleCancelEventArgs e)
        {
            _logger.LogInformation("Ctrl+C has been pressed, ignoring.");
            // e.Cancel = false would turn off the application without the clean up
            e.Cancel = true;
            // not running in task causes dead lock
            // Environment.Exit calls the ProcessExit event which waits for web server which waits for this method to end
            Task.Run(() => Environment.Exit(0));
        }

        public void Dispose()
        {
            Console.CancelKeyPress -= OnCancelKeyPressed;
        }
    }

Just add NoopConsoleLifeTime to dependency injection container.

builder.Services.AddSingleton<IHostLifetime, NoopConsoleLifetime>();

ogererla
  • 21
  • 2