21

I could just write a long-running CLI app and run it, but I'm assuming it wouldn't comply to all the expectations one would have of a standards-compliant linux daemon (responding to SIGTERM, Started by System V init process, Ignore terminal I/O signals, etc.)

Most ecosystems have some best-practice way of doing this, for example, in python, you can use https://pypi.python.org/pypi/python-daemon/

Is there some documentation about how to do this with .Net Core?

Nkosi
  • 235,767
  • 35
  • 427
  • 472
Jordan Morris
  • 2,101
  • 2
  • 24
  • 41
  • Questions seeking off-site resources like tutorials, software, and documentation are off topic here at SO. I would vote to close, but the bounty is preventing it. – JNevill Sep 19 '17 at 15:52
  • 3
    Most of the assumption you list aren't really a concern on modern systems. Process managers like systemd (which is used on fedora/ubuntu/redhat/centos/arch/others) take care of running things in the background, and actually work best with programs that simply stay in the foreground and don't try to do anything fancy with fork() or signals. – larsks Sep 19 '17 at 16:02
  • 1
    https://developers.redhat.com/blog/2017/06/07/writing-a-linux-daemon-in-c/ – Nkosi Sep 20 '17 at 23:31
  • Can you show what you have tried so far in a [mcve] – Nkosi Sep 21 '17 at 03:30
  • Are you using an async Main? – Nkosi Sep 21 '17 at 03:31
  • @SteveClanton Just realized that bounty was not raised by the OP. Did the link provided satisfy you request? – Nkosi Sep 21 '17 at 11:09
  • @Nkosi Can I add a MCVE to the question? It seems like that would be overstepping on editing. In retrospect, I should have written a new version of the question, but this seemed like exactly what I wanted to do when I read it.Honestly, I didn't know you could do an async main. I thought about posting what I did (creating a task an ending that runs until sigkill) as an answer and seeing if that inspires something better. – Steve Clanton Sep 21 '17 at 12:50
  • @JNevill This isn't asking for an offsite resource. It's asking for something that should be documented as part of the framework and doesn't seem to be IMHO. – Steve Clanton Sep 21 '17 at 12:51
  • @SteveClanton I toyed with an idea similar to how .net core web host waits for shutdown in console applications, but wanted to see if it would fit into what you have so far. from there creating the systemd as in the link should get you the rest of the way – Nkosi Sep 21 '17 at 12:54
  • @Nkosi I figured other frameworks in .Net have a way that they run in the background. If you can post the way the web host waits for a shutdown, that would probably be about the best the community has. – Steve Clanton Sep 21 '17 at 12:58
  • @SteveClanton I was reviewing it on GitHub and was able to extract the gist of how they performed the wait https://github.com/aspnet/Hosting/blob/15008b0b7fcb54235a9de3ab844c066aaf42ea44/src/Microsoft.AspNetCore.Hosting/WebHostExtensions.cs#L86 – Nkosi Sep 21 '17 at 13:00
  • @SteveClanton I tried adapting something like a `IConsoleHost` but quickly realized I was over-engineering it. Extracted the main parts into something like `await ConsoleUtil.WaitForShutdownAsync();` that operated like `Console.ReadLine` – Nkosi Sep 21 '17 at 13:04

6 Answers6

26

I toyed with an idea similar to how .net core web host waits for shutdown in console applications. I was reviewing it on GitHub and was able to extract the gist of how they performed the Run

https://github.com/aspnet/Hosting/blob/15008b0b7fcb54235a9de3ab844c066aaf42ea44/src/Microsoft.AspNetCore.Hosting/WebHostExtensions.cs#L86

public static class ConsoleHost {
    /// <summary>
    /// Block the calling thread until shutdown is triggered via Ctrl+C or SIGTERM.
    /// </summary>
    public static void WaitForShutdown() {
        WaitForShutdownAsync().GetAwaiter().GetResult();
    }


    /// <summary>
    /// Runs an application and block the calling thread until host shutdown.
    /// </summary>
    /// <param name="host">The <see cref="IWebHost"/> to run.</param>
    public static void Wait() {
        WaitAsync().GetAwaiter().GetResult();
    }

