From e57605f090102a9c8896b425ae68174960686182 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 22 Feb 2026 23:50:22 -0500 Subject: [PATCH] feat: add PID file and ports file support --- src/NATS.Server/NatsServer.cs | 71 ++++++++++++++++++++++++-- tests/NATS.Server.Tests/ServerTests.cs | 59 +++++++++++++++++++++ 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/NATS.Server/NatsServer.cs b/src/NATS.Server/NatsServer.cs index 88ae76c..f21f0c2 100644 --- a/src/NATS.Server/NatsServer.cs +++ b/src/NATS.Server/NatsServer.cs @@ -42,10 +42,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable private int _lameDuck; - // Used by future ports file implementation -#pragma warning disable CS0169 // Field is never used private string? _portsFilePath; -#pragma warning restore CS0169 private static readonly TimeSpan AcceptMinSleep = TimeSpan.FromMilliseconds(10); private static readonly TimeSpan AcceptMaxSleep = TimeSpan.FromSeconds(1); @@ -108,6 +105,9 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable if (_monitorServer != null) await _monitorServer.DisposeAsync(); + DeletePidFile(); + DeletePortsFile(); + _logger.LogInformation("Server Exiting.."); _shutdownComplete.TrySetResult(); } @@ -263,6 +263,9 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable await _monitorServer.StartAsync(linked.Token); } + WritePidFile(); + WritePortsFile(); + var tmpDelay = AcceptMinSleep; try @@ -492,6 +495,68 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable client.Account?.RemoveClient(client.Id); } + private void WritePidFile() + { + if (string.IsNullOrEmpty(_options.PidFile)) return; + try + { + File.WriteAllText(_options.PidFile, Environment.ProcessId.ToString()); + _logger.LogDebug("Wrote PID file {PidFile}", _options.PidFile); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error writing PID file {PidFile}", _options.PidFile); + } + } + + private void DeletePidFile() + { + if (string.IsNullOrEmpty(_options.PidFile)) return; + try + { + if (File.Exists(_options.PidFile)) + File.Delete(_options.PidFile); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting PID file {PidFile}", _options.PidFile); + } + } + + private void WritePortsFile() + { + if (string.IsNullOrEmpty(_options.PortsFileDir)) return; + try + { + var exeName = Path.GetFileNameWithoutExtension(Environment.ProcessPath ?? "nats-server"); + var fileName = $"{exeName}_{Environment.ProcessId}.ports"; + _portsFilePath = Path.Combine(_options.PortsFileDir, fileName); + + var ports = new { client = _options.Port, monitor = _options.MonitorPort > 0 ? _options.MonitorPort : (int?)null }; + var json = System.Text.Json.JsonSerializer.Serialize(ports); + File.WriteAllText(_portsFilePath, json); + _logger.LogDebug("Wrote ports file {PortsFile}", _portsFilePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error writing ports file to {PortsFileDir}", _options.PortsFileDir); + } + } + + private void DeletePortsFile() + { + if (_portsFilePath == null) return; + try + { + if (File.Exists(_portsFilePath)) + File.Delete(_portsFilePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting ports file {PortsFile}", _portsFilePath); + } + } + public void Dispose() { if (!IsShuttingDown) diff --git a/tests/NATS.Server.Tests/ServerTests.cs b/tests/NATS.Server.Tests/ServerTests.cs index 1897fb1..dd90668 100644 --- a/tests/NATS.Server.Tests/ServerTests.cs +++ b/tests/NATS.Server.Tests/ServerTests.cs @@ -842,3 +842,62 @@ public class LameDuckTests } } } + +public class PidFileTests : IDisposable +{ + private readonly string _tempDir = Path.Combine(Path.GetTempPath(), $"nats-test-{Guid.NewGuid():N}"); + + public PidFileTests() => Directory.CreateDirectory(_tempDir); + + public void Dispose() + { + if (Directory.Exists(_tempDir)) + Directory.Delete(_tempDir, recursive: true); + } + + private static int GetFreePort() + { + using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + sock.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return ((IPEndPoint)sock.LocalEndPoint!).Port; + } + + [Fact] + public async Task Server_writes_pid_file_on_startup() + { + var pidFile = Path.Combine(_tempDir, "nats.pid"); + var port = GetFreePort(); + var server = new NatsServer(new NatsOptions { Port = port, PidFile = pidFile }, NullLoggerFactory.Instance); + _ = server.StartAsync(CancellationToken.None); + await server.WaitForReadyAsync(); + + File.Exists(pidFile).ShouldBeTrue(); + var content = await File.ReadAllTextAsync(pidFile); + int.Parse(content).ShouldBe(Environment.ProcessId); + + await server.ShutdownAsync(); + File.Exists(pidFile).ShouldBeFalse(); + + server.Dispose(); + } + + [Fact] + public async Task Server_writes_ports_file_on_startup() + { + var port = GetFreePort(); + var server = new NatsServer(new NatsOptions { Port = port, PortsFileDir = _tempDir }, NullLoggerFactory.Instance); + _ = server.StartAsync(CancellationToken.None); + await server.WaitForReadyAsync(); + + var portsFiles = Directory.GetFiles(_tempDir, "*.ports"); + portsFiles.Length.ShouldBe(1); + + var content = await File.ReadAllTextAsync(portsFiles[0]); + content.ShouldContain($"\"client\":{port}"); + + await server.ShutdownAsync(); + Directory.GetFiles(_tempDir, "*.ports").Length.ShouldBe(0); + + server.Dispose(); + } +}