1

What is the best practices to make endless console app .NET Core 3.1 for Ubuntu running as a service on systemd?

First I tried:

static void Main()
{
   //timers (System.Timers.Timer) initialization for background tasks

   Console.ReadKey();
}

and it can't work as a service on systemd because of:

System.InvalidOperationException: Cannot read keys when either application does not have a console or when console input has been redirected.

then I tried to change Console.ReadKey() to while(true) {} but it makes 100% CPU usage.

hooboe
  • 133
  • 8
  • First question: what would make your application exit? Would it exit if sent a signal like SIGTERM? If so, 1) Create a new ManualResetEvent (or similar), 2) Subscribe to `AppDomain.CurrentDomain.ProcessExit` and use it to set the event, 3) Have your `Main` method call `manualResetEvent.WaitOne()` – canton7 Dec 09 '20 at 16:00
  • If this is supposed to be an automatic background service when why are you calling ReadKey? It won't be able to accept user input. – ADyson Dec 09 '20 at 16:02
  • 1
    Anyway... https://swimburger.net/blog/dotnet/how-to-run-a-dotnet-core-console-app-as-a-service-using-systemd-on-linux . If you google ".net core service daemon ubuntu" you'll get other useful-looking results too. – ADyson Dec 09 '20 at 16:03
  • [This](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio) is the recommended way – ps2goat Dec 09 '20 at 16:04

2 Answers2

5

What you want is already available through the Worker Service template. This template creates console applications that can then be hosted as either Windows or Linux services.

Worker services are described in Steve Gordon's What are .NET Worker Services?. Systemd hosting is explained in in Scott Hanselman's dotnet new worker - Windows Services or Linux systemd services in .NET Core. These two articles are a lot cleaner and easier to understand than the official docs. Start from these two before reading up on BackgroundService.

Worker class

A worker service is a class that implements the IHostedService interface and lives as long as the Host of an application lives. Typically, this is done through the BackgroundService base class which implements the StartAsync and StopAsync methods so a worker only needs to implement ExecuteAsync :

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1000, stoppingToken);
        }
    }
}

When termination is signaled either through the console or a system message/signal, the Host notifies the service to exit gracefully before it's forcefully shut down.

public static async Task Main(string[] args)
{
    await CreateHostBuilder(args).Build().RunAsync();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices(services =>
        {
            services.AddHostedService<Worker>();
        });

This host is the same one used by ASP.NET Core services, which means one can configure dependency injection, logging, configuration etc

Console Lifecycle

RunConsoleAsync can be used instead of RunAsync() to allow the application to shutdown on Ctrl+C (on Windows) or SIGINT/SIGTERM (on Linux)

Hosting

The Microsoft.Extensions.Hosting.WindowsService and Microsoft.Extensions.Hosting.Systemd packages take care of setting up the console application to run as either a Windows or systemd service.

To either hosting method, all that's needed is a call to the proper function:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseWindowsService()
        .ConfigureServices(services =>
        {
            services.AddHostedService<Worker>();
        });

or

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseSystemd()
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
        });
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • To add to this answer (that I upvoted), see my answer here ; https://stackoverflow.com/questions/62996235/does-it-make-sense-to-run-a-c-sharp-worker-service-in-docker/62996877#62996877 – granadaCoder Dec 09 '20 at 16:08
1

You probably want the Main method to exit when your process is sent a signal sugh as SIGTERM (which systemd will use to terminate your process, by default). In that case, you don't want to block Main forever: you just want to block it until your app receives SIGTERM.

One easy way to do this is using a ManualResetEvent and the AppDomain.CurrentProcess.ProcessExit event:

public static void Main()
{
    var exitEvent = new ManualResetEvent(initialState: false);
    AppDomain.CurrentDomain.ProcessExit += (o, e) => exitEvent.Set();

    // Set up timers, etc

    exitEvent.WaitOne();
}

If you want better integration with systemd, consider using Microsoft.Extensions.Hosting.Systemd. This is more of a managed solution using a bunch of other hosting infrastructure, see here for the top-level details, and here for using it with systemd specifically.

canton7
  • 37,633
  • 3
  • 64
  • 77