Clean stop of Windows Service in .NET

Stop Windows Service in the proper manner in C#

  • Share

The common reason I wrote Windows Services applications for are usually workers. For example, you need to read messages from the queue and process them. Services are ideal for this because you do not need any UI for this.

For this purpose, you may want to add more Tasks that will open multiple connections your queue server and process messages piled up in the queue faster. It is easy to achieve this on service start, but the issue begins once you want to stop the service.

All Tasks created in .NET application will run on a separate background thread. This means, once the main thread is stopped, background threads will be terminated as well. In case you have foreground threads running, in MMC (Microsoft Management Console) when you try to stop service it will show you that service is stopped, but once you got to task manager, you will see that service process is still running.

Note

All sample code snippets are part of a public project repository which is hosted on Github https://github.com/dejanstojanovic/WindowsService.CleanStop

Using Tasks solves this, but produces another problem. In case your Tasks are in the middle of processing the message, when main thread is terminated, all Task threads will be terminated as well at the same time, so might end up by loosing your messages which were not completely processed when the Thread was terminated. There are ways to prevent this depending on the queue platform and approaches, but it is topic for some other subject.

So to make sure all picked messages are processed, we need to wait for all worker Tasks to finish their processing and then we stop the main thread of the service and rvice itself.

So let's start with the code to make things more clear how this approach will work.

Since I really hate to hardcode value, first I am going to configure few App settings keys in the app.config. We are going to need to set number of worker Tasks our service will have and maximum time we are going to allow worker Tasks to finish their processing. We cannot wait htem forever :)

  <appSettings>
    <add key="service.workersnumber" value="2" />
    <add key="service.stoptimeout" value="6000" />
  </appSettings>
    

Since Windows Services do not have any UI, and they have to be registered in Windows in order to be visible in MMC, it is so straight forward to debug them. In article Debugging Windows Service application with console in C# I explained one of the way for debugging the service from Visual Studio, so I will not mention it here, pluse we are going to debug this directly with the real service installation involving MMC as well to stop the service and have the real world working service.

Because we are going to do the test in MMC we are going to have some log, so I decided to use log4net. It comes as a NuGet package so it is easy to involve it in a project. You can either use Visual Studio Package Manager or NuGet Console, or just add a NuGet reference t packages.config and package will be restored to project on the first build.

<package id="log4net" version="2.0.8" targetFramework="net462" />
    

As you can see I am using .NET Framework 4.6.2. It comes with Visual Studio 2017, but if you do not have it installed you can install if from the URL in references section or install the NuGet for the framework version you are using.

Stop signal from MMC will be nadled by CancellationToken. Once cancellation token becomes canceled, we will tell all tasks not to continue processing after they are done.

Now the waiting for each ad every worker Task we will base on the ManualResetEvent class instances. Basically each Task will have it's own ManualResetEvent class instance and once the task processing is done this ManualResetEvent will be set to true. First let's declare and initialize all of these in the constructor.

        private readonly ILog log;
        private readonly CancellationTokenSource cancellationTokenSource;
        private readonly CancellationToken cancellationToken;
        private readonly List<ManualResetEvent> resetEvents;

        public Service()
        {

            this.log = log4net.LogManager.GetLogger(this.GetType().FullName);

            this.cancellationTokenSource = new CancellationTokenSource();
            this.cancellationToken = cancellationTokenSource.Token;
            this.resetEvents = new List<ManualResetEvent>();

            InitializeComponent();
        }
    

