Search code examples
c#linux.net-coredaemonstart-stop-daemon

Gracefully shutdown a generic host in .NET Core 2 linux daemon


I'm completely new with both .NET Core and developing linux daemons. I've been through a couple of similar questions like Killing gracefully a .NET Core daemon running on Linux or Graceful shutdown with Generic Host in .NET Core 2.1 but they didn't solve my problem.

I've built a very simple console application as a test using a hosted service. I want it to run as a daemon but I'm having problems to correctly shut it down. When it runs from the console both in Windows and Linux, everything works fine.

public static async Task Main(string[] args)
{
    try
    {
        Console.WriteLine("Starting");

        var host = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<DaemonService>();
            });

        System.IO.File.WriteAllText("/path-to-app/_main.txt", "Line 1");
        await host.RunConsoleAsync();
        System.IO.File.WriteAllText("/path-to-app/_main.txt", "Line 2");
    }
    finally
    {
        System.IO.File.WriteAllText("/path-to-app/_main-finally.txt", "Line 1");
    }
}

public class DaemonService : IHostedService, IDisposable
{
    public Task StartAsync(CancellationToken cancellationToken)
    {
        System.IO.File.WriteAllText("/path-to-app/_Start.txt", "Line 1");

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        System.IO.File.WriteAllText("/path-to-app/_Stop.txt", "Line 1");

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        System.IO.File.WriteAllText("/path-to-app/_Dispose.txt", "Line 1");
    }
}

If I run the application from the console, everything works as excpected. However when it runs as a daemon, after executing either kill <pid> or systemctl stop <service>, the StopAsync and the Dispose methods are executed, but nothing else: not the in Main after the await nor the finally block.

Note: I'm not using anything from ASP.NET Core. AFAIK it is not necessary for what I'm doing.

Am I doing something wrong? Is this the expected behavior?


Solution

  • Summarizing the conversation below the initial question.

    It appears that the IHostedService used in the HostBuilder is what is controlling the SIGTERM. Once the Task has been marked as completed it determines the service has gracefully shutdown. By moving the System.IO.File.WriteAllText("/path-to-app/_main.txt", "Line 2"); and the code in the finally block inside the scope of the service this was able to be fixed. Modified code provided below.

    public static async Task Main(string[] args)
    {
        Console.WriteLine("Starting");
    
        var host = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
               services.AddHostedService<DaemonService>();
            });
    
        System.IO.File.WriteAllText("/path-to-app/_main.txt", "Line 1");
        await host.RunConsoleAsync();
    }
    
    public class DaemonService : IHostedService, IDisposable
    {
        public Task StartAsync(CancellationToken cancellationToken)
        {
            System.IO.File.WriteAllText("/path-to-app/_Start.txt", "Line 1");
    
            return Task.CompletedTask;
        }
    
        public Task StopAsync(CancellationToken cancellationToken)
        {
                return Task.CompletedTask;
        }
    
        public void Dispose()
        {
            try
            {
                System.IO.File.WriteAllText("/path-to-app/_Dispose.txt", "Line 1");
                System.IO.File.WriteAllText("/path-to-app/_Stop.txt", "Line 1");
            }
            finally
            {
                System.IO.File.WriteAllText("/path-to-app/_main-finally.txt", "Line 1");
            }
        }
    }
    

    As this is running as a service we came to the conclusion that it actually made sense to contain the finalisation of the service itself within that scope, similar to the way ASP.NET Core applications function by providing just the service inside the Program.cs file and allowing the service itself to maintain its dependencies.

    My advice would be to contain as much as you can in the service and just have the Main method initalize it.