// 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; using System.Text; 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 const string ResolvePidError = "unable to resolve pid, try providing one"; private static string _processName = "nats-server"; internal static Func> ResolvePidsHandler { get; set; } = ResolvePids; internal static Func SendSignalHandler { get; set; } = SendSignal; internal static void ResetTestHooks() { ResolvePidsHandler = ResolvePids; SendSignalHandler = SendSignal; } /// /// 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 { var pids = new List(1); var pidStr = pidExpr.TrimEnd('*'); var isGlob = pidExpr.EndsWith('*'); if (!string.IsNullOrEmpty(pidStr)) { if (!int.TryParse(pidStr, out var pid)) return new InvalidOperationException($"invalid pid: {pidStr}"); pids.Add(pid); } if (string.IsNullOrEmpty(pidStr) || isGlob) pids = ResolvePidsHandler(); if (pids.Count > 1 && !isGlob) { var sb = new StringBuilder($"multiple {_processName} processes running:"); foreach (var p in pids) sb.Append('\n').Append(p); return new InvalidOperationException(sb.ToString()); } if (pids.Count == 0) return new InvalidOperationException($"no {_processName} processes running"); UnixSignal signal; try { signal = CommandToUnixSignal(command); } catch (Exception ex) { return ex; } var errBuilder = new StringBuilder(); foreach (var pid in pids) { var pidText = pid.ToString(); if (pidStr.Length > 0 && pidText != pidStr) { if (!isGlob || !pidText.StartsWith(pidStr, StringComparison.Ordinal)) continue; } var err = SendSignalHandler(pid, signal); if (err != null) { errBuilder .Append('\n') .Append("signal \"") .Append(CommandToString(command)) .Append("\" ") .Append(pid) .Append(": ") .Append(err.Message); } } if (errBuilder.Length > 0) return new InvalidOperationException(errBuilder.ToString()); 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(8); try { var psi = new ProcessStartInfo("pgrep", _processName) { RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true, }; using var proc = Process.Start(psi); if (proc == null) throw new InvalidOperationException(ResolvePidError); var output = proc.StandardOutput.ReadToEnd(); proc.WaitForExit(); if (proc.ExitCode != 0) return pids; var currentPid = Environment.ProcessId; foreach (var line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries)) { if (!int.TryParse(line.Trim(), out var pid)) throw new InvalidOperationException(ResolvePidError); if (pid != currentPid) pids.Add(pid); } } catch (InvalidOperationException ex) when (ex.Message == ResolvePidError) { throw; } catch { throw new InvalidOperationException(ResolvePidError); } 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, ServerCommand.LameDuckMode => UnixSignal.SigUsr2, ServerCommand.Term => UnixSignal.SigTerm, _ => throw new ArgumentOutOfRangeException(nameof(command), $"unknown signal \"{CommandToString(command)}\""), }; /// /// Go parity alias for . /// Mirrors CommandToSignal in signal.go. /// public static UnixSignal CommandToSignal(ServerCommand command) => CommandToUnixSignal(command); private static Exception? SendSignal(int pid, UnixSignal signal) { try { Process.GetProcessById(pid).Kill(signal == UnixSignal.SigKill); return null; } catch (Exception ex) { return ex; } } private static string CommandToString(ServerCommand command) => command switch { ServerCommand.Stop => "stop", ServerCommand.Quit => "quit", ServerCommand.Reopen => "reopen", ServerCommand.Reload => "reload", ServerCommand.LameDuckMode => "ldm", ServerCommand.Term => "term", _ => command.ToString().ToLowerInvariant(), }; /// /// Runs the server (non-Windows). Mirrors Run in service.go. /// public static void Run(Action startServer) { var error = ServiceManager.Run(startServer); if (error is not null) throw error; } /// /// Returns false on non-Windows. Mirrors isWindowsService. /// public static bool IsWindowsService() => ServiceManager.IsWindowsService(); } /// Unix signal codes for NATS command mapping. public enum UnixSignal { SigInt = 2, SigKill = 9, SigUsr1 = 10, SigHup = 1, SigUsr2 = 12, SigTerm = 15, }