From d34a4275ad89b9383d84d09b4a75749bb214eab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eug=C3=A9n=20Cowie?= Date: Thu, 26 Sep 2024 17:46:40 +0100 Subject: [PATCH] Add interface to SshCommand --- src/Renci.SshNet/ISshClient.cs | 6 +- src/Renci.SshNet/ISshCommand.cs | 225 ++++++++++++++++++++++++++++++++ src/Renci.SshNet/SshClient.cs | 6 +- src/Renci.SshNet/SshCommand.cs | 197 +++------------------------- 4 files changed, 251 insertions(+), 183 deletions(-) create mode 100644 src/Renci.SshNet/ISshCommand.cs diff --git a/src/Renci.SshNet/ISshClient.cs b/src/Renci.SshNet/ISshClient.cs index 1d6edfebf..602e15102 100644 --- a/src/Renci.SshNet/ISshClient.cs +++ b/src/Renci.SshNet/ISshClient.cs @@ -40,7 +40,7 @@ public interface ISshClient : IBaseClient /// The command text. /// object. /// Client is not connected. - public SshCommand CreateCommand(string commandText); + public ISshCommand CreateCommand(string commandText); /// /// Creates the command to be executed with specified encoding. @@ -51,7 +51,7 @@ public interface ISshClient : IBaseClient /// This method will change current default encoding. /// Client is not connected. /// or is . - public SshCommand CreateCommand(string commandText, Encoding encoding); + public ISshCommand CreateCommand(string commandText, Encoding encoding); /// /// Creates and executes the command. @@ -64,7 +64,7 @@ public interface ISshClient : IBaseClient /// Asynchronous operation is already in progress. /// Client is not connected. /// is . - public SshCommand RunCommand(string commandText); + public ISshCommand RunCommand(string commandText); /// /// Creates the shell. diff --git a/src/Renci.SshNet/ISshCommand.cs b/src/Renci.SshNet/ISshCommand.cs new file mode 100644 index 000000000..364799f5d --- /dev/null +++ b/src/Renci.SshNet/ISshCommand.cs @@ -0,0 +1,225 @@ +#nullable enable +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Renci.SshNet.Common; + +namespace Renci.SshNet +{ + /// + /// Represents an SSH command that can be executed. + /// + public interface ISshCommand : IDisposable + { + /// + /// Gets the command text. + /// + string CommandText { get; } + + /// + /// Gets or sets the command timeout. + /// + /// + /// The command timeout. + /// + TimeSpan CommandTimeout { get; set; } + + /// + /// 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. + /// + /// + 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. + /// + string? ExitSignal { get; } + + /// + /// Gets the output stream. + /// + Stream OutputStream { get; } + + /// + /// Gets the extended output stream. + /// + Stream ExtendedOutputStream { get; } + + /// + /// Creates and returns the input stream for the command. + /// + /// + /// The stream that can be used to transfer data to the command's input stream. + /// + /// + /// Callers should ensure that is called on the + /// returned instance in order to notify the command that no more data will be sent. + /// Failure to do so may result in the command executing indefinitely. + /// + /// + /// This example shows how to stream some data to 'cat' and have the server echo it back. + /// + /// using (SshCommand command = mySshClient.CreateCommand("cat")) + /// { + /// Task executeTask = command.ExecuteAsync(CancellationToken.None); + /// + /// using (Stream inputStream = command.CreateInputStream()) + /// { + /// inputStream.Write("Hello World!"u8); + /// } + /// + /// await executeTask; + /// + /// Console.WriteLine(command.ExitStatus); // 0 + /// Console.WriteLine(command.Result); // "Hello World!" + /// } + /// + /// + Stream CreateInputStream(); + + /// + /// Gets the standard output of the command by reading . + /// + string Result { get; } + + /// + /// Gets the standard error of the command by reading , + /// when extended data has been sent which has been marked as stderr. + /// + string Error { get; } + + /// + /// Executes the command asynchronously. + /// + /// + /// The . When triggered, attempts to terminate the + /// remote command by sending a signal. + /// + /// A representing the lifetime of the command. + /// Command is already executing. Thrown synchronously. + /// Instance has been disposed. Thrown synchronously. + /// The has been cancelled. + /// The command timed out according to . + Task ExecuteAsync(CancellationToken cancellationToken = default); + + /// + /// Begins an asynchronous command execution. + /// + /// + /// An that represents the asynchronous command execution, which could still be pending. + /// + /// Asynchronous operation is already in progress. + /// Invalid operation. + /// CommandText property is empty. + /// Client is not connected. + /// Operation has timed out. + IAsyncResult BeginExecute(); + + /// + /// Begins an asynchronous command execution. + /// + /// An optional asynchronous callback, to be called when the command execution is complete. + /// + /// An that represents the asynchronous command execution, which could still be pending. + /// + /// Asynchronous operation is already in progress. + /// Invalid operation. + /// CommandText property is empty. + /// Client is not connected. + /// Operation has timed out. + IAsyncResult BeginExecute(AsyncCallback? callback); + + /// + /// Begins an asynchronous command execution. + /// + /// An optional asynchronous callback, to be called when the command execution is complete. + /// A user-provided object that distinguishes this particular asynchronous read request from other requests. + /// + /// An that represents the asynchronous command execution, which could still be pending. + /// + /// Asynchronous operation is already in progress. + /// Invalid operation. + /// CommandText property is empty. + /// Client is not connected. + /// Operation has timed out. + IAsyncResult BeginExecute(AsyncCallback? callback, object? state); + + /// + /// Begins an asynchronous command execution. + /// + /// The command text. + /// An optional asynchronous callback, to be called when the command execution is complete. + /// A user-provided object that distinguishes this particular asynchronous read request from other requests. + /// + /// An that represents the asynchronous command execution, which could still be pending. + /// + /// Client is not connected. + /// Operation has timed out. + IAsyncResult BeginExecute(string commandText, AsyncCallback? callback, object? state); + + /// + /// Waits for the pending asynchronous command execution to complete. + /// + /// The reference to the pending asynchronous request to finish. + /// . + /// does not correspond to the currently executing command. + /// is . + string EndExecute(IAsyncResult asyncResult); + + /// + /// Cancels a running command by sending a signal to the remote process. + /// + /// if true send SIGKILL instead of SIGTERM. + /// Time to wait for the server to reply. + /// + /// + /// This method stops the command running on the server by sending a SIGTERM + /// (or SIGKILL, depending on ) signal to the remote + /// process. When the server implements signals, it will send a response which + /// populates with the signal with which the command terminated. + /// + /// + /// When the server does not implement signals, it may send no response. As a fallback, + /// this method waits up to for a response + /// and then completes the object anyway if there was none. + /// + /// + /// If the command has already finished (with or without cancellation), this method does + /// nothing. + /// + /// + /// Command has not been started. + void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500); + + /// + /// Executes the command specified by . + /// + /// . + /// Client is not connected. + /// Operation has timed out. + string Execute(); + + /// + /// Executes the specified command. + /// + /// The command text. + /// . + /// Client is not connected. + /// Operation has timed out. + string Execute(string commandText); + } +} diff --git a/src/Renci.SshNet/SshClient.cs b/src/Renci.SshNet/SshClient.cs index 3051074e1..71d569df2 100644 --- a/src/Renci.SshNet/SshClient.cs +++ b/src/Renci.SshNet/SshClient.cs @@ -191,13 +191,13 @@ private static void DetachForwardedPort(ForwardedPort port) } /// - public SshCommand CreateCommand(string commandText) + public ISshCommand CreateCommand(string commandText) { return CreateCommand(commandText, ConnectionInfo.Encoding); } /// - public SshCommand CreateCommand(string commandText, Encoding encoding) + public ISshCommand CreateCommand(string commandText, Encoding encoding) { EnsureSessionIsOpen(); @@ -206,7 +206,7 @@ public SshCommand CreateCommand(string commandText, Encoding encoding) } /// - public SshCommand RunCommand(string commandText) + public ISshCommand RunCommand(string commandText) { var cmd = CreateCommand(commandText); _ = cmd.Execute(); diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index 6bfd57422..c70e5c48c 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -13,10 +13,8 @@ namespace Renci.SshNet { - /// - /// Represents an SSH command that can be executed. - /// - public class SshCommand : IDisposable + /// + public class SshCommand : ISshCommand { private readonly ISession _session; private readonly Encoding _encoding; @@ -46,17 +44,10 @@ public class SshCommand : IDisposable private int _exitStatus; private volatile bool _haveExitStatus; // volatile to prevent re-ordering of reads/writes of _exitStatus. - /// - /// Gets the command text. - /// + /// public string CommandText { get; private set; } - /// - /// Gets or sets the command timeout. - /// - /// - /// The command timeout. - /// + /// public TimeSpan CommandTimeout { get @@ -71,16 +62,7 @@ public TimeSpan CommandTimeout } } - /// - /// 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 @@ -89,57 +71,16 @@ public int? ExitStatus } } - /// - /// 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; private set; } - /// - /// Gets the output stream. - /// + /// public Stream OutputStream { get; private set; } - /// - /// Gets the extended output stream. - /// + /// public Stream ExtendedOutputStream { get; private set; } - /// - /// Creates and returns the input stream for the command. - /// - /// - /// The stream that can be used to transfer data to the command's input stream. - /// - /// - /// Callers should ensure that is called on the - /// returned instance in order to notify the command that no more data will be sent. - /// Failure to do so may result in the command executing indefinitely. - /// - /// - /// This example shows how to stream some data to 'cat' and have the server echo it back. - /// - /// using (SshCommand command = mySshClient.CreateCommand("cat")) - /// { - /// Task executeTask = command.ExecuteAsync(CancellationToken.None); - /// - /// using (Stream inputStream = command.CreateInputStream()) - /// { - /// inputStream.Write("Hello World!"u8); - /// } - /// - /// await executeTask; - /// - /// Console.WriteLine(command.ExitStatus); // 0 - /// Console.WriteLine(command.Result); // "Hello World!" - /// } - /// - /// + /// public Stream CreateInputStream() { if (_channel == null) @@ -156,9 +97,7 @@ public Stream CreateInputStream() return _inputStream; } - /// - /// Gets the standard output of the command by reading . - /// + /// public string Result { get @@ -180,10 +119,7 @@ public string Result } } - /// - /// Gets the standard error of the command by reading , - /// when extended data has been sent which has been marked as stderr. - /// + /// public string Error { get @@ -228,18 +164,7 @@ internal SshCommand(ISession session, string commandText, Encoding encoding) _session.ErrorOccured += Session_ErrorOccured; } - /// - /// Executes the command asynchronously. - /// - /// - /// The . When triggered, attempts to terminate the - /// remote command by sending a signal. - /// - /// A representing the lifetime of the command. - /// Command is already executing. Thrown synchronously. - /// Instance has been disposed. Thrown synchronously. - /// The has been cancelled. - /// The command timed out according to . + /// #pragma warning disable CA1849 // Call async methods when in an async method; PipeStream.DisposeAsync would complete synchronously anyway. public Task ExecuteAsync(CancellationToken cancellationToken = default) { @@ -307,68 +232,25 @@ public Task ExecuteAsync(CancellationToken cancellationToken = default) } #pragma warning restore CA1849 - /// - /// Begins an asynchronous command execution. - /// - /// - /// An that represents the asynchronous command execution, which could still be pending. - /// - /// Asynchronous operation is already in progress. - /// Invalid operation. - /// CommandText property is empty. - /// Client is not connected. - /// Operation has timed out. + /// public IAsyncResult BeginExecute() { return BeginExecute(callback: null, state: null); } - /// - /// Begins an asynchronous command execution. - /// - /// An optional asynchronous callback, to be called when the command execution is complete. - /// - /// An that represents the asynchronous command execution, which could still be pending. - /// - /// Asynchronous operation is already in progress. - /// Invalid operation. - /// CommandText property is empty. - /// Client is not connected. - /// Operation has timed out. + /// public IAsyncResult BeginExecute(AsyncCallback? callback) { return BeginExecute(callback, state: null); } - /// - /// Begins an asynchronous command execution. - /// - /// An optional asynchronous callback, to be called when the command execution is complete. - /// A user-provided object that distinguishes this particular asynchronous read request from other requests. - /// - /// An that represents the asynchronous command execution, which could still be pending. - /// - /// Asynchronous operation is already in progress. - /// Invalid operation. - /// CommandText property is empty. - /// Client is not connected. - /// Operation has timed out. + /// public IAsyncResult BeginExecute(AsyncCallback? callback, object? state) { return TaskToAsyncResult.Begin(ExecuteAsync(), callback, state); } - /// - /// Begins an asynchronous command execution. - /// - /// The command text. - /// An optional asynchronous callback, to be called when the command execution is complete. - /// A user-provided object that distinguishes this particular asynchronous read request from other requests. - /// - /// An that represents the asynchronous command execution, which could still be pending. - /// - /// Client is not connected. - /// Operation has timed out. + /// public IAsyncResult BeginExecute(string commandText, AsyncCallback? callback, object? state) { ThrowHelper.ThrowIfNull(commandText); @@ -378,13 +260,7 @@ public IAsyncResult BeginExecute(string commandText, AsyncCallback? callback, ob return BeginExecute(callback, state); } - /// - /// Waits for the pending asynchronous command execution to complete. - /// - /// The reference to the pending asynchronous request to finish. - /// . - /// does not correspond to the currently executing command. - /// is . + /// public string EndExecute(IAsyncResult asyncResult) { var executeTask = TaskToAsyncResult.Unwrap(asyncResult); @@ -399,29 +275,7 @@ public string EndExecute(IAsyncResult asyncResult) return Result; } - /// - /// Cancels a running command by sending a signal to the remote process. - /// - /// if true send SIGKILL instead of SIGTERM. - /// Time to wait for the server to reply. - /// - /// - /// This method stops the command running on the server by sending a SIGTERM - /// (or SIGKILL, depending on ) signal to the remote - /// process. When the server implements signals, it will send a response which - /// populates with the signal with which the command terminated. - /// - /// - /// When the server does not implement signals, it may send no response. As a fallback, - /// this method waits up to for a response - /// and then completes the object anyway if there was none. - /// - /// - /// If the command has already finished (with or without cancellation), this method does - /// nothing. - /// - /// - /// Command has not been started. + /// public void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500) { if (_tcs is null) @@ -466,12 +320,7 @@ public void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500) SetAsyncComplete(); } - /// - /// Executes the command specified by . - /// - /// . - /// Client is not connected. - /// Operation has timed out. + /// public string Execute() { ExecuteAsync().GetAwaiter().GetResult(); @@ -479,13 +328,7 @@ public string Execute() return Result; } - /// - /// Executes the specified command. - /// - /// The command text. - /// . - /// Client is not connected. - /// Operation has timed out. + /// public string Execute(string commandText) { CommandText = commandText;