246 lines
8.1 KiB
C#
246 lines
8.1 KiB
C#
// 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;
|
|
|
|
/// <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 const string ResolvePidError = "unable to resolve pid, try providing one";
|
|
private static string _processName = "nats-server";
|
|
internal static Func<List<int>> ResolvePidsHandler { get; set; } = ResolvePids;
|
|
internal static Func<int, UnixSignal, Exception?> SendSignalHandler { get; set; } = SendSignal;
|
|
|
|
internal static void ResetTestHooks()
|
|
{
|
|
ResolvePidsHandler = ResolvePids;
|
|
SendSignalHandler = SendSignal;
|
|
}
|
|
|
|
/// <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
|
|
{
|
|
var pids = new List<int>(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;
|
|
}
|
|
}
|
|
|
|
/// <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>(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;
|
|
}
|
|
|
|
/// <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,
|
|
ServerCommand.LameDuckMode => UnixSignal.SigUsr2,
|
|
ServerCommand.Term => UnixSignal.SigTerm,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(command), $"unknown signal \"{CommandToString(command)}\""),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Go parity alias for <see cref="CommandToUnixSignal"/>.
|
|
/// Mirrors <c>CommandToSignal</c> in signal.go.
|
|
/// </summary>
|
|
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(),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Runs the server (non-Windows). Mirrors <c>Run</c> in service.go.
|
|
/// </summary>
|
|
public static void Run(Action startServer)
|
|
{
|
|
var error = ServiceManager.Run(startServer);
|
|
if (error is not null)
|
|
throw error;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns false on non-Windows. Mirrors <c>isWindowsService</c>.
|
|
/// </summary>
|
|
public static bool IsWindowsService() => ServiceManager.IsWindowsService();
|
|
}
|
|
|
|
/// <summary>Unix signal codes for NATS command mapping.</summary>
|
|
public enum UnixSignal
|
|
{
|
|
SigInt = 2,
|
|
SigKill = 9,
|
|
SigUsr1 = 10,
|
|
SigHup = 1,
|
|
SigUsr2 = 12,
|
|
SigTerm = 15,
|
|
}
|