feat(batch3): implement service feature group
This commit is contained in:
180
dotnet/src/ZB.MOM.NatsNet.Server/Internal/ServiceManager.cs
Normal file
180
dotnet/src/ZB.MOM.NatsNet.Server/Internal/ServiceManager.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service wrappers for platform-specific server startup behavior.
|
||||||
|
/// </summary>
|
||||||
|
public static class ServiceManager
|
||||||
|
{
|
||||||
|
private static readonly Lock ServiceNameLock = new();
|
||||||
|
private static string _serviceName = "nats-server";
|
||||||
|
private static bool _dockerized;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows overriding the service name.
|
||||||
|
/// Mirrors Go <c>SetServiceName</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetServiceName(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (ServiceNameLock)
|
||||||
|
{
|
||||||
|
_serviceName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes service-related environment flags.
|
||||||
|
/// Mirrors Go package <c>init()</c> behavior in service_windows.go.
|
||||||
|
/// </summary>
|
||||||
|
public static void Init(Func<string, string?>? envLookup = null)
|
||||||
|
{
|
||||||
|
var lookup = envLookup ?? Environment.GetEnvironmentVariable;
|
||||||
|
_dockerized = string.Equals(lookup("NATS_DOCKERIZED"), "1", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the server startup action.
|
||||||
|
/// Mirrors Go <c>Run</c> behavior from service.go/service_windows.go.
|
||||||
|
/// </summary>
|
||||||
|
public static Exception? Run(Action startServer, Func<bool>? 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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true when running as a Windows service.
|
||||||
|
/// Mirrors Go <c>isWindowsService</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsWindowsService(Func<bool>? windowsServiceProbe = null)
|
||||||
|
{
|
||||||
|
if (_dockerized)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (windowsServiceProbe is not null)
|
||||||
|
return windowsServiceProbe();
|
||||||
|
|
||||||
|
return !Environment.UserInteractive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Windows service execution wrapper.
|
||||||
|
/// Mirrors Go <c>winServiceWrapper.Execute</c> control loop semantics.
|
||||||
|
/// </summary>
|
||||||
|
public static (bool serviceSpecificExitCode, uint exitCode) Execute(
|
||||||
|
Action startServer,
|
||||||
|
Func<TimeSpan, bool> readyForConnections,
|
||||||
|
IEnumerable<ServiceControlCommand> changes,
|
||||||
|
Action reloadConfig,
|
||||||
|
Action shutdown,
|
||||||
|
Action reopenLogFile,
|
||||||
|
Action enterLameDuckMode,
|
||||||
|
Func<string, string?>? envLookup = null,
|
||||||
|
Action<string>? 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
|
||||||
|
}
|
||||||
@@ -220,12 +220,17 @@ public static class SignalHandler
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the server (non-Windows). Mirrors <c>Run</c> in service.go.
|
/// Runs the server (non-Windows). Mirrors <c>Run</c> in service.go.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns false on non-Windows. Mirrors <c>isWindowsService</c>.
|
/// Returns false on non-Windows. Mirrors <c>isWindowsService</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsWindowsService() => false;
|
public static bool IsWindowsService() => ServiceManager.IsWindowsService();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Unix signal codes for NATS command mapping.</summary>
|
/// <summary>Unix signal codes for NATS command mapping.</summary>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
# NATS .NET Porting Status Report
|
# NATS .NET Porting Status Report
|
||||||
|
|
||||||
Generated: 2026-02-28 12:31:12 UTC
|
Generated: 2026-02-28 12:37:17 UTC
|
||||||
|
|
||||||
## Modules (12 total)
|
## Modules (12 total)
|
||||||
|
|
||||||
@@ -12,10 +12,10 @@ Generated: 2026-02-28 12:31:12 UTC
|
|||||||
|
|
||||||
| Status | Count |
|
| Status | Count |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| deferred | 2348 |
|
| deferred | 2341 |
|
||||||
| n_a | 24 |
|
| n_a | 24 |
|
||||||
| stub | 1 |
|
| stub | 1 |
|
||||||
| verified | 1300 |
|
| verified | 1307 |
|
||||||
|
|
||||||
## Unit Tests (3257 total)
|
## Unit Tests (3257 total)
|
||||||
|
|
||||||
@@ -34,4 +34,4 @@ Generated: 2026-02-28 12:31:12 UTC
|
|||||||
|
|
||||||
## Overall Progress
|
## Overall Progress
|
||||||
|
|
||||||
**2502/6942 items complete (36.0%)**
|
**2509/6942 items complete (36.1%)**
|
||||||
|
|||||||
Reference in New Issue
Block a user