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/Common/CommandExitedEventArgs.cs b/src/Renci.SshNet/Common/CommandExitedEventArgs.cs
new file mode 100644
index 000000000..40a3dc50f
--- /dev/null
+++ b/src/Renci.SshNet/Common/CommandExitedEventArgs.cs
@@ -0,0 +1,46 @@
+#nullable enable
+using System;
+
+namespace Renci.SshNet.Common
+{
+ ///
+ /// Class for command exit related events.
+ ///
+ public class CommandExitedEventArgs : EventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The exit status.
+ /// The exit signal.
+ public CommandExitedEventArgs(int? exitStatus, string? exitSignal)
+ {
+ ExitStatus = exitStatus;
+ ExitSignal = exitSignal;
+ }
+
+ ///
+ /// Gets the number representing the exit status of the command, if applicable,
+ /// otherwise .
+ ///
+ ///
+ /// The value is not when an exit status code has been returned
+ /// from the server. If the command terminated due to a signal,
+ /// may be not instead.
+ ///
+ ///
+ public int? ExitStatus { get; }
+
+
+ ///
+ /// Gets the name of the signal due to which the command
+ /// terminated violently, if applicable, otherwise .
+ ///
+ ///
+ /// 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.
+ ///
+ public string? ExitSignal { get; }
+ }
+}
diff --git a/src/Renci.SshNet/Common/CommandOutputEventArgs.cs b/src/Renci.SshNet/Common/CommandOutputEventArgs.cs
new file mode 100644
index 000000000..d25bc6982
--- /dev/null
+++ b/src/Renci.SshNet/Common/CommandOutputEventArgs.cs
@@ -0,0 +1,44 @@
+#nullable enable
+using System;
+using System.Text;
+
+namespace Renci.SshNet.Common
+{
+ ///
+ /// Base class for command output related events.
+ ///
+ public class CommandOutputEventArgs : EventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The raw data received.
+ /// The encoding used for the transmission.
+ public CommandOutputEventArgs(ArraySegment rawData, Encoding encoding)
+ {
+ RawData = rawData;
+ Encoding = encoding;
+ }
+
+ ///
+ /// Gets the received data as .
+ ///
+ public string Text
+ {
+ get
+ {
+ return Encoding.GetString(RawData.Array, RawData.Offset, RawData.Count);
+ }
+ }
+
+ ///
+ /// Gets the raw data received from the server. This is the data that was used to create the property.
+ ///
+ public ArraySegment RawData { get; }
+
+ ///
+ /// Gets the output encoding used.
+ ///
+ public Encoding Encoding { get; }
+ }
+}
diff --git a/src/Renci.SshNet/Common/ExtendedCommandEventArgs.cs b/src/Renci.SshNet/Common/ExtendedCommandEventArgs.cs
new file mode 100644
index 000000000..fa0a9f5fe
--- /dev/null
+++ b/src/Renci.SshNet/Common/ExtendedCommandEventArgs.cs
@@ -0,0 +1,40 @@
+using System;
+#nullable enable
+using System.Text;
+
+namespace Renci.SshNet.Common
+{
+ ///
+ /// Class for extended text output related events.
+ ///
+ public class ExtendedCommandEventArgs : CommandOutputEventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The raw data received.
+ /// The encoding used for the transmission.
+ /// The data type code.
+ public ExtendedCommandEventArgs(ArraySegment rawData, Encoding encoding, uint dataTypeCode)
+ : base(rawData, encoding)
+ {
+ DataTypeCode = dataTypeCode;
+ }
+
+ ///
+ /// Gets the data type code.
+ ///
+ public uint DataTypeCode { get; }
+
+ ///
+ /// Gets a value indicating whether the current data represents an stderr output.
+ ///
+ public bool IsError
+ {
+ get
+ {
+ return DataTypeCode == 1;
+ }
+ }
+ }
+}
diff --git a/src/Renci.SshNet/ISshClient.cs b/src/Renci.SshNet/ISshClient.cs
index 4507c316b..c3b36964a 100644
--- a/src/Renci.SshNet/ISshClient.cs
+++ b/src/Renci.SshNet/ISshClient.cs
@@ -66,6 +66,38 @@ public interface ISshClient : IBaseClient
/// is .
public SshCommand RunCommand(string commandText);
+ ///
+ /// Creates the command to be executed.
+ ///
+ /// The command text.
+ /// object.
+ /// Client is not connected.
+ public SshCommandLite CreateCommandLite(string commandText);
+
+ ///
+ /// Creates the command to be executed with specified encoding.
+ ///
+ /// The command text.
+ /// The encoding to use for results.
+ /// object which uses specified encoding.
+ /// This method will change current default encoding.
+ /// Client is not connected.
+ /// or is .
+ public SshCommandLite CreateCommandLite(string commandText, Encoding encoding);
+
+ ///
+ /// Creates and executes the command.
+ ///
+ /// The command text.
+ /// Returns an instance of with execution results.
+ /// This method internally uses asynchronous calls.
+ /// CommandText property is empty.
+ /// Invalid Operation - An existing channel was used to execute this command.
+ /// Asynchronous operation is already in progress.
+ /// Client is not connected.
+ /// is .
+ public SshCommandLite RunCommandLite(string commandText);
+
///
/// Creates the shell.
///
diff --git a/src/Renci.SshNet/SshClient.cs b/src/Renci.SshNet/SshClient.cs
index 331ccaf69..a510da23e 100644
--- a/src/Renci.SshNet/SshClient.cs
+++ b/src/Renci.SshNet/SshClient.cs
@@ -209,6 +209,29 @@ public SshCommand RunCommand(string commandText)
return cmd;
}
+ ///
+ public SshCommandLite CreateCommandLite(string commandText)
+ {
+ return CreateCommandLite(commandText, ConnectionInfo.Encoding);
+ }
+
+ ///
+ public SshCommandLite CreateCommandLite(string commandText, Encoding encoding)
+ {
+ EnsureSessionIsOpen();
+
+ ConnectionInfo.Encoding = encoding;
+ return new SshCommandLite(Session!, commandText, encoding);
+ }
+
+ ///
+ public SshCommandLite RunCommandLite(string commandText)
+ {
+ var cmd = CreateCommandLite(commandText);
+ _ = cmd.Execute();
+ return cmd;
+ }
+
///
public Shell CreateShell(Stream input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary? terminalModes, int bufferSize)
{
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 .
///
diff --git a/src/Renci.SshNet/SshCommandLite.cs b/src/Renci.SshNet/SshCommandLite.cs
new file mode 100644
index 000000000..6a19d3144
--- /dev/null
+++ b/src/Renci.SshNet/SshCommandLite.cs
@@ -0,0 +1,589 @@
+#nullable enable
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Renci.SshNet.Channels;
+using Renci.SshNet.Common;
+using Renci.SshNet.Messages.Connection;
+using Renci.SshNet.Messages.Transport;
+
+namespace Renci.SshNet
+{
+ ///
+ /// Represents an SSH command that can be executed.
+ ///
+ public sealed class SshCommandLite : IDisposable
+ {
+ private readonly ISession _session;
+ private readonly Encoding _encoding;
+
+ private IChannelSession _channel;
+ private TaskCompletionSource