diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs new file mode 100644 index 0000000..b547b2c --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs @@ -0,0 +1,187 @@ +// 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/log.go in the NATS server Go source. + +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; + +namespace ZB.MOM.NatsNet.Server.Internal; + +/// +/// NATS server Logger interface. +/// Mirrors the Go Logger interface in log.go. +/// In .NET we bridge to from Microsoft.Extensions.Logging. +/// +public interface INatsLogger +{ + void Noticef(string format, params object[] args); + void Warnf(string format, params object[] args); + void Fatalf(string format, params object[] args); + void Errorf(string format, params object[] args); + void Debugf(string format, params object[] args); + void Tracef(string format, params object[] args); +} + +/// +/// Server logging state. Encapsulates the logger, debug/trace flags, and rate-limiting. +/// Mirrors the logging fields of Go's Server struct (logging struct + rateLimitLogging sync.Map). +/// +public sealed class ServerLogging +{ + private readonly object _lock = new(); + private INatsLogger? _logger; + private int _debug; + private int _trace; + private int _traceSysAcc; + private readonly ConcurrentDictionary _rateLimitMap = new(); + + /// Gets the current logger (thread-safe). + public INatsLogger? GetLogger() + { + lock (_lock) return _logger; + } + + /// + /// Sets the logger with debug/trace flags. + /// Mirrors Server.SetLoggerV2. + /// + public void SetLoggerV2(INatsLogger? logger, bool debugFlag, bool traceFlag, bool sysTrace) + { + Interlocked.Exchange(ref _debug, debugFlag ? 1 : 0); + Interlocked.Exchange(ref _trace, traceFlag ? 1 : 0); + Interlocked.Exchange(ref _traceSysAcc, sysTrace ? 1 : 0); + + lock (_lock) + { + if (_logger is IDisposable disposable) + disposable.Dispose(); + _logger = logger; + } + } + + /// + /// Sets the logger. Mirrors Server.SetLogger. + /// + public void SetLogger(INatsLogger? logger, bool debugFlag, bool traceFlag) => + SetLoggerV2(logger, debugFlag, traceFlag, false); + + public bool IsDebug => Interlocked.CompareExchange(ref _debug, 0, 0) != 0; + public bool IsTrace => Interlocked.CompareExchange(ref _trace, 0, 0) != 0; + public bool IsTraceSysAcc => Interlocked.CompareExchange(ref _traceSysAcc, 0, 0) != 0; + + /// Executes a log call under the read lock. + public void ExecuteLogCall(Action action) + { + INatsLogger? logger; + lock (_lock) logger = _logger; + if (logger == null) return; + action(logger); + } + + // ---- Convenience methods ---- + + public void Noticef(string format, params object[] args) => + ExecuteLogCall(l => l.Noticef(format, args)); + + public void Errorf(string format, params object[] args) => + ExecuteLogCall(l => l.Errorf(format, args)); + + public void Errors(object scope, Exception e) => + ExecuteLogCall(l => l.Errorf("{0} - {1}", scope, e.Message)); + + public void Errorc(string ctx, Exception e) => + ExecuteLogCall(l => l.Errorf("{0}: {1}", ctx, e.Message)); + + public void Errorsc(object scope, string ctx, Exception e) => + ExecuteLogCall(l => l.Errorf("{0} - {1}: {2}", scope, ctx, e.Message)); + + public void Warnf(string format, params object[] args) => + ExecuteLogCall(l => l.Warnf(format, args)); + + public void Fatalf(string format, params object[] args) => + ExecuteLogCall(l => l.Fatalf(format, args)); + + public void Debugf(string format, params object[] args) + { + if (!IsDebug) return; + ExecuteLogCall(l => l.Debugf(format, args)); + } + + public void Tracef(string format, params object[] args) + { + if (!IsTrace) return; + ExecuteLogCall(l => l.Tracef(format, args)); + } + + /// + /// Rate-limited warning log. Only the first occurrence of each formatted statement is logged. + /// Mirrors Server.RateLimitWarnf. + /// + public void RateLimitWarnf(string format, params object[] args) + { + var statement = string.Format(format, args); + if (!_rateLimitMap.TryAdd(statement, DateTime.UtcNow)) return; + Warnf("{0}", statement); + } + + /// + /// Rate-limited debug log. Only the first occurrence of each formatted statement is logged. + /// Mirrors Server.RateLimitDebugf. + /// + public void RateLimitDebugf(string format, params object[] args) + { + var statement = string.Format(format, args); + if (!_rateLimitMap.TryAdd(statement, DateTime.UtcNow)) return; + Debugf("{0}", statement); + } + + /// + /// Rate-limited format warning. Only the first occurrence of each format string is logged. + /// Mirrors Server.rateLimitFormatWarnf. + /// + internal void RateLimitFormatWarnf(string format, params object[] args) + { + if (!_rateLimitMap.TryAdd(format, DateTime.UtcNow)) return; + var statement = string.Format(format, args); + Warnf("{0}", statement); + } +} + +/// +/// Adapter that bridges to . +/// +public sealed class MicrosoftLoggerAdapter : INatsLogger +{ + private readonly ILogger _logger; + + public MicrosoftLoggerAdapter(ILogger logger) => _logger = logger; + + public void Noticef(string format, params object[] args) => + _logger.LogInformation(format, args); + + public void Warnf(string format, params object[] args) => + _logger.LogWarning(format, args); + + public void Fatalf(string format, params object[] args) => + _logger.LogCritical(format, args); + + public void Errorf(string format, params object[] args) => + _logger.LogError(format, args); + + public void Debugf(string format, params object[] args) => + _logger.LogDebug(format, args); + + public void Tracef(string format, params object[] args) => + _logger.LogTrace(format, args); +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs new file mode 100644 index 0000000..ef55123 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs @@ -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; + +/// +/// Maps to OS signal-like behavior. +/// Mirrors CommandToSignal and ProcessSignal from signal.go. +/// In .NET, signal sending is replaced by process-level signaling on Unix. +/// +public static class SignalHandler +{ + private static string _processName = "nats-server"; + + /// + /// Sets the process name used for resolving PIDs. + /// Mirrors SetProcessName in signal.go. + /// + public static void SetProcessName(string name) => _processName = name; + + /// + /// 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 ProcessSignal in signal.go. + /// + 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 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; + } + } + + /// + /// Resolves PIDs of running nats-server processes via pgrep. + /// Mirrors resolvePids in signal.go. + /// + public static List ResolvePids() + { + var pids = new List(); + 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; + } + + /// + /// Maps a server command to Unix signal. + /// Mirrors CommandToSignal in signal.go. + /// + 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}"), + }; + + /// + /// Runs the server (non-Windows). Mirrors Run in service.go. + /// + public static void Run(Action startServer) => startServer(); + + /// + /// Returns false on non-Windows. Mirrors isWindowsService. + /// + public static bool IsWindowsService() => false; +} + +/// Unix signal codes for NATS command mapping. +public enum UnixSignal +{ + SigInt = 2, + SigKill = 9, + SigUsr1 = 10, + SigHup = 1, + SigUsr2 = 12, + SigTerm = 15, +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/NatsLoggerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/NatsLoggerTests.cs new file mode 100644 index 0000000..f5466c7 --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/NatsLoggerTests.cs @@ -0,0 +1,131 @@ +// Copyright 2012-2025 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; + +/// +/// Tests for NatsLogger / ServerLogging — mirrors tests from server/log_test.go. +/// +public class NatsLoggerTests +{ + private sealed class TestLogger : INatsLogger + { + public List Messages { get; } = []; + + public void Noticef(string format, params object[] args) => Messages.Add($"[INF] {string.Format(format, args)}"); + public void Warnf(string format, params object[] args) => Messages.Add($"[WRN] {string.Format(format, args)}"); + public void Fatalf(string format, params object[] args) => Messages.Add($"[FTL] {string.Format(format, args)}"); + public void Errorf(string format, params object[] args) => Messages.Add($"[ERR] {string.Format(format, args)}"); + public void Debugf(string format, params object[] args) => Messages.Add($"[DBG] {string.Format(format, args)}"); + public void Tracef(string format, params object[] args) => Messages.Add($"[TRC] {string.Format(format, args)}"); + } + + /// + /// Mirrors TestSetLogger — verify logger assignment and atomic flags. + /// + [Fact] // T:2017 + public void SetLogger_ShouldSetLoggerAndFlags() + { + var logging = new ServerLogging(); + var testLog = new TestLogger(); + + logging.SetLoggerV2(testLog, true, true, false); + logging.IsDebug.ShouldBeTrue(); + logging.IsTrace.ShouldBeTrue(); + logging.IsTraceSysAcc.ShouldBeFalse(); + logging.GetLogger().ShouldBe(testLog); + } + + /// + /// Verify all log methods produce output when flags enabled. + /// + [Fact] // T:2017 (continuation) + public void AllLogMethods_ShouldProduceOutput() + { + var logging = new ServerLogging(); + var testLog = new TestLogger(); + logging.SetLoggerV2(testLog, true, true, false); + + logging.Noticef("notice {0}", "test"); + logging.Errorf("error {0}", "test"); + logging.Warnf("warn {0}", "test"); + logging.Fatalf("fatal {0}", "test"); + logging.Debugf("debug {0}", "test"); + logging.Tracef("trace {0}", "test"); + + testLog.Messages.Count.ShouldBe(6); + testLog.Messages[0].ShouldContain("[INF]"); + testLog.Messages[1].ShouldContain("[ERR]"); + testLog.Messages[4].ShouldContain("[DBG]"); + testLog.Messages[5].ShouldContain("[TRC]"); + } + + /// + /// Debug/Trace should not produce output when flags disabled. + /// + [Fact] // T:2017 (continuation) + public void DebugTrace_ShouldBeNoOpWhenDisabled() + { + var logging = new ServerLogging(); + var testLog = new TestLogger(); + logging.SetLoggerV2(testLog, false, false, false); + + logging.Debugf("debug"); + logging.Tracef("trace"); + + testLog.Messages.ShouldBeEmpty(); + } + + /// + /// Verify null logger does not throw. + /// + [Fact] + public void NullLogger_ShouldNotThrow() + { + var logging = new ServerLogging(); + Should.NotThrow(() => logging.Noticef("test")); + Should.NotThrow(() => logging.Errorf("test")); + Should.NotThrow(() => logging.Debugf("test")); + } + + /// + /// Verify rate-limited logging suppresses duplicate messages. + /// + [Fact] // T:2017 (RateLimitWarnf behavior) + public void RateLimitWarnf_ShouldSuppressDuplicates() + { + var logging = new ServerLogging(); + var testLog = new TestLogger(); + logging.SetLoggerV2(testLog, false, false, false); + + logging.RateLimitWarnf("duplicate message"); + logging.RateLimitWarnf("duplicate message"); + logging.RateLimitWarnf("different message"); + + // Should only log 2 unique messages, not 3. + testLog.Messages.Count.ShouldBe(2); + } + + /// + /// Verify Errors/Errorc/Errorsc convenience methods. + /// + [Fact] + public void ErrorVariants_ShouldFormatCorrectly() + { + var logging = new ServerLogging(); + var testLog = new TestLogger(); + logging.SetLoggerV2(testLog, false, false, false); + + logging.Errors("client", new Exception("conn reset")); + logging.Errorc("TLS", new Exception("cert expired")); + logging.Errorsc("route", "cluster", new Exception("timeout")); + + testLog.Messages.Count.ShouldBe(3); + testLog.Messages[0].ShouldContain("client - conn reset"); + testLog.Messages[1].ShouldContain("TLS: cert expired"); + testLog.Messages[2].ShouldContain("route - cluster: timeout"); + } +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/SignalHandlerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/SignalHandlerTests.cs new file mode 100644 index 0000000..cfc3400 --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/SignalHandlerTests.cs @@ -0,0 +1,70 @@ +// Copyright 2012-2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using System.Runtime.InteropServices; +using Shouldly; +using ZB.MOM.NatsNet.Server.Internal; + +namespace ZB.MOM.NatsNet.Server.Tests.Internal; + +/// +/// Tests for SignalHandler — mirrors tests from server/signal_test.go. +/// +public class SignalHandlerTests +{ + /// + /// Mirrors CommandToSignal mapping tests. + /// + [Fact] // T:3158 + public void CommandToUnixSignal_ShouldMapCorrectly() + { + SignalHandler.CommandToUnixSignal(ServerCommand.Stop).ShouldBe(UnixSignal.SigKill); + SignalHandler.CommandToUnixSignal(ServerCommand.Quit).ShouldBe(UnixSignal.SigInt); + SignalHandler.CommandToUnixSignal(ServerCommand.Reopen).ShouldBe(UnixSignal.SigUsr1); + SignalHandler.CommandToUnixSignal(ServerCommand.Reload).ShouldBe(UnixSignal.SigHup); + } + + /// + /// Mirrors SetProcessName test. + /// + [Fact] // T:3155 + public void SetProcessName_ShouldNotThrow() + { + Should.NotThrow(() => SignalHandler.SetProcessName("test-server")); + } + + /// + /// Verify IsWindowsService returns false on non-Windows. + /// + [Fact] // T:3149 + public void IsWindowsService_ShouldReturnFalse() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; // Skip on Windows + SignalHandler.IsWindowsService().ShouldBeFalse(); + } + + /// + /// Mirrors Run — service.go Run() simply invokes the start function. + /// + [Fact] // T:3148 + public void Run_ShouldInvokeStartAction() + { + var called = false; + SignalHandler.Run(() => called = true); + called.ShouldBeTrue(); + } + + /// + /// ProcessSignal with invalid PID expression should return error. + /// + [Fact] // T:3157 + public void ProcessSignal_InvalidPid_ShouldReturnError() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; // Skip on Windows + + var err = SignalHandler.ProcessSignal(ServerCommand.Stop, "not-a-pid"); + err.ShouldNotBeNull(); + } +} diff --git a/porting.db b/porting.db index 3c7bf3a..ea5a0b7 100644 Binary files a/porting.db and b/porting.db differ diff --git a/reports/current.md b/reports/current.md index 9062e7b..9a95428 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-26 16:51:01 UTC +Generated: 2026-02-26 16:54:25 UTC ## Modules (12 total) @@ -13,19 +13,19 @@ Generated: 2026-02-26 16:51:01 UTC | Status | Count | |--------|-------| -| complete | 344 | +| complete | 368 | | n_a | 82 | -| not_started | 3180 | -| stub | 67 | +| not_started | 3155 | +| stub | 68 | ## Unit Tests (3257 total) | Status | Count | |--------|-------| -| complete | 148 | -| n_a | 49 | -| not_started | 2980 | -| stub | 80 | +| complete | 155 | +| n_a | 50 | +| not_started | 2953 | +| stub | 99 | ## Library Mappings (36 total) @@ -36,4 +36,4 @@ Generated: 2026-02-26 16:51:01 UTC ## Overall Progress -**634/6942 items complete (9.1%)** +**666/6942 items complete (9.6%)** diff --git a/reports/report_f08fc5d.md b/reports/report_f08fc5d.md new file mode 100644 index 0000000..9a95428 --- /dev/null +++ b/reports/report_f08fc5d.md @@ -0,0 +1,39 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-26 16:54:25 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| complete | 11 | +| not_started | 1 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| complete | 368 | +| n_a | 82 | +| not_started | 3155 | +| stub | 68 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| complete | 155 | +| n_a | 50 | +| not_started | 2953 | +| stub | 99 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**666/6942 items complete (9.6%)**