Skip to content

Commit 39d9042

Browse files
committed
Properly report the exitcode without reporting we're stopping (or we won't trigger a restart)
1 parent 042a5cc commit 39d9042

5 files changed

Lines changed: 84 additions & 10 deletions

File tree

docs/index.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,13 @@ This library used to contain exception handling code in a base service, which is
5656
5757
With the [retirement of .NET 5 on May 8, 2022](https://docs.microsoft.com/en-us/lifecycle/products/microsoft-net-and-net-core), this WindowsServiceExtensions library targets .NET (Platform Extensions) 6 going forward from v3.0.0.
5858

59-
However, in the case of a background service excption, the service doesn't report an error to the Service Control Manager, who will think the process exited nicely. This library fixes that.
59+
However, in the case of a background service excption, the service doesn't report an error to the Service Control Manager, who will think the process exited nicely. So these are the scenarios:
60+
61+
* You set `ServiceBase.ExitCode` to 0 and call `ServiceBase.Stop()`: no events will be logged, your service's recovery actions won't run.
62+
* You set `ServiceBase.ExitCode` to > 0 and call `ServiceBase.Stop()`: events 7023 ("service terminated with the following error") and 7034 (" service terminated unexpectedly") will be logged, your service's recovery actions won't run.
63+
* You set `ServiceBase.ExitCode` to > 0 and _don't_ call `ServiceBase.Stop()` but just exit the application: events 7031 ("service terminated unexpectedly. It has done this N time(s). The following corrective action will be taken") will be logged, your service's recovery actions will be executed.
64+
65+
I prefer the latter, so that's what this library does.
6066

6167
## Host Builder (dependency injection)
6268
To receive session or power events, call `UseWindowsServiceExtensions()` on your Host Builder:
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace CodeCaster.WindowsServiceExtensions
5+
{
6+
internal static class Interop
7+
{
8+
internal static class Advapi32
9+
{
10+
[StructLayout(LayoutKind.Sequential)]
11+
internal struct ServiceStatus
12+
{
13+
internal int serviceType;
14+
internal int currentState;
15+
internal int controlsAccepted;
16+
internal int win32ExitCode;
17+
internal int serviceSpecificExitCode;
18+
internal int checkPoint;
19+
internal int waitHint;
20+
}
21+
22+
[DllImport("advapi32.dll", SetLastError = true)]
23+
public static extern bool SetServiceStatus(IntPtr handle, ref ServiceStatus serviceStatus);
24+
}
25+
}
26+
}

src/CodeCaster.WindowsServiceExtensions/Lifetime/HostApplicationStartupLifetime.cs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Reflection;
3+
using System.ServiceProcess;
24
using System.Threading;
35
using System.Threading.Tasks;
46
using Microsoft.Extensions.Hosting;
@@ -76,14 +78,50 @@ public HostApplicationStartupLifetime(IServiceProvider serviceProvider, IHostEnv
7678
/// </summary>
7779
public new Task StopAsync(CancellationToken cancellationToken)
7880
{
79-
if (!OperatingSystem.IsWindows() || !WindowsServiceHelpers.IsWindowsService() || ExitCode == 0)
81+
if (ExitCode == 0)
8082
{
8183
base.StopAsync(cancellationToken);
8284
}
83-
85+
8486
return Task.CompletedTask;
8587
}
8688

89+
/// <summary>
90+
/// Sets the exit code and reports that back to the Service Control Manager.
91+
///
92+
/// <see cref="ServiceBase.ExitCode"/> only gets flushed on Stop(), which we don't want to call on error.
93+
/// </summary>
94+
public new int ExitCode
95+
{
96+
get => base.ExitCode;
97+
set
98+
{
99+
if (value < 0)
100+
{
101+
throw new ArgumentException("ExitCode must be 0 or greater");
102+
}
103+
104+
base.ExitCode = value;
105+
106+
var privateStatus = typeof(ServiceBase).GetField("_status", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(this)!;
107+
108+
var finalStatus = new Interop.Advapi32.ServiceStatus
109+
{
110+
win32ExitCode = value,
111+
//serviceSpecificExitCode = serviceSpecificExitCode,
112+
113+
// Copy the properties from ServiceBase._status into a struct that we know.
114+
serviceType = GetStatusField(privateStatus, "serviceType"),
115+
checkPoint = GetStatusField(privateStatus, "checkPoint"),
116+
controlsAccepted = GetStatusField(privateStatus, "controlsAccepted"),
117+
waitHint = GetStatusField(privateStatus, "waitHint"),
118+
currentState = GetStatusField(privateStatus, "currentState"),
119+
};
120+
121+
Interop.Advapi32.SetServiceStatus(ServiceHandle, ref finalStatus);
122+
}
123+
}
124+
87125
/// <summary>
88126
/// Override to execute startup code, no need to call base. Async because who knows.
89127
/// </summary>
@@ -119,6 +157,12 @@ protected override void OnStart(string[] args)
119157
base.OnStart(args);
120158
}
121159

160+
private int GetStatusField(object privateStatus, string fieldName)
161+
{
162+
var field = privateStatus.GetType().GetField(fieldName);
163+
return (int)field!.GetValue(privateStatus)!;
164+
}
165+
122166
/// <inheritdoc />
123167
protected override void Dispose(bool disposing)
124168
{

src/CodeCaster.WindowsServiceExtensions/Service/WindowsServiceBackgroundService.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,25 +63,23 @@ protected sealed override async Task ExecuteAsync(CancellationToken stoppingToke
6363

6464
await TryExecuteAsync(stoppingToken);
6565
}
66-
catch (Exception)
66+
catch (Exception ex)
6767
{
68-
Logger.LogInformation("Exception occurred, setting process exit code to {exitCode}", ErrorInvalidData);
68+
Logger.LogInformation(ex, "Unhandled exception in {serviceType}, setting process exit code to {exitCode}:", GetType().FullName, ErrorInvalidData);
6969

7070
// The .NET host will shut down with code 0 if we don't do this.
7171
Environment.ExitCode = ErrorInvalidData;
7272

73-
#pragma warning disable CA1416 // Validate platform compatibility - we are a Windows Service.
7473
if (ServiceLifetime != null)
7574
{
7675
Logger.LogDebug("Setting service exit code to {exitCode}", ErrorInvalidData);
7776

7877
// To report to the Service Control Manager on failure, is uint so > 0.
79-
ServiceLifetime.ExitCode = ErrorInvalidData + 1;
78+
// Do _not_ call ServiceBase.Stop() after this, or it'll think we exited successfully.
79+
ServiceLifetime.ExitCode = ErrorInvalidData;
8080
}
81-
#pragma warning restore CA1416 // Validate platform compatibility
8281

8382
// Let the BackgroundService handle and log the exception.
84-
// Do _not_ call ServiceBase.Stop(), or it'll think we exited successfully.
8583
throw;
8684
}
8785
}

test/TestServiceThatThrows/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static async Task Main()
2525
Debugger.Break();
2626
}
2727

28-
SecondsToWaitBeforeThrowing = Debugger.IsAttached ? 1 : 31;
28+
SecondsToWaitBeforeThrowing = Debugger.IsAttached ? 1 : 5;
2929

3030
await new HostBuilder()
3131
.ConfigureLogging(l => l.AddConsole())

0 commit comments

Comments
 (0)