How should a service exit to prevent an error?

Andy Reynolds 20 Reputation points
2023-01-30T23:11:11.52+00:00

I'm developing a Windows service in C#, .NET 6.0 following the instructions on https://learn.microsoft.com/en-us/dotnet/core/extensions/windows-service The service runs as expected, but when I stop the service, it logs an error

A task was canceled. Exception: System.Threading.Tasks.TaskCanceledException: A task was canceled. at AWLService.WindowsBackgroundService.ExecuteAsync(CancellationToken stoppingToken) in C:\Minerva\Projects\AWLService_Test\WindowsBackgroundService.cs:line 22

The ExecuteAsync for this program is as follows:

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string joke = _jokeService.GetJoke();
                _logger.LogWarning("{Joke}", joke);
                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "{Message}", ex.Message);
            // Terminates this process and returns an exit code to the operating system.
            // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
            // performs one of two scenarios:
            // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
            // 2. When set to "StopHost": will cleanly stop the host, and log errors.
            //
            // In order for the Windows Service Management system to leverage configured
            // recovery options, we need to terminate the process with a non-zero exit code.
            Environment.Exit(1);
        }
    }

I added a handler for TaskCanceledException as follows:

catch (TaskCanceledException ex)
{
    //Do nothing, we're exiting normally.
    Environment.Exit(0);
}

When I do that, stopping the service through MMC shows an error "Error 1067: The process terminated unexpectedly" and a message is not logged indicating the service was stopped. Stopping it with sc on the command line doesn't show an error, but it also doesn't log a message that the service was stopped.

If I remove the Environment.Exit(0) line from this handler, MMC no longer shows an error, but the "Service stopped successfully" log message is written twice.

What is the proper way I should be ending my service?

Thank you.

.NET
.NET
Microsoft Technologies based on the .NET software framework.
3,415 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,308 questions
{count} votes

1 answer

Sort by: Most helpful
  1. ArtixanSystems 56 Reputation points
    2023-05-02T01:37:45.2633333+00:00

    Not sure if you ever solved this, but I can tell you what seems to work fine for me.

    In my override of ExecuteAsync, everything is within a try...catch...finally statement. In my case, within the try block I'm calling all sorts of async tasks and passing the CancellationToken to them.

    My catch statements look something like this:

    catch (OperationCanceledException)
    {
    	// maybe you do some state management or cleanup stuff 
        _logger.Verbose("Shutting down service by request."); // I'm using serilog, but whatever
    	// you can see this is basically just logging and swallowing the exception.
    }
    catch (Exception ex) // generic handler
    {
    	_logger.Fatal($"Big problems abound: {ex.Message}");
    	Environment.Exit(1);
    }
    

    I have a finally block as follows (shown in context, just sub the catch for the ones above):

    protected override async Task ExecuteAsync(CancellationToken stoppingToken) 
    {
    	try {..}
    	catch { } // the specific catch blocks I had above
    	finally
    	{
    		_logger.Information("Service Stopped.");
    	}
    }
    

    And that's it, I'm out! Notice there's no Environment.Exit(0), and notice that the catch of the OperationCanceledException also isn't calling an exit. It's just logging that an organized call to shut down happened, and let's the execution flow through to the bottom of the method.

    As you said, if I have the Environment.Exit(0) anywhere in there, it throws the 'stopped unexpectedly' error in Windows. I believe this happens because your program is technically not able to run to the end -- the ExecuteAsync method is just passing control back to the Main method in program.cs, so when you call the Exit(0) method, you're actually cutting the whole process short.

    With the above, I get clean logging of the shutdown process I go through, and no error when stopping the service.

    Hope it helps!

    0 comments No comments