    /// <summary>
    /// Runs an application and returns a Task that only completes when the token is triggered or shutdown is triggered.
    /// </summary>
    /// <param name="host">The <see cref="IConsoleHost"/> to run.</param>
    /// <param name="token">The token to trigger shutdown.</param>
    public static async Task WaitAsync(CancellationToken token = default(CancellationToken)) {
        //Wait for the token shutdown if it can be cancelled
        if (token.CanBeCanceled) {
            await WaitAsync(token, shutdownMessage: null);
            return;
        }
        //If token cannot be cancelled, attach Ctrl+C and SIGTERN shutdown
        var done = new ManualResetEventSlim(false);
        using (var cts = new CancellationTokenSource()) {
            AttachCtrlcSigtermShutdown(cts, done, shutdownMessage: "Application is shutting down...");
            await WaitAsync(cts.Token, "Application running. Press Ctrl+C to shut down.");
            done.Set();
        }
    }

    /// <summary>
    /// Returns a Task that completes when shutdown is triggered via the given token, Ctrl+C or SIGTERM.
    /// </summary>
    /// <param name="token">The token to trigger shutdown.</param>
    public static async Task WaitForShutdownAsync(CancellationToken token = default (CancellationToken)) {
        var done = new ManualResetEventSlim(false);
        using (var cts = CancellationTokenSource.CreateLinkedTokenSource(token)) {
            AttachCtrlcSigtermShutdown(cts, done, shutdownMessage: string.Empty);
            await WaitForTokenShutdownAsync(cts.Token);
            done.Set();
        }
    }

    private static async Task WaitAsync(CancellationToken token, string shutdownMessage) {
        if (!string.IsNullOrEmpty(shutdownMessage)) {
            Console.WriteLine(shutdownMessage);
        }
        await WaitForTokenShutdownAsync(token);
    }


    private static void AttachCtrlcSigtermShutdown(CancellationTokenSource cts, ManualResetEventSlim resetEvent, string shutdownMessage) {
        Action ShutDown = () => {
            if (!cts.IsCancellationRequested) {
                if (!string.IsNullOrWhiteSpace(shutdownMessage)) {
                    Console.WriteLine(shutdownMessage);
                }
                try {
                    cts.Cancel();
                } catch (ObjectDisposedException) { }
            }
            //Wait on the given reset event
            resetEvent.Wait();
        };

        AppDomain.CurrentDomain.ProcessExit += delegate { ShutDown(); };
        Console.CancelKeyPress += (sender, eventArgs) => {
            ShutDown();
            //Don't terminate the process immediately, wait for the Main thread to exit gracefully.
            eventArgs.Cancel = true;
        };
    }

    private static async Task WaitForTokenShutdownAsync(CancellationToken token) {
        var waitForStop = new TaskCompletionSource<object>();
        token.Register(obj => {
            var tcs = (TaskCompletionSource<object>)obj;
            tcs.TrySetResult(null);
        }, waitForStop);
        await waitForStop.Task;
    }
}

I tried adapting something like a IConsoleHost but quickly realized I was over-engineering it. Extracted the main parts into something like await ConsoleUtil.WaitForShutdownAsync(); that operated like Console.ReadLine

This then allowed the utility to be used like this

public class Program {

    public static async Task Main(string[] args) {
        //relevant code goes here
        //...

        //wait for application shutdown
        await ConsoleUtil.WaitForShutdownAsync();
    }
}

from there creating a systemd as in the following link should get you the rest of the way

Writing a Linux daemon in C#

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • 2
    `async Main` is a C# 7.1 language feature. If you're using previous versions, you can use `static void Main` with `ConsoleUtil.Wait()` or `ConsoleUtil.WaitForShutdown()`. – rianjs Oct 29 '17 at 20:04
  • @rianjs that is correct. Thats why I included it in the util – Nkosi Oct 29 '17 at 20:14
  • Yeah, your second code snippet (`public static async Task Main()`) is a little unusual, so I was specifically calling it out as the more common alternative. Your answer is actually how I discovered that there was a C# 7.1(!). – rianjs Oct 30 '17 at 01:01
4

Implementing Linux Daemon or service for windows quite easy with single codebase using Visual Studio 2019. Just create project using WorkerService template. In my case I have Coraval library to schedule the tasks.

Program.cs class

public class Program
{
    public static void Main(string[] args)
    {
        Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Debug()
                .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
                .Enrich.FromLogContext()
                .WriteTo.File(@"C:\temp\Workerservice\logfile.txt").CreateLogger();

        IHost host = CreateHostBuilder(args).Build();

        host.Services.UseScheduler(scheduler =>
        {

            scheduler
              .Schedule<ReprocessInvocable>()
              .EveryThirtySeconds();
        });
        host.Run();
    }
    public static IHostBuilder CreateHostBuilder(string[] args) =>
          Host.CreateDefaultBuilder(args).UseSystemd() //.UseWindowsService()

