feat: port session 04 — Logging, Signals & Services

- NatsLogger.cs: INatsLogger interface (Noticef/Warnf/Fatalf/Errorf/Debugf/Tracef),
  ServerLogging state class with atomic debug/trace flags, rate-limited logging
  (RateLimitWarnf/RateLimitDebugf), error variants (Errors/Errorc/Errorsc),
  MicrosoftLoggerAdapter bridging to ILogger
- SignalHandler.cs: ProcessSignal (Unix kill via Process), CommandToUnixSignal mapping
  (Stop→SIGKILL, Quit→SIGINT, Reopen→SIGUSR1, Reload→SIGHUP), ResolvePids via pgrep,
  SetProcessName, Run/IsWindowsService stubs for non-Windows
- 11 tests (6 logger, 5 signal/service)
- WASM/Windows signal stubs already n/a
- All 141 tests pass (140 unit + 1 integration)
- DB: features 368/3673 complete, tests 155/3257 complete (9.6% overall)
This commit is contained in:
Joseph Doherty
2026-02-26 11:54:25 -05:00
parent f08fc5d6a7
commit b8f2f66d45
7 changed files with 581 additions and 9 deletions
@@ -0,0 +1,145 @@
// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/signal.go and server/service.go in the NATS server Go source.
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ZB.MOM.NatsNet.Server.Internal;
/// <summary>
/// Maps <see cref="ServerCommand"/> to OS signal-like behavior.
/// Mirrors <c>CommandToSignal</c> and <c>ProcessSignal</c> from signal.go.
/// In .NET, signal sending is replaced by process-level signaling on Unix.
/// </summary>
public static class SignalHandler
{
private static string _processName = "nats-server";
/// <summary>
/// Sets the process name used for resolving PIDs.
/// Mirrors <c>SetProcessName</c> in signal.go.
/// </summary>
public static void SetProcessName(string name) => _processName = name;
/// <summary>
/// Sends a signal command to a running NATS server process.
/// On Unix, maps commands to kill signals.
/// On Windows, this is a no-op (service manager handles signals).
/// Mirrors <c>ProcessSignal</c> in signal.go.
/// </summary>
public static Exception? ProcessSignal(ServerCommand command, string pidExpr = "")
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return new PlatformNotSupportedException("Signal processing not supported on Windows; use service manager.");
try
{
List<int> pids;
if (string.IsNullOrEmpty(pidExpr))
{
pids = ResolvePids();
if (pids.Count == 0)
return new InvalidOperationException("no nats-server processes found");
}
else
{
if (int.TryParse(pidExpr, out var pid))
pids = [pid];
else
return new InvalidOperationException($"invalid pid: {pidExpr}");
}
var signal = CommandToUnixSignal(command);
foreach (var pid in pids)
Process.GetProcessById(pid).Kill(signal == UnixSignal.SigKill);
return null;
}
catch (Exception ex)
{
return ex;
}
}
/// <summary>
/// Resolves PIDs of running nats-server processes via pgrep.
/// Mirrors <c>resolvePids</c> in signal.go.
/// </summary>
public static List<int> ResolvePids()
{
var pids = new List<int>();
try
{
var psi = new ProcessStartInfo("pgrep", _processName)
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
};
using var proc = Process.Start(psi);
if (proc == null) return pids;
var output = proc.StandardOutput.ReadToEnd();
proc.WaitForExit();
var currentPid = Environment.ProcessId;
foreach (var line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries))
{
if (int.TryParse(line.Trim(), out var pid) && pid != currentPid)
pids.Add(pid);
}
}
catch
{
// pgrep not available or failed
}
return pids;
}
/// <summary>
/// Maps a server command to Unix signal.
/// Mirrors <c>CommandToSignal</c> in signal.go.
/// </summary>
public static UnixSignal CommandToUnixSignal(ServerCommand command) => command switch
{
ServerCommand.Stop => UnixSignal.SigKill,
ServerCommand.Quit => UnixSignal.SigInt,
ServerCommand.Reopen => UnixSignal.SigUsr1,
ServerCommand.Reload => UnixSignal.SigHup,
_ => throw new ArgumentOutOfRangeException(nameof(command), $"unknown command: {command}"),
};
/// <summary>
/// Runs the server (non-Windows). Mirrors <c>Run</c> in service.go.
/// </summary>
public static void Run(Action startServer) => startServer();
/// <summary>
/// Returns false on non-Windows. Mirrors <c>isWindowsService</c>.
/// </summary>
public static bool IsWindowsService() => false;
}
/// <summary>Unix signal codes for NATS command mapping.</summary>
public enum UnixSignal
{
SigInt = 2,
SigKill = 9,
SigUsr1 = 10,
SigHup = 1,
SigUsr2 = 12,
SigTerm = 15,
}