fix(host): log a routine service stop as Information, not [FTL] (ScadaBridge#76)

An OperationCanceledException escaping RunAsync after ApplicationStopping has fired
(WindowsServiceLifetime.StopAsync gets a cancelled token after shutdown finished) is a
normal stop. Program.IsGracefulShutdown now routes it to an Information line; any other
exception, or a cancellation with no shutdown requested, still logs Fatal and rethrows.
This commit is contained in:
Joseph Doherty
2026-11-09 12:05:20 -05:00
parent a5c3de63e9
commit e0ec2a0507
2 changed files with 65 additions and 0 deletions
@@ -0,0 +1,39 @@
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
/// <summary>
/// ScadaBridge#76: a routine Windows service stop must not be logged as
/// <c>[FTL] host terminated unexpectedly</c>.
/// </summary>
public class GracefulShutdownTests
{
/// <summary>A cancellation escaping RunAsync after ApplicationStopping fired is a routine stop.</summary>
[Fact]
public void Cancellation_AfterStopRequested_IsGraceful()
{
using var cts = new CancellationTokenSource();
cts.Cancel();
Assert.True(Program.IsGracefulShutdown(new OperationCanceledException(), cts.Token));
Assert.True(Program.IsGracefulShutdown(new TaskCanceledException(), cts.Token));
}
/// <summary>A cancellation with no shutdown requested is still a genuine failure.</summary>
[Fact]
public void Cancellation_WithoutStopRequested_IsNotGraceful()
{
using var cts = new CancellationTokenSource();
Assert.False(Program.IsGracefulShutdown(new OperationCanceledException(), cts.Token));
Assert.False(Program.IsGracefulShutdown(new OperationCanceledException(), CancellationToken.None));
}
/// <summary>A real fault during shutdown must still be reported as fatal.</summary>
[Fact]
public void NonCancellationException_IsNeverGraceful()
{
using var cts = new CancellationTokenSource();
cts.Cancel();
Assert.False(Program.IsGracefulShutdown(new InvalidOperationException("boom"), cts.Token));
}
}