Skip to content

Commit 6814f7e

Browse files
committed
Handle service error by setting positive error code and stopping the service
1 parent a84fc4d commit 6814f7e

8 files changed

Lines changed: 52 additions & 77 deletions

File tree

Docs.projitems

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
<Content Include="$(MSBuildThisFileDirectory)docs\downloads.md" />
1313
<Content Include="$(MSBuildThisFileDirectory)docs\upgrading-v2-v3.md" />
1414
<Content Include="$(MSBuildThisFileDirectory)docs\_layouts\cayman-with-menu.html" />
15-
<Content Include="$(MSBuildThisFileDirectory)docs\demo.md" />
1615
<Content Include="$(MSBuildThisFileDirectory)docs\_config.yml" />
1716
<Content Include="$(MSBuildThisFileDirectory)docs\index.md" />
1817
</ItemGroup>

Readme.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
# WindowsServiceExtensions
2-
This project exists of a few classes that make building reliable Windows Services easier.
1+
# Windows Service Extensions
2+
Building a basic Windows Service that does some long-running background work is trivial, using .NET's [`UseWindowsService()`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.windowsservicelifetimehostbuilderextensions.usewindowsservice?view=dotnet-plat-ext-6.0) (from the Platform Extensions package `Microsoft.Extensions.Hosting.WindowsServices`) and [`Microsoft.Extensions.Hosting.BackgroundService`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice?view=dotnet-plat-ext-6.0) from `Microsoft.Extensions.Hosting.Abstractions`.
33

4-
Make a .NET Core Windows Service that runs as `IHostedService` aware of the computer shutting down and starting up. On consumer OS Windows 10+, shutting down the computer will actually hibernate the OS. Services won't get another `OnStart()` call when the computer starts again.
4+
Running as a _Windows Service_ and _running BackgroundServices_ are two separate things though, and they are not connected in any way. A non-service console app can run background services, and so can and do web applications. Each of those are different hosting environments, with their own lifetimes.
55

