Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/Renci.SshNet/CommandSignal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace Renci.SshNet
{
/// <summary>
/// The ssh compatible POSIX/ANSI signals with their libc compatible values.
/// </summary>
#pragma warning disable CA1720 // Identifier contains type name
public enum CommandSignal
{
/// <summary>
/// Hangup (POSIX).
/// </summary>
HUP = 1,

/// <summary>
/// Interrupt (ANSI).
/// </summary>
INT = 2,

/// <summary>
/// Quit (POSIX).
/// </summary>
QUIT = 3,

/// <summary>
/// Illegal instruction (ANSI).
/// </summary>
ILL = 4,

/// <summary>
/// Abort (ANSI).
/// </summary>
ABRT = 6,

/// <summary>
/// Floating-point exception (ANSI).
/// </summary>
FPE = 8,

/// <summary>
/// Kill, unblockable (POSIX).
/// </summary>
KILL = 9,

/// <summary>
/// User-defined signal 1 (POSIX).
/// </summary>
USR1 = 10,

/// <summary>
/// Segmentation violation (ANSI).
/// </summary>
SEGV = 11,

/// <summary>
/// User-defined signal 2 (POSIX).
/// </summary>
USR2 = 12,

/// <summary>
/// Broken pipe (POSIX).
/// </summary>
PIPE = 13,

/// <summary>
/// Alarm clock (POSIX).
/// </summary>
ALRM = 14,

/// <summary>
/// Termination (ANSI).
/// </summary>
TERM = 15,
}
}
46 changes: 46 additions & 0 deletions src/Renci.SshNet/Common/CommandExitedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#nullable enable
using System;

namespace Renci.SshNet.Common
{
/// <summary>
/// Class for command exit related events.
/// </summary>
public class CommandExitedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="CommandExitedEventArgs"/> class.
/// </summary>
/// <param name="exitStatus">The exit status.</param>
/// <param name="exitSignal">The exit signal.</param>
public CommandExitedEventArgs(int? exitStatus, string? exitSignal)
{
ExitStatus = exitStatus;
ExitSignal = exitSignal;
}

/// <summary>
/// Gets the number representing the exit status of the command, if applicable,
/// otherwise <see langword="null"/>.
/// </summary>
/// <remarks>
/// The value is not <see langword="null"/> when an exit status code has been returned
/// from the server. If the command terminated due to a signal, <see cref="ExitSignal"/>
/// may be not <see langword="null"/> instead.
/// </remarks>
/// <seealso cref="ExitSignal"/>
public int? ExitStatus { get; }


/// <summary>
/// Gets the name of the signal due to which the command
/// terminated violently, if applicable, otherwise <see langword="null"/>.
/// </summary>
/// <remarks>
/// The value (if it exists) is supplied by the server and is usually one of the
/// following, as described in https://datatracker.ietf.org/doc/html/rfc4254#section-6.10:
/// ABRT, ALRM, FPE, HUP, ILL, INT, KILL, PIPE, QUIT, SEGV, TER, USR1, USR2.
/// </remarks>
public string? ExitSignal { get; }
}
}
44 changes: 44 additions & 0 deletions src/Renci.SshNet/Common/CommandOutputEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#nullable enable
using System;
using System.Text;

namespace Renci.SshNet.Common
{
/// <summary>
/// Base class for command output related events.
/// </summary>
public class CommandOutputEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="CommandOutputEventArgs"/> class.
/// </summary>
/// <param name="rawData">The raw data received.</param>
/// <param name="encoding">The encoding used for the transmission.</param>
public CommandOutputEventArgs(ArraySegment<byte> rawData, Encoding encoding)
{
RawData = rawData;
Encoding = encoding;
}

/// <summary>
/// Gets the received data as <see langword="string"/>.
/// </summary>
public string Text
{
get
{
return Encoding.GetString(RawData.Array, RawData.Offset, RawData.Count);
}
}

/// <summary>
/// Gets the raw data received from the server. This is the data that was used to create the <see cref="Text"/> property.
/// </summary>
public ArraySegment<byte> RawData { get; }

/// <summary>
/// Gets the output encoding used.
/// </summary>
public Encoding Encoding { get; }
}
}
40 changes: 40 additions & 0 deletions src/Renci.SshNet/Common/ExtendedCommandEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
#nullable enable
using System.Text;

