2

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?

Diego
  • 16,436
  • 26
  • 84
  • 136
  • Hmm. I'm wondering as the `IHostedService` is used in the `HostBuilder` it's what is controlling the `SIGTERM`. Once the `Task` has been marked as completed it determines the service has gracefully shutdown. – Tubs Apr 23 '19 at 15:37
  • That could be it. However, I find it kind of wired because I have only one service instance, but I could have multiple instances from different classes. So, if that the case, how should I handle the gracefully shutdown from the outmost scope? – Diego Apr 23 '19 at 17:28
  • I would assume it would continue going down the instance chain until it completed the last one. If you move the `WriteAllText` from after the `RunConsoleAsync` and `finally` to inside the `StopAsync` it could work? Keep the `finally` condition when moving it – Tubs Apr 24 '19 at 08:04
  • If a move the code blocks after the `await` and the one in `finally` to de `Dispose`, it does work. But I think things start to get really messed up. I believe that its not a good idea to let the `HostedService` know how to finalize the whole program. – Diego Apr 24 '19 at 12:28
  • I do see where you are coming from and we did have the same concerns in the past. However, 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 initalise it. – Tubs Apr 24 '19 at 12:33
  • Ok, that seems to make sense. I might follow that path. I was surprised with how hard it was to find information about this though. Thanks @Tubs! – Diego Apr 24 '19 at 13:00
  • Yeah it's quite a different way to approach it that I've only really fully comprehended since starting my work with microservices. I'll summarise what we've discussed as an answer so others can follow along in the future. – Tubs Apr 24 '19 at 13:07
  • I also found [this answer](https://stackoverflow.com/questions/41454563/how-to-write-a-linux-daemon-with-net-core?rq=1) that could prove useful to you – Tubs Apr 24 '19 at 13:19

2 Answers2

1

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");
Paul Totzke
  • 1,470
  • 17
  • 33
0

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.

InteXX
  • 6,135
  • 6
  • 43
  • 80
Tubs
  • 717
  • 9
  • 18