feat(batch3): implement service feature group

This commit is contained in:
Joseph Doherty
2026-02-28 07:37:17 -05:00
parent beab0e60da
commit 7ebbaeb6b3
5 changed files with 288 additions and 6 deletions

View 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
}

View File

@@ -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>