diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ServiceManager.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ServiceManager.cs
new file mode 100644
index 0000000..2bade64
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ServiceManager.cs
@@ -0,0 +1,180 @@
+// Copyright 2012-2026 The NATS Authors
+// Licensed under the Apache License, Version 2.0
+//
+// Adapted from server/service.go and server/service_windows.go in the NATS server Go source.
+
+using System.Runtime.InteropServices;
+
+namespace ZB.MOM.NatsNet.Server.Internal;
+
+///
+/// Service wrappers for platform-specific server startup behavior.
+///
+public static class ServiceManager
+{
+ private static readonly Lock ServiceNameLock = new();
+ private static string _serviceName = "nats-server";
+ private static bool _dockerized;
+
+ ///
+ /// Allows overriding the service name.
+ /// Mirrors Go SetServiceName.
+ ///
+ public static void SetServiceName(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ return;
+
+ lock (ServiceNameLock)
+ {
+ _serviceName = name;
+ }
+ }
+
+ ///
+ /// Initializes service-related environment flags.
+ /// Mirrors Go package init() behavior in service_windows.go.
+ ///
+ public static void Init(Func? envLookup = null)
+ {
+ var lookup = envLookup ?? Environment.GetEnvironmentVariable;
+ _dockerized = string.Equals(lookup("NATS_DOCKERIZED"), "1", StringComparison.Ordinal);
+ }
+
+ ///
+ /// Runs the server startup action.
+ /// Mirrors Go Run behavior from service.go/service_windows.go.
+ ///
+ public static Exception? Run(Action startServer, Func? windowsServiceProbe = null)
+ {
+ ArgumentNullException.ThrowIfNull(startServer);
+
+ if (_dockerized)
+ {
+ startServer();
+ return null;
+ }
+
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ startServer();
+ return null;
+ }
+
+ if (!IsWindowsService(windowsServiceProbe))
+ {
+ startServer();
+ return null;
+ }
+
+ return new PlatformNotSupportedException(
+ "Windows service hosting is managed by Microsoft.Extensions.Hosting.WindowsServices.");
+ }
+
+ ///
+ /// Returns true when running as a Windows service.
+ /// Mirrors Go isWindowsService.
+ ///
+ public static bool IsWindowsService(Func? windowsServiceProbe = null)
+ {
+ if (_dockerized)
+ return false;
+
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return false;
+
+ if (windowsServiceProbe is not null)
+ return windowsServiceProbe();
+
+ return !Environment.UserInteractive;
+ }
+
+ ///
+ /// Windows service execution wrapper.
+ /// Mirrors Go winServiceWrapper.Execute control loop semantics.
+ ///
+ public static (bool serviceSpecificExitCode, uint exitCode) Execute(
+ Action startServer,
+ Func readyForConnections,
+ IEnumerable changes,
+ Action reloadConfig,
+ Action shutdown,
+ Action reopenLogFile,
+ Action enterLameDuckMode,
+ Func? envLookup = null,
+ Action? logError = null)
+ {
+ ArgumentNullException.ThrowIfNull(startServer);
+ ArgumentNullException.ThrowIfNull(readyForConnections);
+ ArgumentNullException.ThrowIfNull(changes);
+ ArgumentNullException.ThrowIfNull(reloadConfig);
+ ArgumentNullException.ThrowIfNull(shutdown);
+ ArgumentNullException.ThrowIfNull(reopenLogFile);
+ ArgumentNullException.ThrowIfNull(enterLameDuckMode);
+
+ var startupDelay = TimeSpan.FromSeconds(10);
+ var lookup = envLookup ?? Environment.GetEnvironmentVariable;
+ var configuredDelay = lookup("NATS_STARTUP_DELAY");
+ if (!string.IsNullOrEmpty(configuredDelay))
+ {
+ if (TimeSpan.TryParse(configuredDelay, out var parsedDelay))
+ {
+ startupDelay = parsedDelay;
+ }
+ else
+ {
+ logError?.Invoke($"Failed to parse \"{configuredDelay}\" as a duration for startup.");
+ }
+ }
+
+ Task.Run(startServer);
+
+ if (!readyForConnections(startupDelay))
+ return (false, 1);
+
+ foreach (var change in changes)
+ {
+ switch (change)
+ {
+ case ServiceControlCommand.Interrogate:
+ continue;
+ case ServiceControlCommand.Stop:
+ case ServiceControlCommand.Shutdown:
+ shutdown();
+ return (false, 0);
+ case ServiceControlCommand.ReopenLog:
+ reopenLogFile();
+ break;
+ case ServiceControlCommand.LameDuckMode:
+ Task.Run(enterLameDuckMode);
+ break;
+ case ServiceControlCommand.ParamChange:
+ reloadConfig();
+ break;
+ }
+ }
+
+ return (false, 0);
+ }
+
+ internal static string CurrentServiceName
+ {
+ get
+ {
+ lock (ServiceNameLock)
+ {
+ return _serviceName;
+ }
+ }
+ }
+}
+
+public enum ServiceControlCommand
+{
+ Interrogate,
+ Stop,
+ Shutdown,
+ ReopenLog,
+ LameDuckMode,
+ ParamChange
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs
index f0e4429..640c36c 100644
--- a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs
@@ -220,12 +220,17 @@ public static class SignalHandler
///
/// Runs the server (non-Windows). Mirrors Run in service.go.
///
- public static void Run(Action startServer) => startServer();
+ public static void Run(Action startServer)
+ {
+ var error = ServiceManager.Run(startServer);
+ if (error is not null)
+ throw error;
+ }
///
/// Returns false on non-Windows. Mirrors isWindowsService.
///
- public static bool IsWindowsService() => false;
+ public static bool IsWindowsService() => ServiceManager.IsWindowsService();
}
/// Unix signal codes for NATS command mapping.
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServiceManagerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServiceManagerTests.cs
new file mode 100644
index 0000000..9250020
--- /dev/null
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServiceManagerTests.cs
@@ -0,0 +1,97 @@
+// Copyright 2026 The NATS Authors
+// Licensed under the Apache License, Version 2.0
+
+using Shouldly;
+using ZB.MOM.NatsNet.Server.Internal;
+
+namespace ZB.MOM.NatsNet.Server.Tests.Internal;
+
+public sealed class ServiceManagerTests
+{
+ public ServiceManagerTests()
+ {
+ ServiceManager.Init(static _ => null);
+ ServiceManager.SetServiceName("nats-server");
+ }
+
+ [Fact]
+ public void SetServiceName_WhenProvided_StoresConfiguredName()
+ {
+ ServiceManager.SetServiceName("custom-svc");
+
+ ServiceManager.CurrentServiceName.ShouldBe("custom-svc");
+ }
+
+ [Fact]
+ public void IsWindowsService_WhenDockerized_ReturnsFalse()
+ {
+ ServiceManager.Init(static key => key == "NATS_DOCKERIZED" ? "1" : null);
+
+ ServiceManager.IsWindowsService(() => true).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void Run_WhenNotRunningAsWindowsService_InvokesStartAction()
+ {
+ var called = false;
+
+ var error = ServiceManager.Run(() => called = true, () => false);
+
+ error.ShouldBeNull();
+ called.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Execute_WhenServerNotReady_ReturnsFailureExitCode()
+ {
+ using var started = new ManualResetEventSlim(false);
+ var reloadCalls = 0;
+ var shutdownCalls = 0;
+
+ var result = ServiceManager.Execute(
+ startServer: () => started.Set(),
+ readyForConnections: _ => false,
+ changes: [],
+ reloadConfig: () => reloadCalls++,
+ shutdown: () => shutdownCalls++,
+ reopenLogFile: () => { },
+ enterLameDuckMode: () => { });
+
+ result.exitCode.ShouldBe((uint)1);
+ result.serviceSpecificExitCode.ShouldBeFalse();
+ started.Wait(TimeSpan.FromSeconds(1)).ShouldBeTrue();
+ reloadCalls.ShouldBe(0);
+ shutdownCalls.ShouldBe(0);
+ }
+
+ [Fact]
+ public void Execute_WhenCommandsReceived_DispatchesControlHandlers()
+ {
+ var reloadCalls = 0;
+ var shutdownCalls = 0;
+ var reopenCalls = 0;
+ var ldmCalled = false;
+ using var ldmInvoked = new ManualResetEventSlim(false);
+
+ var result = ServiceManager.Execute(
+ startServer: () => { },
+ readyForConnections: _ => true,
+ changes: [ServiceControlCommand.ReopenLog, ServiceControlCommand.ParamChange, ServiceControlCommand.LameDuckMode, ServiceControlCommand.Stop],
+ reloadConfig: () => reloadCalls++,
+ shutdown: () => shutdownCalls++,
+ reopenLogFile: () => reopenCalls++,
+ enterLameDuckMode: () =>
+ {
+ ldmCalled = true;
+ ldmInvoked.Set();
+ });
+
+ result.exitCode.ShouldBe((uint)0);
+ result.serviceSpecificExitCode.ShouldBeFalse();
+ reloadCalls.ShouldBe(1);
+ reopenCalls.ShouldBe(1);
+ shutdownCalls.ShouldBe(1);
+ ldmInvoked.Wait(TimeSpan.FromSeconds(1)).ShouldBeTrue();
+ ldmCalled.ShouldBeTrue();
+ }
+}
diff --git a/porting.db b/porting.db
index 3da9f5a..27bdc82 100644
Binary files a/porting.db and b/porting.db differ
diff --git a/reports/current.md b/reports/current.md
index e860f29..d5fdf37 100644
--- a/reports/current.md
+++ b/reports/current.md
@@ -1,6 +1,6 @@
# NATS .NET Porting Status Report
-Generated: 2026-02-28 12:31:12 UTC
+Generated: 2026-02-28 12:37:17 UTC
## Modules (12 total)
@@ -12,10 +12,10 @@ Generated: 2026-02-28 12:31:12 UTC
| Status | Count |
|--------|-------|
-| deferred | 2348 |
+| deferred | 2341 |
| n_a | 24 |
| stub | 1 |
-| verified | 1300 |
+| verified | 1307 |
## Unit Tests (3257 total)
@@ -34,4 +34,4 @@ Generated: 2026-02-28 12:31:12 UTC
## Overall Progress
-**2502/6942 items complete (36.0%)**
+**2509/6942 items complete (36.1%)**