From da9d3fc42036e6fb93b3eda8400f25e19831b8c7 Mon Sep 17 00:00:00 2001 From: Levi Gillis Date: Wed, 25 Mar 2026 14:28:37 +0100 Subject: [PATCH 1/3] Add ability to send posix/ansi signals --- src/Renci.SshNet/CommandSignal.cs | 74 +++++++++++++++++++++++++++++++ src/Renci.SshNet/SshCommand.cs | 67 ++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 src/Renci.SshNet/CommandSignal.cs diff --git a/src/Renci.SshNet/CommandSignal.cs b/src/Renci.SshNet/CommandSignal.cs new file mode 100644 index 000000000..5d37c24c6 --- /dev/null +++ b/src/Renci.SshNet/CommandSignal.cs @@ -0,0 +1,74 @@ +namespace Renci.SshNet +{ + /// + /// The ssh compatible POSIX/ANSI signals with their libc compatible values. + /// +#pragma warning disable CA1720 // Identifier contains type name + public enum CommandSignal + { + /// + /// Hangup (POSIX). + /// + HUP = 1, + + /// + /// Interrupt (ANSI). + /// + INT = 2, + + /// + /// Quit (POSIX). + /// + QUIT = 3, + + /// + /// Illegal instruction (ANSI). + /// + ILL = 4, + + /// + /// Abort (ANSI). + /// + ABRT = 6, + + /// + /// Floating-point exception (ANSI). + /// + FPE = 8, + + /// + /// Kill, unblockable (POSIX). + /// + KILL = 9, + + /// + /// User-defined signal 1 (POSIX). + /// + USR1 = 10, + + /// + /// Segmentation violation (ANSI). + /// + SEGV = 11, + + /// + /// User-defined signal 2 (POSIX). + /// + USR2 = 12, + + /// + /// Broken pipe (POSIX). + /// + PIPE = 13, + + /// + /// Alarm clock (POSIX). + /// + ALRM = 14, + + /// + /// Termination (ANSI). + /// + TERM = 15, + } +} diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index ce1042244..460e6c30e 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -478,6 +478,73 @@ public void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500) } } + private static string? GetSignalName(CommandSignal signal) + { +#if NETCOREAPP + return Enum.GetName(signal); +#else + + // Boxes signal, but Enum.GetName does not have a non-boxing overload prior to .NET Core. + return Enum.GetName(typeof(CommandSignal), signal); +#endif + } + + /// + /// Tries to send a POSIX/ANSI signal to the remote process executing the command, such as SIGINT or SIGTERM. + /// + /// The signal to send + /// If the signal was sent. + public bool TrySendSignal(CommandSignal signal) + { + var signalName = GetSignalName(signal); + if (signalName is null) + { + return false; + } + + if (_tcs is null || _tcs.Task.IsCompleted || _channel?.IsOpen != true) + { + return false; + } + + try + { + // Try to send the cancellation signal. + return _channel.SendSignalRequest(signalName); + } + catch (Exception) + { + // Exception can be ignored since we are in a Try method + // Possible exceptions here: InvalidOperationException, SshConnectionException, SshOperationTimeoutException + } + + return false; + } + + /// + /// Tries to send a POSIX/ANSI signal to the remote process executing the command, such as SIGINT or SIGTERM. + /// + /// The signal to send + /// Signal was not a valid CommandSignal. + /// The client is not connected. + /// The operation timed out. + /// The size of the packet exceeds the maximum size defined by the protocol. + /// Command has not been started. + public void SendSignal(CommandSignal signal) + { + var signalName = GetSignalName(signal); + if (signalName is null) + { + throw new ArgumentException("Signal was not a valid CommandSignal."); + } + if (_tcs is null || _tcs.Task.IsCompleted || _channel?.IsOpen != true) + { + throw new InvalidOperationException("Command has not been started."); + } + + _ = _channel.SendSignalRequest(signalName); + } + /// /// Executes the command specified by . /// From 45794dc53d297a2cd3b405ecda8d43856311dff5 Mon Sep 17 00:00:00 2001 From: Levi Gillis Date: Fri, 3 Apr 2026 15:33:27 +0200 Subject: [PATCH 2/3] Fix formatting and warnigns --- src/Renci.SshNet/SshCommand.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index 460e6c30e..48864c5b6 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -492,7 +492,7 @@ public void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500) /// /// Tries to send a POSIX/ANSI signal to the remote process executing the command, such as SIGINT or SIGTERM. /// - /// The signal to send + /// The signal to send. /// If the signal was sent. public bool TrySendSignal(CommandSignal signal) { @@ -524,7 +524,7 @@ public bool TrySendSignal(CommandSignal signal) /// /// Tries to send a POSIX/ANSI signal to the remote process executing the command, such as SIGINT or SIGTERM. /// - /// The signal to send + /// The signal to send. /// Signal was not a valid CommandSignal. /// The client is not connected. /// The operation timed out. @@ -537,6 +537,7 @@ public void SendSignal(CommandSignal signal) { throw new ArgumentException("Signal was not a valid CommandSignal."); } + if (_tcs is null || _tcs.Task.IsCompleted || _channel?.IsOpen != true) { throw new InvalidOperationException("Command has not been started."); From 7fd2ea2c91d1754825335b94175f5b73a163b8cd Mon Sep 17 00:00:00 2001 From: Levi Gillis Date: Mon, 6 Apr 2026 09:52:31 +0200 Subject: [PATCH 3/3] Adjust CommandSignals to be strings --- .../{CommandSignal.cs => CommandSignals.cs} | 31 +++++++++-------- src/Renci.SshNet/SshCommand.cs | 33 ++++++------------- 2 files changed, 25 insertions(+), 39 deletions(-) rename src/Renci.SshNet/{CommandSignal.cs => CommandSignals.cs} (62%) diff --git a/src/Renci.SshNet/CommandSignal.cs b/src/Renci.SshNet/CommandSignals.cs similarity index 62% rename from src/Renci.SshNet/CommandSignal.cs rename to src/Renci.SshNet/CommandSignals.cs index 5d37c24c6..ceb84d475 100644 --- a/src/Renci.SshNet/CommandSignal.cs +++ b/src/Renci.SshNet/CommandSignals.cs @@ -1,74 +1,73 @@ namespace Renci.SshNet { /// - /// The ssh compatible POSIX/ANSI signals with their libc compatible values. + /// The ssh compatible standard POSIX/ANSI signals. /// -#pragma warning disable CA1720 // Identifier contains type name - public enum CommandSignal + public static class CommandSignals { /// /// Hangup (POSIX). /// - HUP = 1, + public const string SIGHUP = "HUP"; /// /// Interrupt (ANSI). /// - INT = 2, + public const string SIGINT = "INT"; /// /// Quit (POSIX). /// - QUIT = 3, + public const string SIGQUIT = "QUIT"; /// /// Illegal instruction (ANSI). /// - ILL = 4, + public const string SIGILL = "ILL"; /// /// Abort (ANSI). /// - ABRT = 6, + public const string SIGABRT = "ABRT"; /// /// Floating-point exception (ANSI). /// - FPE = 8, + public const string SIGFPE = "FPE"; /// /// Kill, unblockable (POSIX). /// - KILL = 9, + public const string SIGKILL = "KILL"; /// /// User-defined signal 1 (POSIX). /// - USR1 = 10, + public const string SIGUSR1 = "USR1"; /// /// Segmentation violation (ANSI). /// - SEGV = 11, + public const string SIGSEGV = "SEGV"; /// /// User-defined signal 2 (POSIX). /// - USR2 = 12, + public const string SIGUSR2 = "USR2"; /// /// Broken pipe (POSIX). /// - PIPE = 13, + public const string SIGPIPE = "PIPE"; /// /// Alarm clock (POSIX). /// - ALRM = 14, + public const string SIGALRM = "ALRM"; /// /// Termination (ANSI). /// - TERM = 15, + public const string SIGTERM = "TERM"; } } diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index 48864c5b6..a66065d70 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -478,26 +478,14 @@ public void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500) } } - private static string? GetSignalName(CommandSignal signal) - { -#if NETCOREAPP - return Enum.GetName(signal); -#else - - // Boxes signal, but Enum.GetName does not have a non-boxing overload prior to .NET Core. - return Enum.GetName(typeof(CommandSignal), signal); -#endif - } - /// - /// Tries to send a POSIX/ANSI signal to the remote process executing the command, such as SIGINT or SIGTERM. + /// Tries to send a POSIX/ANSI signal to the remote process executing the command, such as SIGTERM or any of the . /// - /// The signal to send. + /// The signal to send. See for a standard list of signals. /// If the signal was sent. - public bool TrySendSignal(CommandSignal signal) + public bool TrySendSignal(string signal) { - var signalName = GetSignalName(signal); - if (signalName is null) + if (signal is null) { return false; } @@ -510,7 +498,7 @@ public bool TrySendSignal(CommandSignal signal) try { // Try to send the cancellation signal. - return _channel.SendSignalRequest(signalName); + return _channel.SendSignalRequest(signal); } catch (Exception) { @@ -522,18 +510,17 @@ public bool TrySendSignal(CommandSignal signal) } /// - /// Tries to send a POSIX/ANSI signal to the remote process executing the command, such as SIGINT or SIGTERM. + /// Tries to send a POSIX/ANSI signal to the remote process executing the command, such as SIGTERM or any of the . /// - /// The signal to send. + /// The signal to send. See for a standard list of signals. /// Signal was not a valid CommandSignal. /// The client is not connected. /// The operation timed out. /// The size of the packet exceeds the maximum size defined by the protocol. /// Command has not been started. - public void SendSignal(CommandSignal signal) + public void SendSignal(string signal) { - var signalName = GetSignalName(signal); - if (signalName is null) + if (signal is null) { throw new ArgumentException("Signal was not a valid CommandSignal."); } @@ -543,7 +530,7 @@ public void SendSignal(CommandSignal signal) throw new InvalidOperationException("Command has not been started."); } - _ = _channel.SendSignalRequest(signalName); + _ = _channel.SendSignalRequest(signal); } ///