Clean service stop by handling SIGTERM on Linux with .NET Core 2.1

Application stop with SIGTERM handling in .NET Core 2.1 on Linux using generic host

Some time ago I wrote about setting up .NET Core service/daemon on Linux. Back than I was using .NET Core 2.0 SDK on both development Windows machine and Linux host machine. In .NET Core 2.0, there was no support for handling SIGTERM sent from Linux host for gradually stopping your application. Instead you could only rely on AppDomain unload to do the clean up before you close your process.

New release of .NET Core 2.1 has all this already supporting out of the box with .NET Generic Host. In previous version of .NET Core this was only the case for ASP.NET Core applications which were using WebHost class for running the ASP.NET application. This has been changed in new release and now you can use generic host as an ideal approach for long running tasks as services/daemons.

Note

Before we start with the code make sure that you have installed Visual Studio 2017 version 15.7 or later which you can download from https://www.visualstudio.com/downloads/ if you do not have installed and .NET Core SDK for Visual Studio 2017 from https://www.microsoft.com/net/download/visual-studio-sdks

So let's start with the code to make things more clear. To be able to handle SIGTERM sent from the Linux host environment you first need to write your implementation of IHostedService interface. But do not forget to add all the nuget packages to your project first :)

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <LangVersion>latest</LangVersion>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.1.0" />
    <PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" />
    <PackageReference Include="Serilog.Extensions.Logging.File" Version="1.1.0" />
    <PackageReference Include="Serilog.Settings.Configuration" Version="2.6.1" />
  </ItemGroup>

  <ItemGroup>
    <None Update="appsettings.development.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>

    

Now when we have all nicely set up in our project we can start implementation of our service host which is implementation of IHostedService interface. An instance of IApplicationLifetime implementation is injected on constructor our class which implements IHostedService and provides us with methods to handle application startup and shutdown events.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;

namespace Core.Service.Sample
{
    public class ApplicationLifetimeHostedService : IHostedService
    {
        IApplicationLifetime appLifetime;
        ILogger<ApplicationLifetimeHostedService> logger;
        IHostingEnvironment environment;
        IConfiguration configuration;
        public ApplicationLifetimeHostedService(
            IConfiguration configuration,
            IHostingEnvironment environment,
            ILogger<ApplicationLifetimeHostedService> logger, 
            IApplicationLifetime appLifetime)
        {
            this.configuration = configuration;
            this.logger = logger;
            this.appLifetime = appLifetime;
            this.environment = environment;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            this.logger.LogInformation("StartAsync method called.");

            this.appLifetime.ApplicationStarted.Register(OnStarted);
            this.appLifetime.ApplicationStopping.Register(OnStopping);
            this.appLifetime.ApplicationStopped.Register(OnStopped);

            return Task.CompletedTask;

        }

        private void OnStarted()
        {
            this.logger.LogInformation("OnStarted method called.");

            // Post-startup code goes here
        }

        private void OnStopping()
        {
            this.logger.LogInformation("OnStopping method called.");

            // On-stopping code goes here
        }

        private void OnStopped()
        {
            this.logger.LogInformation("OnStopped method called.");

            // Post-stopped code goes here
        }


        public Task StopAsync(CancellationToken cancellationToken)
        {
            this.logger.LogInformation("StopAsync method called.");

            return Task.CompletedTask;
        }
    }
}

    

Since this is just a sample application I will only log method calls to file using Serilog which is setup in Program.cs entry point class of the application.

using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;

namespace Core.Service.Sample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            IHost host = new HostBuilder()
                 .ConfigureHostConfiguration(configHost =>
                 {
                     configHost.SetBasePath(Directory.GetCurrentDirectory());
                     configHost.AddEnvironmentVariables(prefix: "ASPNETCORE_");
                     configHost.AddCommandLine(args);
                 })
                 .ConfigureAppConfiguration((hostContext, configApp) =>
                 {
                     configApp.SetBasePath(Directory.GetCurrentDirectory());
                     configApp.AddEnvironmentVariables(prefix: "ASPNETCORE_");
                     configApp.AddJsonFile($"appsettings.json", true);
                     configApp.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", true);
                     configApp.AddCommandLine(args);
                 })
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddLogging();
                    services.AddHostedService<ApplicationLifetimeHostedService>();
                })
                .ConfigureLogging((hostContext, configLogging) =>
                {
                    configLogging.AddSerilog(new LoggerConfiguration()
                              .ReadFrom.Configuration(hostContext.Configuration)
                              .CreateLogger());
                    configLogging.AddConsole();
                    configLogging.AddDebug();
                })
                .Build();

            await host.RunAsync();
        }


    }
}

    

And to have our logs nicely organized we need to see them up in appsettings.json, but because we want our application to have a concept of environment we will create appsetting.Development.json as well for our development environment. 