        .ConfigureServices(services =>
        {
            services.AddScheduler();
            services.AddTransient<ReprocessInvocable>();
        });
}

ReprocessInvocable.cs class

public class ReprocessInvocable : IInvocable
{
    private readonly ILogger<ReprocessInvocable> _logger;
    public ReprocessInvocable(ILogger<ReprocessInvocable> logger)
    {
        _logger = logger;
    }
    public async Task Invoke()
    {
        //your code goes here
        _logger.LogInformation("Information - Worker running at: {time}", DateTimeOffset.Now);
        _logger.LogWarning("Warning - Worker running at: {time}", DateTimeOffset.Now);
        _logger.LogCritical("Critical - Worker running at: {time}", DateTimeOffset.Now);
        Log.Information("Invoke has called at: {time}", DateTimeOffset.Now);
    }
}

For linux daemon use UseSystemd and for windows service use UseWindowsService as per the above code.

R15
  • 13,982
  • 14
  • 97
  • 173
3

The best I could come up with is based on the answer to two other questions: Killing gracefully a .NET Core daemon running on Linux and Is it possible to await an event instead of another async method?

using System;
using System.Runtime.Loader;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    public class Program
    {
        private static TaskCompletionSource<object> taskToWait;

        public static void Main(string[] args)
        {
            taskToWait = new TaskCompletionSource<object>();

            AssemblyLoadContext.Default.Unloading += SigTermEventHandler;
            Console.CancelKeyPress += new ConsoleCancelEventHandler(CancelHandler);

            //eventSource.Subscribe(eventSink) or something...

            taskToWait.Task.Wait();

            AssemblyLoadContext.Default.Unloading -= SigTermEventHandler;
            Console.CancelKeyPress -= new ConsoleCancelEventHandler(CancelHandler);

        }


        private static void SigTermEventHandler(AssemblyLoadContext obj)
        {
            System.Console.WriteLine("Unloading...");
            taskToWait.TrySetResult(null);
        }

        private static void CancelHandler(object sender, ConsoleCancelEventArgs e)
        {
            System.Console.WriteLine("Exiting...");
            taskToWait.TrySetResult(null);
        }

    }
}
Steve Clanton
  • 4,064
  • 3
  • 32
  • 38
2

If you're trying to find something more robust, I found an implementation on Github that looks promising: .NET Core Application blocks for message-based communication. It uses Host, HostBuilder, ApplicationServices, ApplicationEnvironment, etc classes to implement a messaging service.

It doesn't quite look ready for black box reuse, but it seems like it could be a good starting point.

var host = new HostBuilder()
            .ConfigureServices(services =>
            {
                var settings = new RabbitMQSettings { ServerName = "192.168.80.129", UserName = "admin", Password = "Pass@word1" };
           })
            .Build();

Console.WriteLine("Starting...");
await host.StartAsync();

var messenger = host.Services.GetRequiredService<IRabbitMQMessenger>();

Console.WriteLine("Running. Type text and press ENTER to send a message.");

Console.CancelKeyPress += async (sender, e) =>
{
    Console.WriteLine("Shutting down...");
    await host.StopAsync(new CancellationTokenSource(3000).Token);
    Environment.Exit(0);
};
...
Steve Clanton
  • 4,064
  • 3
  • 32
  • 38
1

Have you tried Thread.Sleep (Timeout.Infinite) ?

using System;
using System.IO;
using System.Threading;

namespace Daemon {
    class Program {
        static int Main(string[] args) {
            if (Environment.OSVersion.Platform == PlatformID.Win32NT) {
                Log.Critical("Windows is not supported!");
                return 1;
            }
            Agent.Init();
            Agent.Start();
            if (Agent.Settings.DaemonMode || args.FirstOrDefault() == "daemon") {
                Log.Info("Daemon started.");
                Thread.Sleep(Timeout.Infinite);
            }
            Agent.Stop();
        }
    }
}
MarineHero
  • 143
  • 2
  • 5
0

I'm not sure it is production grade, but for a quick and dirty console app this works well:

await Task.Delay(-1); //-1 indicates infinite timeout
Tom Makin
  • 3,203
  • 23
  • 23