6-
The Lifetime classes also include [kmcclellan's fixes](https://github.com/dotnet/runtime/issues/50019#issuecomment-678658133) that make the service throw when something fails on .NET Host startup, instead of reprorting it started successfully, and code to react to user session changes.
6+
This project exists of a few classes that make building reliable Windows Services easier, to gap this disconnect.
7+
8+
## Lifetime
9+
The following improvements are included in this library:
10+
11+
* `OnStart()` can fail because of invalid service configuration, quit the application when that happens (by [kmcclellan](https://github.com/dotnet/runtime/issues/50019#issuecomment-678658133)).
12+
* On consumer OS Windows 10+, shutting down the computer will actually hibernate the OS. Services won't get another `OnStart()` call when the computer starts again, nor will your background services be notified. Now they will.
13+
* This library makes an `IHostedService` running inside a .NET Windows Service that aware of the user logging in and out, and the computer shutting down and starting up.
14+
* When an exception occurs during your BackgroundService's lifetime, .NET Platform Extensions < 6 didn't stop the application host. Now it does, but it doesn't report an error to the Service Control Manager. With this extension, it does, as well as setting a process exit code: 13 in both cases ("invalid data").
715

816
## Installation
917
Through [NuGet](https://www.nuget.org/packages/CodeCaster.WindowsServiceExtensions/):
@@ -27,9 +35,13 @@ Extended upgrading docs: see https://codecasternl.github.io/WindowsServiceExtens
2735
## Usage
2836
These methods from this package allow your `IHostedService`s to respond to Windows Service events relating to sessions (user logon/logoff) and power state (shutdown/hibernate/resume):
2937

30-
* On your Host Builder, call `UseWindowsServiceExtensions()` instead of [`UseWindowsService()`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.windowsservicelifetimehostbuilderextensions.usewindowsservice?view=dotnet-plat-ext-3.1).
31-
* Instead of letting your service inherit [`Microsoft.Extensions.Hosting.BackgroundService`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice?view=dotnet-plat-ext-5.0), inherit from `CodeCaster.WindowsServiceExtensions.WindowsServiceBackgroundService`.
38+
* On your Host Builder, call `UseWindowsServiceExtensions()` instead of `UseWindowsService()`.
39+
* Instead of letting your service inherit `BackgroundService`, inherit from `CodeCaster.WindowsServiceExtensions.WindowsServiceBackgroundService`.
3240
* Implement the method `public override bool OnPowerEvent(PowerBroadcastStatus powerStatus) { ... }` and do your thing when it's called with a certain status.
3341
* Implement the method `public override bool OnSessionChange(SessionChangeDescription changeDescription) { ... }` and do your thing when it's called with a certain status.
3442

3543
Do note that the statuses received can vary. You get either `ResumeSuspend`, `ResumeAutomatic` or both reported to `OnPowerEvent()`, never neither, after a machine wake, reboot or boot.
44+
45+
# Contributing
46+
Please file an issue or PR. Even if you use this and are happy with it.
47+

docs/demo.md

Lines changed: 0 additions & 41 deletions
This file was deleted.

src/CodeCaster.WindowsServiceExtensions/Lifetime/ExtendedWindowsServiceLifetime.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ public ExtendedWindowsServiceLifetime(IServiceProvider serviceProvider, IHostEnv
3131
throw new PlatformNotSupportedException($"Windows Service needs to run on Windows. Remove the call to {methodName}()");
3232
}
3333

34+
// Explicitly stop the service instead of just exiting the process.
35+
// But this this will kill the application quickly, do other BackgroundServices get the time to exit nicely?
36+
applicationLifetime.ApplicationStopping.Register(Stop);
37+
3438
CanHandlePowerEvent = true;
3539
CanHandleSessionChangeEvent = true;
3640
}

src/CodeCaster.WindowsServiceExtensions/Lifetime/HostApplicationStartupLifetime.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public HostApplicationStartupLifetime(IServiceProvider serviceProvider, IHostEnv
5151
public new async Task WaitForStartAsync(CancellationToken cancellationToken)
5252
{
5353
Logger.LogInformation("Windows Service start requested");
54+
5455
ApplicationLifetime.ApplicationStarted.Register(() => _started.Set());
5556

5657
try
@@ -91,11 +92,11 @@ protected override void OnStart(string[] args)
9192
_started.Wait(ApplicationLifetime.ApplicationStopping);
9293

9394
// This can happen very early in the startup process (even before ApplicationStopping is cancelled), so this may not be logged nor reported at all,
94-
// because the DI isn't complete and the logger (file, event log, ...) not available while we're being disposed.
95+
// because the DI isn't complete and the logger (file, event log, ...) not available while we're being disposed. Probably corrupt config file?
9596
if (!ApplicationLifetime.ApplicationStarted.IsCancellationRequested)
9697
{
9798
const string errorString = "Windows Service failed to start: some part reported it started, the other didn't";
98-
99+
99100
Logger.LogError(errorString);
100101

101102
// Prevent the service from happily reporting successful startup, while the .NET Core ApplicationHost isn't started at all.

src/CodeCaster.WindowsServiceExtensions/Service/WindowsServiceBackgroundService.cs

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ namespace CodeCaster.WindowsServiceExtensions.Service
1414
/// </summary>
1515
public abstract class WindowsServiceBackgroundService : BackgroundService, IWindowsServiceAwareHostedService
1616
{
17+
/// <summary>
18+
/// Used for error reporting.
19+
/// </summary>
20+
private const int ErrorInvalidData = 13;
21+
1722
/// <summary>
1823
/// Logs.
1924
/// </summary>
@@ -58,39 +63,32 @@ public virtual void OnSessionChange(SessionChangeDescription changeDescription)
5863
/// <summary>
5964
/// Overridden and sealed from <see cref="BackgroundService.ExecuteAsync "/> to set the exit code on exception.
6065
/// </summary>
61-
protected sealed override Task ExecuteAsync(CancellationToken stoppingToken)
66+
protected sealed override async Task ExecuteAsync(CancellationToken stoppingToken)
6267
{
6368
try
6469
{
65-
Logger.LogDebug("{serviceType}.{execute}() called, calling {tryExecute}()", GetType().FullName, nameof(ExecuteAsync), nameof(TryExecuteAsync));
70+
Logger.LogInformation("{serviceType}.{execute}() called, calling {tryExecute}()", GetType().FullName, nameof(ExecuteAsync), nameof(TryExecuteAsync));
6671

67-
return TryExecuteAsync(stoppingToken);
72+
await TryExecuteAsync(stoppingToken);
6873
}
6974
catch (Exception)
7075
{
71-
// The host will shut down with code 0 if we don't do this.
72-
const int exitCode = -1;
76+
Logger.LogDebug("Setting process exit code to {exitCode}", ErrorInvalidData);
7377

74-
Logger.LogInformation("Setting process exit code to {exitCode}", exitCode);
75-
76-
// Doesn't work, overwritten by ConsoleLifetime?
77-
Environment.ExitCode = exitCode;
78+
// The host will shut down with code 0 if we don't do this.
79+
Environment.ExitCode = ErrorInvalidData;
7880

7981
#pragma warning disable CA1416 // Validate platform compatibility - we are a Windows Service.
8082
if (ServiceLifetime != null)
8183
{
82-
Logger.LogWarning("Setting service exit code to {exitCode}", exitCode);
84+
Logger.LogDebug("Setting service exit code to {exitCode}", ErrorInvalidData);
8385

84-
ServiceLifetime.ExitCode = exitCode;
85-
86-
// This will probably yank the .NET Host from under us, not executing any more (or just a little) code.
87-
ServiceLifetime.Stop();
86+
// To report to the Service Control Manager on failure, is uint so > 0.
87+
ServiceLifetime.ExitCode = ErrorInvalidData;
8888
}
8989
#pragma warning restore CA1416 // Validate platform compatibility
9090

91-
ApplicationLifetime.StopApplication();
92-
93-
// Let the WindowsServiceLifetime handle the exception.
91+
// Let the BackgroundService handle the exception.
9492
throw;
9593
}
9694
}

test/TestServiceThatThrows/Program.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ public static class Program
1414
{
1515
public static async Task Main()
1616
{
17+
// Hint: Reattach to Process (Shift+Alt+P)
1718
if (!Debugger.IsAttached)
1819
{
19-
Console.WriteLine("Waiting 5s for debugger to be attached...");
20+
Console.WriteLine("Waiting 5s for debugger to be attached and pretending the service starts up slowly...");
2021
Thread.Sleep(5000);
2122
Debugger.Break();
2223
}
@@ -28,9 +29,9 @@ public static async Task Main()
2829
// Services to run are defined below.
2930

3031
// These three run successfully.
31-
s.AddHostedService<MyHappyService>();
32-
s.AddHostedService<MyHappyBackgroundService>();
33-
s.AddHostedService<QuicklyQuittingBackgroundService>();
32+
//s.AddHostedService<MyHappyService>();
33+
//s.AddHostedService<MyHappyBackgroundService>();
34+
//s.AddHostedService<QuicklyQuittingBackgroundService>();
3435

3536
// This one breaks in OnStart() (should give startup error).
3637
//s.AddHostedService<MyFaultyService>();
@@ -44,11 +45,12 @@ public static async Task Main()
4445
// Should give startup error.
4546
//throw new InvalidOperationException("Heh");
4647
})
47-
//.UseWindowsService()
48-
.UseWindowsServiceExtensions(o =>
49-
{
50-
//o.ServiceName = ...
51-
})
48+
.UseWindowsService()
49+
.UseWindowsServiceExtensions()
50+
//.UseWindowsServiceExtensions(o =>
51+
//{
52+
// //o.ServiceName = ...
53+
//})
5254
.Build()
5355
.RunAsync();
5456
}

test/TestServiceThatThrows/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"EventLog": {
99
"LogLevel": {
1010
"Default": "Warning",
11-
"Microsoft.Hosting.Lifetime": "Warning"
11+
"Microsoft.Hosting.Lifetime": "Information"
1212
}
1313
}
1414
}

0 commit comments

Comments
 (0)