swirl
Home Software Blog Wallpapers Webtools
Writing a Linux service in DotNetCore
Tuesday 12, September 2023   |   Post link

Overview

This blogpost discusses how to create a Linux service using DotNet Core (6). It also discusses how to implement a clean shutdown routine.

Creating the application skeleton

The application boilerplate code can be quickly generated using Visual Studio. Just select to create a new "Worker Service" (the last one in the screenshot).

New VisualStudio Worker project

You can do the same using just the dotnet CLI:

dotnet new worker

The files that are of interest are:

  • Program.cs
  • Worker.cs

Program.cs creates the host and registers the Worker class. The Worker class is where you start to write your logic. The generated code in Program.cs is:

IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
    services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();

We need to add a reference to Microsoft.Extensions.Hosting.Systemd package in order to interact with the Service manager of Linux. The short and somewhat useless documentation by Microsoft says "Sets the host lifetime to SystemdLifetime, provides notification messages for application started and stopping, and configures console logging to the systemd format.". Hopefully that explains everything about its use. The sample application uses version 6.0.0 because trying to use 7.0.0.0 resulted in compilation errors. You can add the package using:

dotnet add package Microsoft.Extensions.Hosting.Systemd --version 6.0.0

You need to make one code change after the package has been added, its at the time of creating the host:

IHost host = Host.CreateDefaultBuilder(args)
	.ConfigureServices(services =>
	{
		services.AddHostedService<Worker>();
	})
	.UseSystemd() // added to work with systemd
	.Build();

Note: the sample code defines the Main method and adds another method to create the HostBuilder instead of using the generated code as-is.

Deploying the service

We need to abide by a couple of rules to deploy a service managed by 'Systemd'. What's systemd you ask? Systemd is the service manager used by Linux today just like we have the SCM (Service Control Manager) in Windows.

The first step is to create a service file, the service file defines the name of the service, its dependencies and its startup & shutdown behavior among other things..

Here is a basic service file which can be used for our service. The name of the file defines the name of the service. The file should be named as "servicename.service".

[Unit]
Description=Sample Linux Service

[Service]
Type=notify
WorkingDirectory=/usr/sbin/SampleLinuxService/
ExecStart=/usr/bin/dotnet /usr/sbin/SampleLinuxService/SampleLinuxService.dll
Environment=DOTNET_ROOT=/usr/lib64/dotnet
User=ec2-user
SyslogIdentifier=SampleLinuxService
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

The important lines here are:

Property Description
WorkingDirectory Setting this is specially important if we are not using a single-contained application. The sample discussed in the blogpost is published as a framework-dependent application
ExecStart Defines the command to run to start the service. We are using the dotnet command to run our application stored in folder '/usr/sbin/SampleLinuxService/'
User Specifies the Linux user used to run the service.
Environment We can setup various environment variables, this is not needed if the user used to start the service already had the correct environment variables defined in the profile.

You can read about the other properties here. The service file needs to be stored at /etc/systemd/system/. Ensure that all the application files are accessible by the user mentioned in the service file. You can change the ownership to the correct user with the following command:

sudo chown ec2-user /usr/sbin/SampleLinuxService/*

Finally, we need to tell systemd about our service by issuing the following command:

sudo systemctl daemon-reload

Interacting with the service

Now that the service is registered, you can start interacting with the service. What kind of interaction you ask? First let's query its status:

$ sudo systemctl status SampleLinuxService
SampleLinuxService.service - Sample Linux Service
   Loaded: loaded (/etc/systemd/system/SampleLinuxService.service; disabled; vendor preset: disabled)
   Active: inactive (dead)

To start the service use:

$ sudo systemctl start SampleLinuxService

To stop the service use:

sudo systemctl stop SampleLinuxService

To ensure service auto-starts on machine reboots:

sudo systemctl enable SampleLinuxService

To view the service logs use:

sudo journalctl -u SampleLinuxService

To read the last (latest) 10 log lines use:

sudo journalctl -u SampleLinuxService -n 10

One interesting thing to notice is that logs written to by service (to the console in our example) are available using the journalctl command.

The matter graceful shutdown

Services usually perform important tasks like reading data, storing stuff here, writing a record there and many more. We obviously don't want to abandon these tasks half-way and would like to stop the service cleanly.

The default code does not allow for clean shutdown, in fact there is no shutdown handler which can be used to stop exisiting execution, wait for their completion and then continue the service shutdown.

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

We however can make use of the StopAsync virtual method of the BackgroundService, override it and implement a clean-shutdown mechanism. The Worker.cs class has code which does exactly this but not in a very clean way. The idea here is:

  • We have two boolean variables _stop & _stopped both initialized to false.
  • The main loop in the ExecuteAsync method monitors the value of _stop, if _stop is true, it starts its shutdown routine - a Thread.Sleep(5000) for now.
  • The ExecuteAsync method sets _stopped to true once it has completed all its shutdown activities.
  • The StopAsync method is the one that is called by the framework when a service stop command is requested.
  • The StopAsync method then sets the _stop to true and waits until the _stopped variable is true.

The code for the two methods are below:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{			
    bool stop = false;
    while (!stop)
    {
        _logger.LogInformation("ThreadID: {0} Worker running at: {1}", Thread.CurrentThread.ManagedThreadId, DateTimeOffset.Now);
        await Task.Delay(1000, stoppingToken);
        _logger.LogInformation("I woke up");
        if (stoppingToken.IsCancellationRequested)
        {
            _logger.LogCritical("Cancellation is requested.");
            Console.WriteLine("Waiting for {0}ms before stopping.", StopIntervalMS);
            _logger.LogCritical("Waiting for {0}ms before stopping.", StopIntervalMS);
            Thread.Sleep(StopIntervalMS);
            stop = true;
        }
        
        if (_stop)
        {
            _logger.LogCritical("ThreadID: {0}, Stop signal has been set, cleaning up...", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(5000);
            _logger.LogCritical("ThreadID: {0}, Finished cleaning up", Thread.CurrentThread.ManagedThreadId);
            stop = true;
        }
    }
    _stopped = true;
}

public override async Task StopAsync(CancellationToken cancellationToken)
{			
    _logger.LogCritical("ThreadID: {0}, In StopAsync(), Setting stop signal", Thread.CurrentThread.ManagedThreadId);
    _stop = true;
    while(_stopped != true)
    {
        _logger.LogCritical("ThreadID: {0}, In StopAsync(), waiting for task to cleanup...", Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(1000);				
    }
    _logger.LogCritical("ThreadID: {0}, In StopAsync(), cleapup complete detected", Thread.CurrentThread.ManagedThreadId);
    await base.StopAsync(cancellationToken);
    return;
}

The code illustrates how a clean-shutdown mechanism can be implemented. A cleaner implementation is attempted using the GracefulShutdownWorker class. This class encapsulates the implementation for the StopAsync method. The MyWorker class inherits GracefulShutdownWorker and queries the RequireStop property to know if it has to start its shutdown routine and finally sets the SafeToShutdown property to true which results in the service shutting down.

namespace SampleLinuxService
{
    public abstract class GracefulShutdownWorker : BackgroundService
    {
        protected readonly ILogger<GracefulShutdownWorker> _logger;
        protected volatile bool _stop = false;
        private volatile bool _stopped = false;

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

        public override async Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogCritical("Setting stop signal");
            _stop = true;
            while (_stopped != true)
            {
                _logger.LogCritical("Waiting for cleanup to complete");
                Thread.Sleep(_cleanupIntervalMS);				
            }
            _logger.LogCritical("Cleapup complete detected");
            await base.StopAsync(cancellationToken);
            return;
        }

        protected bool RequireStop
        {
            get { return _stop; }
        }

        protected bool SafeToShutdown
        {
            get { return _stopped; }
            set { _stopped = value; }
        }

        protected int _cleanupIntervalMS = 1000;
    }
}    

The MyWorker class which derives from the GracefulShutdownWorker class.

namespace SampleLinuxService
{
    public class MyWorker : GracefulShutdownWorker
    {
        public MyWorker(ILogger<MyWorker> logger) : base(logger)
        {		
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {			
            bool stop = false;
            while (!stop)
            {
                _logger.LogInformation("ThreadID: {0} I am working at {1}", Thread.CurrentThread.ManagedThreadId, DateTimeOffset.Now);
                await Task.Delay(1000, stoppingToken);
                
                if (RequireStop)
                {
                    _logger.LogCritical("ThreadID: {0}, Stop signal has been set, cleaning up...", Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(5000);
                    _logger.LogCritical("ThreadID: {0}, Finished cleaning up", Thread.CurrentThread.ManagedThreadId);
                    stop = true;
                }
            }
            SafeToShutdown = true;
        }

        public static int StopIntervalMS = 1000;
    }
}

Source code

The source code for the blogpost is available at Github.

Conclusion

DotNetCore makes it quite easy to write Linux services. Using systemd package maps the logging levels of the DotNetCore logger to systemd logging and the logs showup as part of the service logs. We need to write a bit of extra code to handle clean service shutdowns but its always a good idea to do so.




Comments

Posts By Year

2024 (4)
2023 (5)
2022 (10)
2021 (5)
2020 (12)
2019 (6)
2018 (8)
2017 (11)
2016 (6)
2015 (17)
2014 (2)
2013 (4)
2012 (2)

Posts By Category

.NET (4)
.NET Core (2)
ASP.NET MVC (4)
AWS (5)
AWS API Gateway (1)
Android (1)
Apache Camel (1)
Architecture (1)
Audio (1)
Azure (2)
Book review (3)
Business (1)
C# (3)
C++ (2)
CloudHSM (1)
Containers (4)
Corporate culture (1)
Database (3)
Database migration (1)
Desktop (1)
Docker (1)
DotNet (3)
DotNet Core (2)
ElasticSearch (1)
Entity Framework (3)
Git (3)
IIS (1)
JDBC (1)
Java (10)
Kibana (1)
Kubernetes (1)
Lambda (1)
Learning (1)
Life (7)
Linux (2)
Lucene (1)
Multi-threading (1)
Music (1)
OData (1)
Office (1)
PHP (1)
Photography (1)
PowerShell (2)
Programming (28)
Python (1)
Rants (5)
SQL (2)
SQL Server (1)
Security (3)
Software (1)
Software Engineering (1)
Software development (2)
Solr (1)
Sql Server (2)
Storage (1)
T-SQL (1)
TDD (1)
TSQL (5)
Tablet (1)
Technology (1)
Test Driven (1)
Testing (1)
Tomcat (1)
Unit Testing (1)
Unit Tests (1)
Utilities (3)
VC++ (1)
VMWare (1)
VSCode (1)
Visual Studio (2)
Wallpapers (1)
Web API (2)
Win32 (1)
Windows (9)
XML (2)

Posts By Tags

.NET(6) API Gateway(1) ASP.NET(4) AWS(3) Adults(1) Advertising(1) Android(1) Anti-forgery(1) Asynch(1) Authentication(2) Azure(2) Backup(1) Beliefs(1) BlockingQueue(1) Book review(2) Books(1) Busy(1) C#(4) C++(3) CLR(1) CORS(1) CSRF(1) CTE(1) Callbacks(1) Camel(1) Certificates(1) Checkbox(1) Client authentication(1) CloudHSM(1) Cmdlet(1) Company culture(1) Complexity(1) Consumer(1) Consumerism(1) Containers(3) Core(2) Custom(2) DPI(1) Data-time(1) Database(4) Debugging(1) Delegates(1) Developer(2) Dockers(2) DotNetCore(3) EF 1.0(1) Earphones(1) Elastic Search(2) ElasticSearch(1) Encrypted(1) Entity framework(1) Events(1) File copy(1) File history(1) Font(1) Git(2) HierarchyID(1) Hyper-V(1) IIS(1) Installing(1) Intelli J(1) JDBC(1) JSON(1) JUnit(1) JWT(1) Java(3) JavaScript(1) Kubernetes(1) Life(1) LinkedIn(1) Linux(2) Localization(1) Log4J(1) Log4J2(1) Logging(1) Lucene(1) MVC(4) Management(2) Migration history(1) Mirror(1) Mobile Apps(1) Modern Life(1) Money(1) Music(1) NGINX(1) NTFS(1) NUnit(2) OData(1) OPENXML(1) Objects(1) Office(1) OpenCover(1) Organization(1) PHP(1) Paths(1) PowerShell(2) Processes(1) Producer(1) Programming(2) Python(2) QAAC(1) Quality(1) REDIS(2) REST(1) Runtimes(1) S3-Select(1) SD card(1) SLF4J(1) SQL(2) SQL Code-first Migration(1) SSH(2) SSL(1) Sattelite assemblies(1) School(1) Secrets Manager(1) Self reliance(1) Service(1) Shell(1) Solr(1) Sony VAIO(1) Spirituality(1) Spring(1) Sql Express(1) System Image(1) TDD(1) TSQL(3) Table variables(1) Tables(1) Tablet(1) Ubuntu(1) Url rewrite(1) VMWare(1) VSCode(1) Validation(2) VeraCode(1) Wallpaper(1) Wallpapers(1) Web Development(4) Windows(2) Windows 10(2) Windows 2016(2) Windows 8.1(1) Work culture(1) XML(1) Yii(1) iTunes(1) open file handles(1) renew(1) security(1) static ip address(1) ulimit(1)