{
  "Logging": {
    "PathFormat": "Logs/Core.Service.Sample.{Date}.log",
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "Serilog": {
    "MinimumLevel": "Debug",
    "WriteTo": [
      {
        "Name": "RollingFile",
        "Args": {
          "logDirectory": ".\\Logs",
          "fileSizeLimitBytes": 1024,
          "pathFormat": "Logs/Core.Service.Sample.{Date}.log",
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"
        }
      }
    ]
  }
}

    

Now let's run our console app from Visual Studio and exit it by pressing CTRL+C. There should be Logs folder generated in \bin\Debug\netcoreapp2.1\Logs folder of the project on your machine. You will see that we logged all events to .log file.

2018-06-15 12:35:36.007 +04:00 [Debug] Hosting starting
2018-06-15 12:35:36.064 +04:00 [Information] StartAsync method called.
2018-06-15 12:35:36.091 +04:00 [Information] OnStarted method called.
2018-06-15 12:35:36.094 +04:00 [Debug] Hosting started
2018-06-15 12:35:37.757 +04:00 [Information] OnStopping method called.
2018-06-15 12:35:37.806 +04:00 [Debug] Hosting stopping
2018-06-15 12:35:37.809 +04:00 [Information] StopAsync method called.
2018-06-15 12:35:37.811 +04:00 [Information] OnStopped method called.
2018-06-15 12:35:37.815 +04:00 [Debug] Hosting stopped

All looks good on our development Windows machine, but let's spin this application as a service on a Linux host machine. I used Debian VM for the test, but you can choose any other distro of your choice, you just might have to alter a bit following command to setup the machine. I explained in details, step by step how to setup the service on Debian Linux in article How to setup .NET Core 2 on Debian or Ubuntu Linux distro the easy way, so I will just briefly go through the setup with all commands without much explanation of each since you can already find it in the mentioned article.

First we'll setup.NET Core 2.1 SDK on our Linux host. You can download binaries from your desktop from https://www.microsoft.com/net/download/thank-you/dotnet-sdk-2.1.300-linux-x64-binaries and upload the .tr.gz archive to a Linux machine or you can download it on your Linux host from bash CLI.

cd $HOME
wget https://download.microsoft.com/download/8/8/5/88544F33-836A-49A5-8B67-451C24709A8F/dotnet-sdk-2.1.300-linux-x64.tar.gz
mkdir /bin/dotnet
tar zxf dotnet-sdk-2.1.300-linux-x64.tar.gz -C /bin/dotnet
chmod  x /bin/dotnet
apt-get install -y libunwind-dev
apt-get install libunwind8 icu-devtools

/bin/dotnet/dotnet --version
    

After these commands executed you should get 2.1.300 as an output message meaning that .NET Core 2.1 SDK is successfully installed and you can now run .NET Core 2.1 applications on this machine.

Next step is setting up support for our service and an account under which service will run.

apt-get install -y systemd
useradd -m dotnetuser -p dotnetpass
    

Now we can publish our application from Visual Studio to a local folder on a dev machine. I am using Windows for development, so to make it available on Linux machine I used FTP to upload it there in my home folder. Using your home folder for applications especially services is not a good idea, so back to our Linux console and let's copy our published application folder to /etc on Linux host.

cd $HOME
cp ./Core.Service.Sample -r /etc/Core.Service.Sample
chmod +x /etc/Core.Service.Sample
    

We need to configure our service for systemd. Navigate to first to systemd configurations folder

cd /etc/systemd/system
    

Start nano, paste the following code and save as dotnetcore-sample-generichost.service

[Unit]
Description=dotnetcore 2.1 sample service with generic host

[Service]
ExecStart=/bin/dotnet/dotnet Core.Service.Sample.dll
WorkingDirectory=/etc/Core.Service.Sample/
User=dotnetuser
Group=dotnetuser
Restart=on-failure
SyslogIdentifier=dotnetcore-sample-generichost-service
PrivateTemp=true

[Install]
WantedBy=multi-user.target
    

We are ready to register our service now and start it

systemctl daemon-reload
systemctl enable dotnetcore-sample-generichost.service
systemctl start dotnetcore-sample-generichost.service
systemctl status dotnetcore-sample-generichost.service
    

We should get the following message confirming that our service is running properly

Core 21 Svc

Let's stop the service and check our logs to see if all events are handled property, same as on our development machine from Visual Studio

systemctl stop dotnetcore-sample-generichost.service
    
Note

You might have to manually create Logs folder in /etc/Core.Sample.Service and set write permission to it with chmod 777

Our log file should have all events captured and loged as following file I took from my Linux VM where I run a test

2018-06-15 21:02:41.656 +04:00 [Debug] Hosting starting
2018-06-15 21:02:41.690 +04:00 [Information] StartAsync method called.
2018-06-15 21:02:41.702 +04:00 [Information] OnStarted method called.
2018-06-15 21:02:41.705 +04:00 [Debug] Hosting started
2018-06-15 21:03:17.516 +04:00 [Information] OnStopping method called.
2018-06-15 21:03:17.517 +04:00 [Debug] Hosting stopping
2018-06-15 21:03:17.518 +04:00 [Information] StopAsync method called.
2018-06-15 21:03:17.518 +04:00 [Information] OnStopped method called.
2018-06-15 21:03:17.519 +04:00 [Debug] Hosting stopped

Finally we write Windows like services on Linux host and handle stop and start with .NET Core 2.1. This is really important if we need to explicitly close connection on service stop and perform a clean stop.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article