Search code examples
c#.net-corewindows-servicesbackground-service

Prevent .NET BackgroundService running as Windows Service from being stopped


UPDATE: After creating this question and receiving some feedback, I then asked How does Topshelf prevent Windows Service from stopping which was more specific about what I was trying to do and I wanted to attract Topshelf experts. I didn't update this question because I didn't think it made sense to completely change this question's content since there were already views and responses nor add all the additional content to this question making it overwhelming/unfocused. Just noting that I did come up with a solution that was serving me well at the time of righting.

Original Question

Following the Create Windows Service using BackgroundService tutorial from Microsoft, I created a new BackgroundService in an attempt to migrate from .Net Framework/Topshelf.

Topshelf had a bool Stop() method that was called before stopping where a return of false prevented the service from shutting down. I need this functionality and was hoping to accomlish the same with BackgroundService with no success.

My service has some long running processes that run via files being dropped in monitored folders and could run for a very long time (1hr+). So, if a user (probably me) needs to update the Windows Service code they go and stop the service, update the files, and start the service. Since there is no progress displayed in Services MMC snap-in, I simply prevent the shutdown if something is running. My logic allows for a forced shutdown via X shutdown requests within a Y second sliding window, then I will allow the service to shutdown.

I've seen posts that demonstrate using IHostApplicationLifetime and registering a delegate on the CancellationToken ApplicationStopping but I don't want to 'pause' indefinitely, and wait for a job that could run for >1hr or could have a serious problem, so I wanted to 'reject' the request and using my sliding window logic.

The BackgroundService.StopAsync doesn't really offer much help as there is no return or status I can set to prevent the shutdown, the IHostApplicationLifetime.StopApplication has already been called by the time this overload is executed.

I tried subclassing WindowsServiceLifetime and pass in a ICancelableBackgroundService (my interface), but again the OnStop is too late in the workflow and not able to stop the service shutdown. The way the ServiceBase was written doesn't really seem to allow 'rejecting' a request.

[System.Runtime.Versioning.SupportedOSPlatform( "windows" )]
public class CancelableWindowsServiceLifetime : WindowsServiceLifetime
{
    private readonly ICancelableBackgroundService cancelableBackgroundService;

    public CancelableWindowsServiceLifetime( IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ICancelableBackgroundService cancelableBackgroundService, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor )
        : this( environment, applicationLifetime, cancelableBackgroundService, loggerFactory, optionsAccessor, Options.Create( new WindowsServiceLifetimeOptions() ) )
    {
    }

    public CancelableWindowsServiceLifetime( IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ICancelableBackgroundService cancelableBackgroundService, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, IOptions<WindowsServiceLifetimeOptions> windowsServiceOptionsAccessor )
        : base( environment, applicationLifetime, loggerFactory, optionsAccessor, windowsServiceOptionsAccessor )
    {
        this.cancelableBackgroundService = cancelableBackgroundService;
    }

    protected override void OnStop()
    {
        if ( !cancelableBackgroundService.CanStop() )
        {
            // What should we do here?  Neither exceptions or return stop host service from shutting down.
            // throw new InvalidOperationException( "CancelableBackgroundService is currently processing, cannot stop service." ); // This still does not prevent service from shutting down
            // return; // This still does not prevent service from shutting down.
        }

        base.OnStop();
    }
}

Are there any patterns/mechanisms to achieve the behavior I've described?


Solution

  • A sample code that works in .NET 6 or later (no conflict even if you have added the "Topshelf" nugget package to the project). It was tested using "Service Control Manager (SCM)"

    If "IHostApplicationLifetime.ApplicationStopping" is set, on "Graceful Shutdown" of service, Windows will wait for the operation defined in that event to complete.

    If the operation is long, for example, it takes 5 minutes, Windows will display an error message and, of course, in this code that we wrote, it will stop the service after the operation.

    ServiceStopError

    To test the code, do the following steps:

    1- Create a "Worker Service" type solution in "Visual Studio 2022".

    2- Add the nugget package "Microsoft.Extensions.Hosting.WindowsServices" with appropriate version to the .NET project. For example, if you use .NET 6, version 6 works.

    3- Replace the "Worker.cs" and "Program.cs" files with the following codes and build the project in "Debug" or "Release" mode.

    4- Run "cmd.exe" as administrator and then install service using command same as follows:

    sc Create TestService BinPath= "D:\Projects\TestService\bin\Debug\net6.0\TestService.exe"
    

    5- Start and then stop the service using SCM. Notice "C:\[Temp]\test.txt". After clicking the "Stop" menu in "SCM", about 40 seconds later, the service will stop, and during this time, if you delete "test.txt", the service will recreate it.

    Worker.cs:

        public class Worker : BackgroundService
        {
            private readonly ILogger<Worker> _logger;
    
            private readonly IHostApplicationLifetime _HostApplicationLifetime;
            public Worker(ILogger<Worker> logger, IHostApplicationLifetime HostApplicationLifetime)
            {
                _logger = logger;
                _HostApplicationLifetime = HostApplicationLifetime;
                _HostApplicationLifetime.ApplicationStopping.Register(() =>
                 {
                     while (!IsTerminatedJob && (ElapsedSecondsAfterStartingService < MaxDelayTimeForServiceTerminationInSeconds))
                         Task.Delay(1000).Wait();
                 });
            }
            public static bool IsTerminatedJob { get; set; } = false;
            public int ElapsedSecondsAfterStartingService = 0;
            public int MaxDelayTimeForServiceTerminationInSeconds = 0;
            public override Task StartAsync(CancellationToken cancellationToken)
            {
                IsTerminatedJob = false;
                ElapsedSecondsAfterStartingService = 0;
                MaxDelayTimeForServiceTerminationInSeconds = 40;
                return base.StartAsync(cancellationToken);
            }
            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    // To specify that the service can be terminated, set "IsTerminatedJob = true"
                    ElapsedSecondsAfterStartingService++;
                    WriteTestFile();
                    await Task.Delay(1000, stoppingToken);
                }
            }
            public void WriteTestFile()
            {
                CreateTextFile("test.txt", "service is running");
            }
            public void CreateTextFile(string FileName, string Message)
            {
                const string FILE_PATH = @"C:\[Temp]";
                if (!Directory.Exists(FILE_PATH))
                    Directory.CreateDirectory(FILE_PATH);
                File.WriteAllText(FILE_PATH + @"\" + FileName, Message);
                Console.Beep();
            }
    
        }
    

    Program.cs:

    using TestService;
    
    var builder = Host.CreateDefaultBuilder(args)
        .ConfigureServices(services => services.AddHostedService<Worker>())
        .UseWindowsService(options => options.ServiceName = "TestService")
        .Build();
    
    builder.Run();
    

    Other solution:

    There is another solution that is not so easy and interesting! If possible, replace the "StopAsync" method of the "Microsoft.Extensions.Hosting.Host" class with your own designed method at runtime. This may be done by "reflection" or any other allowed approach. To prevent the service from stopping, I think that in addition to the "StopAsync" method in the "BackgroundService" class, the "StopAsync" method in the "Host" class should also be overridden, which doesn't handle these things the way you want.