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>
|
||||
/// Runs the server (non-Windows). Mirrors <c>Run</c> in service.go.
|
||||
/// </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>
|
||||
/// Returns false on non-Windows. Mirrors <c>isWindowsService</c>.
|
||||
/// </summary>
|
||||
public static bool IsWindowsService() => false;
|
||||
public static bool IsWindowsService() => ServiceManager.IsWindowsService();
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
||||
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%)**
|
||||
|
||||
Reference in New Issue
Block a user