namespace Renci.SshNet.Common
{
/// <summary>
/// Class for extended text output related events.
/// </summary>
public class ExtendedCommandEventArgs : CommandOutputEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ExtendedCommandEventArgs"/> class.
/// </summary>
/// <param name="rawData">The raw data received.</param>
/// <param name="encoding">The encoding used for the transmission.</param>
/// <param name="dataTypeCode">The data type code.</param>
public ExtendedCommandEventArgs(ArraySegment<byte> rawData, Encoding encoding, uint dataTypeCode)
: base(rawData, encoding)
{
DataTypeCode = dataTypeCode;
}

/// <summary>
/// Gets the data type code.
/// </summary>
public uint DataTypeCode { get; }

/// <summary>
/// Gets a value indicating whether the current data represents an stderr output.
/// </summary>
public bool IsError
{
get
{
return DataTypeCode == 1;
}
}
}
}
32 changes: 32 additions & 0 deletions src/Renci.SshNet/ISshClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,38 @@ public interface ISshClient : IBaseClient
/// <exception cref="ArgumentNullException"><paramref name="commandText"/> is <see langword="null"/>.</exception>
public SshCommand RunCommand(string commandText);

/// <summary>
/// Creates the command to be executed.
/// </summary>
/// <param name="commandText">The command text.</param>
/// <returns><see cref="SshCommandLite"/> object.</returns>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
public SshCommandLite CreateCommandLite(string commandText);

/// <summary>
/// Creates the command to be executed with specified encoding.
/// </summary>
/// <param name="commandText">The command text.</param>
/// <param name="encoding">The encoding to use for results.</param>
/// <returns><see cref="SshCommandLite"/> object which uses specified encoding.</returns>
/// <remarks>This method will change current default encoding.</remarks>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <exception cref="ArgumentNullException"><paramref name="commandText"/> or <paramref name="encoding"/> is <see langword="null"/>.</exception>
public SshCommandLite CreateCommandLite(string commandText, Encoding encoding);

/// <summary>
/// Creates and executes the command.
/// </summary>
/// <param name="commandText">The command text.</param>
/// <returns>Returns an instance of <see cref="SshCommandLite"/> with execution results.</returns>
/// <remarks>This method internally uses asynchronous calls.</remarks>
/// <exception cref="ArgumentException">CommandText property is empty.</exception>
/// <exception cref="SshException">Invalid Operation - An existing channel was used to execute this command.</exception>
/// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <exception cref="ArgumentNullException"><paramref name="commandText"/> is <see langword="null"/>.</exception>
public SshCommandLite RunCommandLite(string commandText);

/// <summary>
/// Creates the shell.
/// </summary>
Expand Down
23 changes: 23 additions & 0 deletions src/Renci.SshNet/SshClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,29 @@ public SshCommand RunCommand(string commandText)
return cmd;
}

/// <inheritdoc />
public SshCommandLite CreateCommandLite(string commandText)
{
return CreateCommandLite(commandText, ConnectionInfo.Encoding);
}

/// <inheritdoc />
public SshCommandLite CreateCommandLite(string commandText, Encoding encoding)
{
EnsureSessionIsOpen();

ConnectionInfo.Encoding = encoding;
return new SshCommandLite(Session!, commandText, encoding);
}

/// <inheritdoc />
public SshCommandLite RunCommandLite(string commandText)
{
var cmd = CreateCommandLite(commandText);
_ = cmd.Execute();
return cmd;
}

/// <inheritdoc />
public Shell CreateShell(Stream input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint>? terminalModes, int bufferSize)
{
Expand Down
67 changes: 67 additions & 0 deletions src/Renci.SshNet/SshCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/// <summary>
/// Tries to send a POSIX/ANSI signal to the remote process executing the command, such as SIGINT or SIGTERM.
/// </summary>
/// <param name="signal">The signal to send</param>
/// <returns>If the signal was sent.</returns>
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;
}

/// <summary>
/// Tries to send a POSIX/ANSI signal to the remote process executing the command, such as SIGINT or SIGTERM.
/// </summary>
/// <param name="signal">The signal to send</param>
/// <exception cref="ArgumentException">Signal was not a valid CommandSignal.</exception>
/// <exception cref="SshConnectionException">The client is not connected.</exception>
/// <exception cref="SshOperationTimeoutException">The operation timed out.</exception>
/// <exception cref="InvalidOperationException">The size of the packet exceeds the maximum size defined by the protocol.</exception>
/// <exception cref="InvalidOperationException">Command has not been started.</exception>
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);
}

/// <summary>
/// Executes the command specified by <see cref="CommandText"/>.
/// </summary>
Expand Down
Loading