From df39ebdc58e7694bcdd192b54ebdbc82451e31a6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 22 Feb 2026 23:52:49 -0500 Subject: [PATCH] feat: add signal handling (SIGTERM, SIGUSR2, SIGHUP) and CLI stubs --- src/NATS.Server.Host/Program.cs | 23 +++++++++++++--- src/NATS.Server/NatsServer.cs | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/NATS.Server.Host/Program.cs b/src/NATS.Server.Host/Program.cs index bdb6827..4dde566 100644 --- a/src/NATS.Server.Host/Program.cs +++ b/src/NATS.Server.Host/Program.cs @@ -32,6 +32,15 @@ for (int i = 0; i < args.Length; i++) case "--https_port" when i + 1 < args.Length: options.MonitorHttpsPort = int.Parse(args[++i]); break; + case "-c" when i + 1 < args.Length: + options.ConfigFile = args[++i]; + break; + case "--pid" when i + 1 < args.Length: + options.PidFile = args[++i]; + break; + case "--ports_file_dir" when i + 1 < args.Length: + options.PortsFileDir = args[++i]; + break; case "--tls": break; case "--tlscert" when i + 1 < args.Length: @@ -50,18 +59,24 @@ for (int i = 0; i < args.Length; i++) } using var loggerFactory = new Serilog.Extensions.Logging.SerilogLoggerFactory(Log.Logger); -var server = new NatsServer(options, loggerFactory); +using var server = new NatsServer(options, loggerFactory); -var cts = new CancellationTokenSource(); +// Register signal handlers +server.HandleSignals(); + +// Ctrl+C triggers graceful shutdown Console.CancelKeyPress += (_, e) => { e.Cancel = true; - cts.Cancel(); + Log.Information("Trapped SIGINT signal"); + _ = Task.Run(async () => await server.ShutdownAsync()); }; try { - await server.StartAsync(cts.Token); + _ = server.StartAsync(CancellationToken.None); + await server.WaitForReadyAsync(); + server.WaitForShutdown(); } catch (OperationCanceledException) { diff --git a/src/NATS.Server/NatsServer.cs b/src/NATS.Server/NatsServer.cs index f21f0c2..1222559 100644 --- a/src/NATS.Server/NatsServer.cs +++ b/src/NATS.Server/NatsServer.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Net; using System.Net.Security; using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Text; using Microsoft.Extensions.Logging; @@ -42,6 +43,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable private int _lameDuck; + private readonly List _signalRegistrations = []; + private string? _portsFilePath; private static readonly TimeSpan AcceptMinSleep = TimeSpan.FromMilliseconds(10); @@ -187,6 +190,50 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable await ShutdownAsync(); } + /// + /// Registers Unix signal handlers. + /// SIGTERM → shutdown, SIGUSR2 → lame duck, SIGUSR1 → log reopen (stub), SIGHUP → reload (stub). + /// + public void HandleSignals() + { + _signalRegistrations.Add(PosixSignalRegistration.Create(PosixSignal.SIGTERM, ctx => + { + ctx.Cancel = true; + _logger.LogInformation("Trapped SIGTERM signal"); + _ = Task.Run(async () => await ShutdownAsync()); + })); + + _signalRegistrations.Add(PosixSignalRegistration.Create(PosixSignal.SIGQUIT, ctx => + { + ctx.Cancel = true; + _logger.LogInformation("Trapped SIGQUIT signal"); + _ = Task.Run(async () => await ShutdownAsync()); + })); + + _signalRegistrations.Add(PosixSignalRegistration.Create(PosixSignal.SIGHUP, ctx => + { + ctx.Cancel = true; + _logger.LogWarning("Trapped SIGHUP signal — config reload not yet supported"); + })); + + // SIGUSR1 and SIGUSR2 only on non-Windows + if (!OperatingSystem.IsWindows()) + { + _signalRegistrations.Add(PosixSignalRegistration.Create((PosixSignal)10, ctx => + { + ctx.Cancel = true; + _logger.LogWarning("Trapped SIGUSR1 signal — log reopen not yet supported"); + })); + + _signalRegistrations.Add(PosixSignalRegistration.Create((PosixSignal)12, ctx => + { + ctx.Cancel = true; + _logger.LogInformation("Trapped SIGUSR2 signal — entering lame duck mode"); + _ = Task.Run(async () => await LameDuckShutdownAsync()); + })); + } + } + public NatsServer(NatsOptions options, ILoggerFactory loggerFactory) { _options = options; @@ -561,6 +608,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable { if (!IsShuttingDown) ShutdownAsync().GetAwaiter().GetResult(); + foreach (var reg in _signalRegistrations) + reg.Dispose(); _quitCts.Dispose(); _tlsRateLimiter?.Dispose(); _listener?.Dispose();