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

1.5k views Asked by At

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?

2

There are 2 answers

0
Tubs On BEST ANSWER

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.

0
Paul Totzke On

This answer is correct for dotnet core 3.1 but it should be the same.

host.RunConsoleAsync() waits for Sigterm or ctrl + C.

Switch to host.Start() and the program stops when the IHostedServices finish.

I dont think this line gets hit currently:

System.IO.File.WriteAllText("/path-to-app/_main.txt", "Line 2");