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.
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.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
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
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.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
- https://www.microsoft.com/net/download/visual-studio-sdks
- https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.1
- https://www.microsoft.com/net/download/thank-you/dotnet-sdk-2.1.300-linux-x64-binaries
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.
Comments for this article