Variable resetEvents will hold all the ManualResetEvent instances of each Task and on stop we will tell main thread to wait for all of them to become true, but let's first see how we are going to start all the tasks on service start. Application settings key service.stoptimeout in app.config is holding the value of the milliseconds main thread will wait for all Tasks ManualResetEvents to be set after which it will terminate main thread of the service process.

        protected override void OnStart(string[] args)
        {
            this.StartService();
        }

        public void StartService()
        {
            log.Info("Service start invoked...");
            if (!int.TryParse(ConfigurationManager.AppSettings["service.workersnumber"], out int workersNumber))
            {
                workersNumber = 1;
            }

            log.Info(%%%~COMPRESS~PRE~3~%%%quot;Workers count {workersNumber}");

            for (int i = 0; i < workersNumber; i++)
            {
                var resetEvent = new ManualResetEvent(false);
                resetEvents.Add(resetEvent);
                StartWorker(i,resetEvent);
            }

            log.Info("Service start finished.");
        }

        public void StartWorker(int workerNumber,ManualResetEvent resetEvent)
        {
            Task.Run(() =>
            {
                log.Info(%%%~COMPRESS~PRE~3~%%%quot;Starting worker {workerNumber}...");

                while (!cancellationToken.IsCancellationRequested)
                {
                    //Logic here

                    /* Simulation code start */
                    Thread.Sleep(5000);
                    log.Info(%%%~COMPRESS~PRE~3~%%%quot;Worker {workerNumber} done work ");
                    /* Simulation code end */

                    Thread.Sleep(100); //Avoid high CPU
                }

                resetEvent.Set();
                log.Info(%%%~COMPRESS~PRE~3~%%%quot;Stopping worker {workerNumber}...");
            }, this.cancellationToken);
        }
    

For demo purpose we will simulate 5 seconds processing long time with Thread.Sleep(5000);. In real world this will be a long running processing of the Task.

Cancellation token will be responsible to tell the tasks to stop processing and once they are done, each Task will set it's ManualResetEvent. Now on service stop we will wait for all manual reset events set.

        protected override void OnStop()
        {
            log.Info("Stopping service...");
            if (!int.TryParse(ConfigurationManager.AppSettings["service.stoptimeout"], out int serviceStopTimeout))
            {
                serviceStopTimeout = 3000;
            }
            this.cancellationTokenSource.Cancel();
            WaitHandle.WaitAll(resetEvents.Select(e => e as WaitHandle).ToArray(), serviceStopTimeout); //Wait for all workers to exit
            log.Info("Service stopped.");
        }
    

Testing this from console will not simulate the real situation, so that is why we are going to install the service and do a debug through logs to make sure all the Tasks are finished processing before the service actually stopped. Fr that purpose we have all the log messages inside the methods.

So first we need to install the service on our machine pointing to Debug bin folder of the project, but before we install the service we need to add ServiceInstaller class which will hold the info of the service so we do not have to pass them with installutil.

    [RunInstaller(true)]
    public class ServiceInstaller : Installer
    {
        public ServiceInstaller()
        {

            var processInstaller = new System.ServiceProcess.ServiceProcessInstaller();
            var serviceInstaller = new System.ServiceProcess.ServiceInstaller();
            var assemblyDescription = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false).OfType<AssemblyDescriptionAttribute>().FirstOrDefault();

            processInstaller.Account = ServiceAccount.LocalSystem;
            serviceInstaller.DisplayName = typeof(Service).FullName;
            serviceInstaller.StartType = ServiceStartMode.Manual;
            serviceInstaller.ServiceName = typeof(Service).FullName;
            serviceInstaller.Description = assemblyDescription!=null ? assemblyDescription.Description : null;

            this.Installers.Add(processInstaller);
            this.Installers.Add(serviceInstaller);
        }
    }
    
Note

This ServiceInstaller class is generic, so you can use it with any project. The service name and descriptions are picked up from the assembly via reflection, so no additional coding is required

Now that we have all ready, we can install the service and start testing it.

"C:\Windows\Microsoft.NET\Framework\v4.0.30319\installutil.exe" "C:\Github\WindowsService.CleanStop\src\bin\Debug\WindowsService.CleanStop.exe"
    

Since service is set to start manually it will not be started after installing. Now ween need to start and stop it an check logs will be in the same place as executable of the service which is in /bin/Debug folder.

 2017-09-2110:17:31,529 [4] INFO WindowsService.CleanStop.Service [(null)] - Service start invoked... 2017-09-21 10:17:31,549 [4] INFO WindowsService.CleanStop.Service [(null)] - Workers count 2 2017-09-21 10:17:31,549 [4] INFO WindowsService.CleanStop.Service [(null)] - Service start finished. 2017-09-21 10:17:31,549 [5] INFO WindowsService.CleanStop.Service [(null)] - Starting worker 0... 2017-09-21 10:17:31,549 [6] INFO WindowsService.CleanStop.Service [(null)] - Starting worker 1... 2017-09-21 10:17:36,560 [5] INFO WindowsService.CleanStop.Service [(null)] - Worker 0 done work 2017-09-21 10:17:36,560 [6] INFO WindowsService.CleanStop.Service [(null)] - Worker 1 done work 2017-09-21 10:17:41,678 [6] INFO WindowsService.CleanStop.Service [(null)] - Worker 1 done work 2017-09-21 10:17:41,678 [5] INFO WindowsService.CleanStop.Service [(null)] - Worker 0 done work 2017-09-21 10:17:46,794 [6] INFO WindowsService.CleanStop.Service [(null)] - Worker 1 done work 2017-09-21 10:17:46,794 [5] INFO WindowsService.CleanStop.Service [(null)] - Worker 0 done work 2017-09-21 10:17:51,913 [6] INFO WindowsService.CleanStop.Service [(null)] - Worker 1 done work 2017-09-21 10:17:51,913 [5] INFO WindowsService.CleanStop.Service [(null)] - Worker 0 done work 2017-09-21 10:17:54,998 [7] INFO WindowsService.CleanStop.Service [(null)] - Stopping service... 2017-09-21 10:17:57,034 [5] INFO WindowsService.CleanStop.Service [(null)] - Worker 0 done work 2017-09-21 10:17:57,034 [6] INFO WindowsService.CleanStop.Service [(null)] - Worker 1 done work 2017-09-21 10:17:57,149 [6] INFO WindowsService.CleanStop.Service [(null)] - Stopping worker 1... 2017-09-21 10:17:57,149 [7] INFO WindowsService.CleanStop.Service [(null)] - Service stopped. 2017-09-21 10:17:57,149 [5] INFO WindowsService.CleanStop.Service [(null)] - Stopping worker 0... 

If you check logs these sample logs, you will see that worker Task always logged "Worker done work" each 5 seconds even after "Stopping service" message is logged which means service stop waited for worker task to finish.

Once we did the test with this demo service we should remove it from our machine with same installutil tool.

"C:\Windows\Microsoft.NET\Framework\v4.0.30319\installutil.exe" /u "C:\Github\WindowsService.CleanStop\src\bin\Debug\WindowsService.CleanStop.exe"
    

References

  • Share

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

comments powered by Disqus