feat: port session 04 — Logging, Signals & Services
- NatsLogger.cs: INatsLogger interface (Noticef/Warnf/Fatalf/Errorf/Debugf/Tracef), ServerLogging state class with atomic debug/trace flags, rate-limited logging (RateLimitWarnf/RateLimitDebugf), error variants (Errors/Errorc/Errorsc), MicrosoftLoggerAdapter bridging to ILogger - SignalHandler.cs: ProcessSignal (Unix kill via Process), CommandToUnixSignal mapping (Stop→SIGKILL, Quit→SIGINT, Reopen→SIGUSR1, Reload→SIGHUP), ResolvePids via pgrep, SetProcessName, Run/IsWindowsService stubs for non-Windows - 11 tests (6 logger, 5 signal/service) - WASM/Windows signal stubs already n/a - All 141 tests pass (140 unit + 1 integration) - DB: features 368/3673 complete, tests 155/3257 complete (9.6% overall)
This commit is contained in:
187
dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs
Normal file
187
dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// NATS server Logger interface.
|
||||
/// Mirrors the Go <c>Logger</c> interface in log.go.
|
||||
/// In .NET we bridge to <see cref="ILogger"/> from Microsoft.Extensions.Logging.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server logging state. Encapsulates the logger, debug/trace flags, and rate-limiting.
|
||||
/// Mirrors the logging fields of Go's <c>Server</c> struct (logging struct + rateLimitLogging sync.Map).
|
||||
/// </summary>
|
||||
public sealed class ServerLogging
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private INatsLogger? _logger;
|
||||
private int _debug;
|
||||
private int _trace;
|
||||
private int _traceSysAcc;
|
||||
private readonly ConcurrentDictionary<string, DateTime> _rateLimitMap = new();
|
||||
|
||||
/// <summary>Gets the current logger (thread-safe).</summary>
|
||||
public INatsLogger? GetLogger()
|
||||
{
|
||||
lock (_lock) return _logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the logger with debug/trace flags.
|
||||
/// Mirrors <c>Server.SetLoggerV2</c>.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the logger. Mirrors <c>Server.SetLogger</c>.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>Executes a log call under the read lock.</summary>
|
||||
public void ExecuteLogCall(Action<INatsLogger> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate-limited warning log. Only the first occurrence of each formatted statement is logged.
|
||||
/// Mirrors <c>Server.RateLimitWarnf</c>.
|
||||
/// </summary>
|
||||
public void RateLimitWarnf(string format, params object[] args)
|
||||
{
|
||||
var statement = string.Format(format, args);
|
||||
if (!_rateLimitMap.TryAdd(statement, DateTime.UtcNow)) return;
|
||||
Warnf("{0}", statement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate-limited debug log. Only the first occurrence of each formatted statement is logged.
|
||||
/// Mirrors <c>Server.RateLimitDebugf</c>.
|
||||
/// </summary>
|
||||
public void RateLimitDebugf(string format, params object[] args)
|
||||
{
|
||||
var statement = string.Format(format, args);
|
||||
if (!_rateLimitMap.TryAdd(statement, DateTime.UtcNow)) return;
|
||||
Debugf("{0}", statement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate-limited format warning. Only the first occurrence of each format string is logged.
|
||||
/// Mirrors <c>Server.rateLimitFormatWarnf</c>.
|
||||
/// </summary>
|
||||
internal void RateLimitFormatWarnf(string format, params object[] args)
|
||||
{
|
||||
if (!_rateLimitMap.TryAdd(format, DateTime.UtcNow)) return;
|
||||
var statement = string.Format(format, args);
|
||||
Warnf("{0}", statement);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adapter that bridges <see cref="INatsLogger"/> to <see cref="ILogger"/>.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
145
dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs
Normal file
145
dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs
Normal file
@@ -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;
|
||||
|
||||
/// <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 static string _processName = "nats-server";
|
||||
|
||||
/// <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
|
||||
{
|
||||
List<int> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(command), $"unknown command: {command}"),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Runs the server (non-Windows). Mirrors <c>Run</c> in service.go.
|
||||
/// </summary>
|
||||
public static void Run(Action startServer) => startServer();
|
||||
|
||||
/// <summary>
|
||||
/// Returns false on non-Windows. Mirrors <c>isWindowsService</c>.
|
||||
/// </summary>
|
||||
public static bool IsWindowsService() => false;
|
||||
}
|
||||
|
||||
/// <summary>Unix signal codes for NATS command mapping.</summary>
|
||||
public enum UnixSignal
|
||||
{
|
||||
SigInt = 2,
|
||||
SigKill = 9,
|
||||
SigUsr1 = 10,
|
||||
SigHup = 1,
|
||||
SigUsr2 = 12,
|
||||
SigTerm = 15,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for NatsLogger / ServerLogging — mirrors tests from server/log_test.go.
|
||||
/// </summary>
|
||||
public class NatsLoggerTests
|
||||
{
|
||||
private sealed class TestLogger : INatsLogger
|
||||
{
|
||||
public List<string> 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)}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors TestSetLogger — verify logger assignment and atomic flags.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify all log methods produce output when flags enabled.
|
||||
/// </summary>
|
||||
[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]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debug/Trace should not produce output when flags disabled.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify null logger does not throw.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NullLogger_ShouldNotThrow()
|
||||
{
|
||||
var logging = new ServerLogging();
|
||||
Should.NotThrow(() => logging.Noticef("test"));
|
||||
Should.NotThrow(() => logging.Errorf("test"));
|
||||
Should.NotThrow(() => logging.Debugf("test"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify rate-limited logging suppresses duplicate messages.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify Errors/Errorc/Errorsc convenience methods.
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for SignalHandler — mirrors tests from server/signal_test.go.
|
||||
/// </summary>
|
||||
public class SignalHandlerTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Mirrors CommandToSignal mapping tests.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors SetProcessName test.
|
||||
/// </summary>
|
||||
[Fact] // T:3155
|
||||
public void SetProcessName_ShouldNotThrow()
|
||||
{
|
||||
Should.NotThrow(() => SignalHandler.SetProcessName("test-server"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify IsWindowsService returns false on non-Windows.
|
||||
/// </summary>
|
||||
[Fact] // T:3149
|
||||
public void IsWindowsService_ShouldReturnFalse()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return; // Skip on Windows
|
||||
SignalHandler.IsWindowsService().ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors Run — service.go Run() simply invokes the start function.
|
||||
/// </summary>
|
||||
[Fact] // T:3148
|
||||
public void Run_ShouldInvokeStartAction()
|
||||
{
|
||||
var called = false;
|
||||
SignalHandler.Run(() => called = true);
|
||||
called.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ProcessSignal with invalid PID expression should return error.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user