Procházet zdrojové kódy

A couple of changes/fixes in SshCommand (#1423)

* Send "signal" and set the wait handle on completion always

* Make ExitStatus nullable
Rob Hague před 1 rokem
rodič
revize
ac395dd64c

+ 1 - 1
.editorconfig

@@ -688,7 +688,7 @@ dotnet_diagnostic.CA1852.severity = none
 # CA1859: Change return type for improved performance
 #
 # By default, this diagnostic is only reported for private members.
-dotnet_code_quality.CA1859.api_surface = all
+dotnet_code_quality.CA1859.api_surface = private,internal
 
 # CA2208: Instantiate argument exceptions correctly
 # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2208

+ 0 - 29
src/Renci.SshNet/Channels/ChannelSession.cs

@@ -274,35 +274,6 @@ namespace Renci.SshNet.Channels
             return true;
         }
 
-        /// <summary>
-        /// Sends the exit status request.
-        /// </summary>
-        /// <param name="exitStatus">The exit status.</param>
-        /// <returns>
-        /// <see langword="true"/> if request was successful; otherwise <see langword="false"/>.
-        /// </returns>
-        public bool SendExitStatusRequest(uint exitStatus)
-        {
-            SendMessage(new ChannelRequestMessage(RemoteChannelNumber, new ExitStatusRequestInfo(exitStatus)));
-            return true;
-        }
-
-        /// <summary>
-        /// Sends the exit signal request.
-        /// </summary>
-        /// <param name="signalName">Name of the signal.</param>
-        /// <param name="coreDumped">if set to <see langword="true"/> [core dumped].</param>
-        /// <param name="errorMessage">The error message.</param>
-        /// <param name="language">The language.</param>
-        /// <returns>
-        /// <see langword="true"/> if request was successful; otherwise <see langword="false"/>.
-        /// </returns>
-        public bool SendExitSignalRequest(string signalName, bool coreDumped, string errorMessage, string language)
-        {
-            SendMessage(new ChannelRequestMessage(RemoteChannelNumber, new ExitSignalRequestInfo(signalName, coreDumped, errorMessage, language)));
-            return true;
-        }
-
         /// <summary>
         /// Sends eow@openssh.com request.
         /// </summary>

+ 0 - 21
src/Renci.SshNet/Channels/IChannelSession.cs

@@ -120,27 +120,6 @@ namespace Renci.SshNet.Channels
         /// </returns>
         bool SendSignalRequest(string signalName);
 
-        /// <summary>
-        /// Sends the exit status request.
-        /// </summary>
-        /// <param name="exitStatus">The exit status.</param>
-        /// <returns>
-        /// <see langword="true"/> if request was successful; otherwise <see langword="false"/>.
-        /// </returns>
-        bool SendExitStatusRequest(uint exitStatus);
-
-        /// <summary>
-        /// Sends the exit signal request.
-        /// </summary>
-        /// <param name="signalName">Name of the signal.</param>
-        /// <param name="coreDumped">if set to <see langword="true"/> [core dumped].</param>
-        /// <param name="errorMessage">The error message.</param>
-        /// <param name="language">The language.</param>
-        /// <returns>
-        /// <see langword="true"/> if request was successful; otherwise <see langword="false"/>.
-        /// </returns>
-        bool SendExitSignalRequest(string signalName, bool coreDumped, string errorMessage, string language);
-
         /// <summary>
         /// Sends eow@openssh.com request.
         /// </summary>

+ 0 - 10
src/Renci.SshNet/CommandAsyncResult.cs

@@ -56,15 +56,5 @@ namespace Renci.SshNet
         /// true if the operation is complete; otherwise, false.
         /// </returns>
         public bool IsCompleted { get; internal set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether <see cref="SshCommand.EndExecute(IAsyncResult)"/> was already called for this
-        /// <see cref="CommandAsyncResult"/>.
-        /// </summary>
-        /// <returns>
-        /// <see langword="true"/> if <see cref="SshCommand.EndExecute(IAsyncResult)"/> was already called for this <see cref="CommandAsyncResult"/>;
-        /// otherwise, <see langword="false"/>.
-        /// </returns>
-        internal bool EndCalled { get; set; }
     }
 }

+ 1 - 1
src/Renci.SshNet/SshClient.cs

@@ -235,7 +235,7 @@ namespace Renci.SshNet
             EnsureSessionIsOpen();
 
             ConnectionInfo.Encoding = encoding;
-            return new SshCommand(Session, commandText, encoding);
+            return new SshCommand(Session!, commandText, encoding);
         }
 
         /// <summary>

+ 226 - 241
src/Renci.SshNet/SshCommand.cs

@@ -1,5 +1,6 @@
-using System;
-using System.Globalization;
+#nullable enable
+using System;
+using System.Diagnostics;
 using System.IO;
 using System.Runtime.ExceptionServices;
 using System.Text;
@@ -18,24 +19,30 @@ namespace Renci.SshNet
     /// </summary>
     public class SshCommand : IDisposable
     {
+        private static readonly object CompletedResult = new();
+
+        private readonly ISession _session;
         private readonly Encoding _encoding;
-        private readonly object _endExecuteLock = new object();
-
-        private ISession _session;
-        private IChannelSession _channel;
-        private CommandAsyncResult _asyncResult;
-        private AsyncCallback _callback;
-        private EventWaitHandle _sessionErrorOccuredWaitHandle;
-        private EventWaitHandle _commandCancelledWaitHandle;
-        private Exception _exception;
-        private string _result;
-        private string _error;
+
+        /// <summary>
+        /// The result of the command: an exception, <see cref="CompletedResult"/>
+        /// or <see langword="null"/>.
+        /// </summary>
+        private object? _result;
+
+        private IChannelSession? _channel;
+        private CommandAsyncResult? _asyncResult;
+        private AsyncCallback? _callback;
+        private string? _stdOut;
+        private string? _stdErr;
         private bool _hasError;
         private bool _isDisposed;
-        private bool _isCancelled;
-        private ChannelInputStream _inputStream;
+        private ChannelInputStream? _inputStream;
         private TimeSpan _commandTimeout;
 
+        private int _exitStatus;
+        private volatile bool _haveExitStatus; // volatile to prevent re-ordering of reads/writes of _exitStatus.
+
         /// <summary>
         /// Gets the command text.
         /// </summary>
@@ -62,23 +69,43 @@ namespace Renci.SshNet
         }
 
         /// <summary>
-        /// Gets the command exit status.
+        /// Gets the number representing the exit status of the command, if applicable,
+        /// otherwise <see langword="null"/>.
         /// </summary>
-        public int ExitStatus { get; private set; }
+        /// <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
+            {
+                return _haveExitStatus ? _exitStatus : null;
+            }
+        }
+
+        /// <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; private set; }
 
         /// <summary>
         /// Gets the output stream.
         /// </summary>
-#pragma warning disable CA1859 // Use concrete types when possible for improved performance
         public Stream OutputStream { get; private set; }
-#pragma warning restore CA1859 // Use concrete types when possible for improved performance
 
         /// <summary>
         /// Gets the extended output stream.
         /// </summary>
-#pragma warning disable CA1859 // Use concrete types when possible for improved performance
         public Stream ExtendedOutputStream { get; private set; }
-#pragma warning restore CA1859 // Use concrete types when possible for improved performance
 
         /// <summary>
         /// Creates and returns the input stream for the command.
@@ -109,21 +136,19 @@ namespace Renci.SshNet
         {
             get
             {
-                if (_result is not null)
+                if (_stdOut is not null)
                 {
-                    return _result;
+                    return _stdOut;
                 }
 
-                if (OutputStream is null)
+                if (_asyncResult is null)
                 {
                     return string.Empty;
                 }
 
-                using (var sr = new StreamReader(OutputStream,
-                                                 _encoding,
-                                                 detectEncodingFromByteOrderMarks: true))
+                using (var sr = new StreamReader(OutputStream, _encoding))
                 {
-                    return _result = sr.ReadToEnd();
+                    return _stdOut = sr.ReadToEnd();
                 }
             }
         }
@@ -135,21 +160,19 @@ namespace Renci.SshNet
         {
             get
             {
-                if (_error is not null)
+                if (_stdErr is not null)
                 {
-                    return _error;
+                    return _stdErr;
                 }
 
-                if (ExtendedOutputStream is null || !_hasError)
+                if (_asyncResult is null || !_hasError)
                 {
                     return string.Empty;
                 }
 
-                using (var sr = new StreamReader(ExtendedOutputStream,
-                                                 _encoding,
-                                                 detectEncodingFromByteOrderMarks: true))
+                using (var sr = new StreamReader(ExtendedOutputStream, _encoding))
                 {
-                    return _error = sr.ReadToEnd();
+                    return _stdErr = sr.ReadToEnd();
                 }
             }
         }
@@ -182,8 +205,8 @@ namespace Renci.SshNet
             CommandText = commandText;
             _encoding = encoding;
             CommandTimeout = Timeout.InfiniteTimeSpan;
-            _sessionErrorOccuredWaitHandle = new AutoResetEvent(initialState: false);
-            _commandCancelledWaitHandle = new AutoResetEvent(initialState: false);
+            OutputStream = new PipeStream();
+            ExtendedOutputStream = new PipeStream();
             _session.Disconnected += Session_Disconnected;
             _session.ErrorOccured += Session_ErrorOccured;
         }
@@ -216,7 +239,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException">CommandText property is empty.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
-        public IAsyncResult BeginExecute(AsyncCallback callback)
+        public IAsyncResult BeginExecute(AsyncCallback? callback)
         {
             return BeginExecute(callback, state: null);
         }
@@ -234,47 +257,55 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException">CommandText property is empty.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
-#pragma warning disable CA1859 // Use concrete types when possible for improved performance
-        public IAsyncResult BeginExecute(AsyncCallback callback, object state)
-#pragma warning restore CA1859 // Use concrete types when possible for improved performance
+        public IAsyncResult BeginExecute(AsyncCallback? callback, object? state)
         {
-            // Prevent from executing BeginExecute before calling EndExecute
-            if (_asyncResult != null && !_asyncResult.EndCalled)
+#if NET7_0_OR_GREATER
+            ObjectDisposedException.ThrowIf(_isDisposed, this);
+#else
+            if (_isDisposed)
             {
-                throw new InvalidOperationException("Asynchronous operation is already in progress.");
+                throw new ObjectDisposedException(GetType().FullName);
+            }
+#endif
+
+            if (_asyncResult is not null)
+            {
+                if (!_asyncResult.AsyncWaitHandle.WaitOne(0))
+                {
+                    throw new InvalidOperationException("Asynchronous operation is already in progress.");
+                }
+
+                OutputStream.Dispose();
+                ExtendedOutputStream.Dispose();
+
+                // Initialize output streams. We already initialised them for the first
+                // execution in the constructor (to allow passing them around before execution)
+                // so we just need to reinitialise them for subsequent executions.
+                OutputStream = new PipeStream();
+                ExtendedOutputStream = new PipeStream();
             }
 
             // Create new AsyncResult object
             _asyncResult = new CommandAsyncResult
             {
                 AsyncWaitHandle = new ManualResetEvent(initialState: false),
-                IsCompleted = false,
                 AsyncState = state,
             };
 
-            if (_channel is not null)
-            {
-                throw new SshException("Invalid operation.");
-            }
-
-            if (string.IsNullOrEmpty(CommandText))
-            {
-                throw new ArgumentException("CommandText property is empty.");
-            }
-
-            OutputStream?.Dispose();
-            ExtendedOutputStream?.Dispose();
-
-            // Initialize output streams
-            OutputStream = new PipeStream();
-            ExtendedOutputStream = new PipeStream();
-
+            _exitStatus = default;
+            _haveExitStatus = false;
+            ExitSignal = null;
             _result = null;
-            _error = null;
+            _stdOut = null;
+            _stdErr = null;
             _hasError = false;
             _callback = callback;
 
-            _channel = CreateChannel();
+            _channel = _session.CreateChannelSession();
+            _channel.DataReceived += Channel_DataReceived;
+            _channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
+            _channel.RequestReceived += Channel_RequestReceived;
+            _channel.Closed += Channel_Closed;
             _channel.Open();
 
             _ = _channel.SendExecRequest(CommandText);
@@ -293,8 +324,13 @@ namespace Renci.SshNet
         /// </returns>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
-        public IAsyncResult BeginExecute(string commandText, AsyncCallback callback, object state)
+        public IAsyncResult BeginExecute(string commandText, AsyncCallback? callback, object? state)
         {
+            if (commandText is null)
+            {
+                throw new ArgumentNullException(nameof(commandText));
+            }
+
             CommandText = commandText;
 
             return BeginExecute(callback, state);
@@ -314,55 +350,88 @@ namespace Renci.SshNet
                 throw new ArgumentNullException(nameof(asyncResult));
             }
 
-            if (asyncResult is not CommandAsyncResult commandAsyncResult || _asyncResult != commandAsyncResult)
+            if (_asyncResult != asyncResult)
             {
-                throw new ArgumentException(string.Format("The {0} object was not returned from the corresponding asynchronous method on this class.", nameof(IAsyncResult)));
+                throw new ArgumentException("Argument does not correspond to the currently executing command.", nameof(asyncResult));
             }
 
-            lock (_endExecuteLock)
-            {
-                if (commandAsyncResult.EndCalled)
-                {
-                    throw new ArgumentException("EndExecute can only be called once for each asynchronous operation.");
-                }
-
-                _inputStream?.Close();
+            _inputStream?.Dispose();
 
-                try
-                {
-                    // wait for operation to complete (or time out)
-                    WaitOnHandle(_asyncResult.AsyncWaitHandle);
-                }
-                finally
-                {
-                    UnsubscribeFromEventsAndDisposeChannel(_channel);
-                    _channel = null;
+            if (!_asyncResult.AsyncWaitHandle.WaitOne(CommandTimeout))
+            {
+                // Complete the operation with a TimeoutException (which will be thrown below).
+                SetAsyncComplete(new SshOperationTimeoutException($"Command '{CommandText}' timed out. ({nameof(CommandTimeout)}: {CommandTimeout})."));
+            }
 
-                    OutputStream?.Dispose();
-                    ExtendedOutputStream?.Dispose();
+            Debug.Assert(_asyncResult.IsCompleted);
 
-                    commandAsyncResult.EndCalled = true;
-                }
+            if (_result is Exception exception)
+            {
+                ExceptionDispatchInfo.Capture(exception).Throw();
+            }
 
-                if (!_isCancelled)
-                {
-                    return Result;
-                }
+            Debug.Assert(_result == CompletedResult);
+            Debug.Assert(!OutputStream.CanWrite, $"{nameof(OutputStream)} should have been disposed (else we will block).");
 
-                SetAsyncComplete();
-                throw new OperationCanceledException();
-            }
+            return Result;
         }
 
         /// <summary>
-        /// Cancels command execution in asynchronous scenarios.
+        /// Cancels a running command by sending a signal to the remote process.
         /// </summary>
         /// <param name="forceKill">if true send SIGKILL instead of SIGTERM.</param>
-        public void CancelAsync(bool forceKill = false)
+        /// <param name="millisecondsTimeout">Time to wait for the server to reply.</param>
+        /// <remarks>
+        /// <para>
+        /// This method stops the command running on the server by sending a SIGTERM
+        /// (or SIGKILL, depending on <paramref name="forceKill"/>) signal to the remote
+        /// process. When the server implements signals, it will send a response which
+        /// populates <see cref="ExitSignal"/> with the signal with which the command terminated.
+        /// </para>
+        /// <para>
+        /// When the server does not implement signals, it may send no response. As a fallback,
+        /// this method waits up to <paramref name="millisecondsTimeout"/> for a response
+        /// and then completes the <see cref="SshCommand"/> object anyway if there was none.
+        /// </para>
+        /// <para>
+        /// If the command has already finished (with or without cancellation), this method does
+        /// nothing.
+        /// </para>
+        /// </remarks>
+        /// <exception cref="InvalidOperationException">Command has not been started.</exception>
+        public void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500)
         {
-            var signal = forceKill ? "KILL" : "TERM";
-            _ = _channel?.SendExitSignalRequest(signal, coreDumped: false, "Command execution has been cancelled.", "en");
-            _ = _commandCancelledWaitHandle?.Set();
+            if (_asyncResult is not { } asyncResult)
+            {
+                throw new InvalidOperationException("Command has not been started.");
+            }
+
+            var exception = new OperationCanceledException($"Command '{CommandText}' was cancelled.");
+
+            if (Interlocked.CompareExchange(ref _result, exception, comparand: null) is not null)
+            {
+                // Command has already completed.
+                return;
+            }
+
+            // Try to send the cancellation signal.
+            if (_channel?.SendSignalRequest(forceKill ? "KILL" : "TERM") is null)
+            {
+                // Command has completed (in the meantime since the last check).
+                // We won the race above and the command has finished by some other means,
+                // but will throw the OperationCanceledException.
+                return;
+            }
+
+            // Having sent the "signal" message, we expect to receive "exit-signal"
+            // and then a close message. But since a server may not implement signals,
+            // we can't guarantee that, so we wait a short time for that to happen and
+            // if it doesn't, just set the WaitHandle ourselves to unblock EndExecute.
+
+            if (!asyncResult.AsyncWaitHandle.WaitOne(millisecondsTimeout))
+            {
+                SetAsyncComplete(asyncResult);
+            }
         }
 
         /// <summary>
@@ -394,88 +463,73 @@ namespace Renci.SshNet
             return Execute();
         }
 
-        private IChannelSession CreateChannel()
+        private void Session_Disconnected(object? sender, EventArgs e)
         {
-            var channel = _session.CreateChannelSession();
-            channel.DataReceived += Channel_DataReceived;
-            channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
-            channel.RequestReceived += Channel_RequestReceived;
-            channel.Closed += Channel_Closed;
-            return channel;
+            SetAsyncComplete(new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost));
         }
 
-        private void Session_Disconnected(object sender, EventArgs e)
+        private void Session_ErrorOccured(object? sender, ExceptionEventArgs e)
         {
-            // If objected is disposed or being disposed don't handle this event
-            if (_isDisposed)
-            {
-                return;
-            }
-
-            _exception = new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost);
-
-            _ = _sessionErrorOccuredWaitHandle.Set();
+            SetAsyncComplete(e.Exception);
         }
 
-        private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
+        private void SetAsyncComplete(object result)
         {
-            // If objected is disposed or being disposed don't handle this event
-            if (_isDisposed)
+            _ = Interlocked.CompareExchange(ref _result, result, comparand: null);
+
+            if (_asyncResult is CommandAsyncResult asyncResult)
             {
-                return;
+                SetAsyncComplete(asyncResult);
             }
-
-            _exception = e.Exception;
-
-            _ = _sessionErrorOccuredWaitHandle.Set();
         }
 
-        private void SetAsyncComplete()
+        private void SetAsyncComplete(CommandAsyncResult asyncResult)
         {
-            OutputStream?.Dispose();
-            ExtendedOutputStream?.Dispose();
+            UnsubscribeFromEventsAndDisposeChannel();
+
+            OutputStream.Dispose();
+            ExtendedOutputStream.Dispose();
 
-            _asyncResult.IsCompleted = true;
+            asyncResult.IsCompleted = true;
 
-            if (_callback is not null && !_isCancelled)
+            _ = ((EventWaitHandle)asyncResult.AsyncWaitHandle).Set();
+
+            if (Interlocked.Exchange(ref _callback, value: null) is AsyncCallback callback)
             {
-                // Execute callback on different thread
-                ThreadAbstraction.ExecuteThread(() => _callback(_asyncResult));
+                ThreadAbstraction.ExecuteThread(() => callback(asyncResult));
             }
-
-            _ = ((EventWaitHandle)_asyncResult.AsyncWaitHandle).Set();
         }
 
-        private void Channel_Closed(object sender, ChannelEventArgs e)
+        private void Channel_Closed(object? sender, ChannelEventArgs e)
         {
-            SetAsyncComplete();
+            SetAsyncComplete(CompletedResult);
         }
 
-        private void Channel_RequestReceived(object sender, ChannelRequestEventArgs e)
+        private void Channel_RequestReceived(object? sender, ChannelRequestEventArgs e)
         {
             if (e.Info is ExitStatusRequestInfo exitStatusInfo)
             {
-                ExitStatus = (int)exitStatusInfo.ExitStatus;
+                _exitStatus = (int)exitStatusInfo.ExitStatus;
+                _haveExitStatus = true;
 
-                if (exitStatusInfo.WantReply)
-                {
-                    var replyMessage = new ChannelSuccessMessage(_channel.RemoteChannelNumber);
-                    _session.SendMessage(replyMessage);
-                }
+                Debug.Assert(!exitStatusInfo.WantReply, "exit-status is want_reply := false by definition.");
             }
-            else
+            else if (e.Info is ExitSignalRequestInfo exitSignalInfo)
             {
-                if (e.Info.WantReply)
-                {
-                    var replyMessage = new ChannelFailureMessage(_channel.RemoteChannelNumber);
-                    _session.SendMessage(replyMessage);
-                }
+                ExitSignal = exitSignalInfo.SignalName;
+
+                Debug.Assert(!exitSignalInfo.WantReply, "exit-signal is want_reply := false by definition.");
+            }
+            else if (e.Info.WantReply && _channel?.RemoteChannelNumber is uint remoteChannelNumber)
+            {
+                var replyMessage = new ChannelFailureMessage(remoteChannelNumber);
+                _session.SendMessage(replyMessage);
             }
         }
 
-        private void Channel_ExtendedDataReceived(object sender, ChannelExtendedDataEventArgs e)
+        private void Channel_ExtendedDataReceived(object? sender, ChannelExtendedDataEventArgs e)
         {
-            ExtendedOutputStream?.Write(e.Data, 0, e.Data.Length);
+            ExtendedOutputStream.Write(e.Data, 0, e.Data.Length);
 
             if (e.DataTypeCode == 1)
             {
@@ -483,64 +537,34 @@ namespace Renci.SshNet
             }
         }
 
-        private void Channel_DataReceived(object sender, ChannelDataEventArgs e)
+        private void Channel_DataReceived(object? sender, ChannelDataEventArgs e)
         {
-            OutputStream?.Write(e.Data, 0, e.Data.Length);
+            OutputStream.Write(e.Data, 0, e.Data.Length);
 
-            if (_asyncResult != null)
+            if (_asyncResult is CommandAsyncResult asyncResult)
             {
-                lock (_asyncResult)
+                lock (asyncResult)
                 {
-                    _asyncResult.BytesReceived += e.Data.Length;
+                    asyncResult.BytesReceived += e.Data.Length;
                 }
             }
         }
 
-        /// <exception cref="SshOperationTimeoutException">Command '{0}' has timed out.</exception>
-        /// <remarks>The actual command will be included in the exception message.</remarks>
-        private void WaitOnHandle(WaitHandle waitHandle)
-        {
-            var waitHandles = new[]
-                {
-                    _sessionErrorOccuredWaitHandle,
-                    waitHandle,
-                    _commandCancelledWaitHandle
-                };
-
-            var signaledElement = WaitHandle.WaitAny(waitHandles, CommandTimeout);
-            switch (signaledElement)
-            {
-                case 0:
-                    ExceptionDispatchInfo.Capture(_exception).Throw();
-                    break;
-                case 1:
-                    // Specified waithandle was signaled
-                    break;
-                case 2:
-                    _isCancelled = true;
-                    break;
-                case WaitHandle.WaitTimeout:
-                    throw new SshOperationTimeoutException(string.Format(CultureInfo.CurrentCulture, "Command '{0}' has timed out.", CommandText));
-                default:
-                    throw new SshException($"Unexpected element '{signaledElement.ToString(CultureInfo.InvariantCulture)}' signaled.");
-            }
-        }
-
         /// <summary>
         /// Unsubscribes the current <see cref="SshCommand"/> from channel events, and disposes
-        /// the <see cref="IChannel"/>.
+        /// the <see cref="_channel"/>.
         /// </summary>
-        /// <param name="channel">The channel.</param>
-        /// <remarks>
-        /// Does nothing when <paramref name="channel"/> is <see langword="null"/>.
-        /// </remarks>
-        private void UnsubscribeFromEventsAndDisposeChannel(IChannel channel)
+        private void UnsubscribeFromEventsAndDisposeChannel()
         {
+            var channel = _channel;
+
             if (channel is null)
             {
                 return;
             }
 
+            _channel = null;
+
             // unsubscribe from events as we do not want to be signaled should these get fired
             // during the dispose of the channel
             channel.DataReceived -= Channel_DataReceived;
@@ -576,66 +600,27 @@ namespace Renci.SshNet
             {
                 // unsubscribe from session events to ensure other objects that we're going to dispose
                 // are not accessed while disposing
-                var session = _session;
-                if (session != null)
-                {
-                    session.Disconnected -= Session_Disconnected;
-                    session.ErrorOccured -= Session_ErrorOccured;
-                    _session = null;
-                }
+                _session.Disconnected -= Session_Disconnected;
+                _session.ErrorOccured -= Session_ErrorOccured;
 
                 // unsubscribe from channel events to ensure other objects that we're going to dispose
                 // are not accessed while disposing
-                var channel = _channel;
-                if (channel != null)
-                {
-                    UnsubscribeFromEventsAndDisposeChannel(channel);
-                    _channel = null;
-                }
+                UnsubscribeFromEventsAndDisposeChannel();
 
-                var inputStream = _inputStream;
-                if (inputStream != null)
-                {
-                    inputStream.Dispose();
-                    _inputStream = null;
-                }
+                _inputStream?.Dispose();
+                _inputStream = null;
 
-                var outputStream = OutputStream;
-                if (outputStream != null)
-                {
-                    outputStream.Dispose();
-                    OutputStream = null;
-                }
+                OutputStream.Dispose();
+                ExtendedOutputStream.Dispose();
 
-                var extendedOutputStream = ExtendedOutputStream;
-                if (extendedOutputStream != null)
+                if (_asyncResult is not null && _result is null)
                 {
-                    extendedOutputStream.Dispose();
-                    ExtendedOutputStream = null;
+                    // In case an operation is still running, try to complete it with an ObjectDisposedException.
+                    SetAsyncComplete(new ObjectDisposedException(GetType().FullName));
                 }
 
-                var sessionErrorOccuredWaitHandle = _sessionErrorOccuredWaitHandle;
-                if (sessionErrorOccuredWaitHandle != null)
-                {
-                    sessionErrorOccuredWaitHandle.Dispose();
-                    _sessionErrorOccuredWaitHandle = null;
-                }
-
-                _commandCancelledWaitHandle?.Dispose();
-                _commandCancelledWaitHandle = null;
-
                 _isDisposed = true;
             }
         }
-
-        /// <summary>
-        /// Finalizes an instance of the <see cref="SshCommand"/> class.
-        /// Releases unmanaged resources and performs other cleanup operations before the
-        /// <see cref="SshCommand"/> is reclaimed by garbage collection.
-        /// </summary>
-        ~SshCommand()
-        {
-            Dispose(disposing: false);
-        }
     }
 }

+ 96 - 187
test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs

@@ -19,14 +19,14 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
                 client.Connect();
 
                 var testValue = Guid.NewGuid().ToString();
-                var command = client.RunCommand(string.Format("echo {0}", testValue));
+                using var command = client.RunCommand(string.Format("echo {0}", testValue));
                 var result = command.Result;
                 result = result.Substring(0, result.Length - 1);    //  Remove \n character returned by command
 
                 client.Disconnect();
                 #endregion
 
-                Assert.IsTrue(result.Equals(testValue));
+                Assert.AreEqual(testValue, result);
             }
         }
 
@@ -39,15 +39,14 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
                 client.Connect();
 
                 var testValue = Guid.NewGuid().ToString();
-                var command = string.Format("echo {0}", testValue);
-                var cmd = client.CreateCommand(command);
+                var command = string.Format("echo -n {0}", testValue);
+                using var cmd = client.CreateCommand(command);
                 var result = cmd.Execute();
-                result = result.Substring(0, result.Length - 1);    //  Remove \n character returned by command
 
                 client.Disconnect();
                 #endregion
 
-                Assert.IsTrue(result.Equals(testValue));
+                Assert.AreEqual(testValue, result);
             }
         }
 
@@ -56,75 +55,65 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
         public void Test_CancelAsync_Unfinished_Command()
         {
             using var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password);
-            #region Example SshCommand CancelAsync Unfinished Command Without Sending exit-signal
             client.Connect();
             var testValue = Guid.NewGuid().ToString();
-            var command = $"sleep 15s; echo {testValue}";
-            using var cmd = client.CreateCommand(command);
+            using var cmd = client.CreateCommand($"sleep 15s; echo {testValue}");
+
             var asyncResult = cmd.BeginExecute();
+
             cmd.CancelAsync();
+
             Assert.ThrowsException<OperationCanceledException>(() => cmd.EndExecute(asyncResult));
             Assert.IsTrue(asyncResult.IsCompleted);
-            client.Disconnect();
-            Assert.AreEqual(string.Empty, cmd.Result.Trim());
-            #endregion
+            Assert.IsTrue(asyncResult.AsyncWaitHandle.WaitOne(0));
+            Assert.AreEqual(string.Empty, cmd.Result);
+            Assert.AreEqual("TERM", cmd.ExitSignal);
+            Assert.IsNull(cmd.ExitStatus);
         }
 
         [TestMethod]
-        public async Task Test_CancelAsync_Finished_Command()
+        [Timeout(5000)]
+        public async Task Test_CancelAsync_Kill_Unfinished_Command()
         {
             using var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password);
-            #region Example SshCommand CancelAsync Finished Command
             client.Connect();
             var testValue = Guid.NewGuid().ToString();
-            var command = $"echo {testValue}";
-            using var cmd = client.CreateCommand(command);
+            using var cmd = client.CreateCommand($"sleep 15s; echo {testValue}");
+
             var asyncResult = cmd.BeginExecute();
-            while (!asyncResult.IsCompleted)
-            {
-                await Task.Delay(200);
-            }
 
-            cmd.CancelAsync();
-            cmd.EndExecute(asyncResult);
-            client.Disconnect();
+            Task<string> executeTask = Task.Factory.FromAsync(asyncResult, cmd.EndExecute);
 
+            cmd.CancelAsync(forceKill: true);
+
+            await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => executeTask);
             Assert.IsTrue(asyncResult.IsCompleted);
-            Assert.AreEqual(testValue, cmd.Result.Trim());
-            #endregion
+            Assert.IsTrue(asyncResult.AsyncWaitHandle.WaitOne(0));
+            Assert.AreEqual(string.Empty, cmd.Result);
+            Assert.AreEqual("KILL", cmd.ExitSignal);
+            Assert.IsNull(cmd.ExitStatus);
         }
 
         [TestMethod]
-        public void Test_Execute_OutputStream()
+        public void Test_CancelAsync_Finished_Command()
         {
-            using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
-            {
-                #region Example SshCommand CreateCommand Execute OutputStream
-                client.Connect();
-
-                var cmd = client.CreateCommand("ls -l");   //  very long list
-                var asynch = cmd.BeginExecute();
-
-                var reader = new StreamReader(cmd.OutputStream);
-
-                while (!asynch.IsCompleted)
-                {
-                    var result = reader.ReadToEnd();
-                    if (string.IsNullOrEmpty(result))
-                    {
-                        continue;
-                    }
+            using var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password);
+            client.Connect();
+            var testValue = Guid.NewGuid().ToString();
+            using var cmd = client.CreateCommand($"echo -n {testValue}");
 
-                    Console.Write(result);
-                }
+            var asyncResult = cmd.BeginExecute();
 
-                _ = cmd.EndExecute(asynch);
+            Assert.IsTrue(asyncResult.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
 
-                client.Disconnect();
-                #endregion
+            cmd.CancelAsync(); // Should not throw
+            Assert.AreEqual(testValue, cmd.EndExecute(asyncResult)); // Should not throw
+            cmd.CancelAsync(); // Should not throw
 
-                Assert.Inconclusive();
-            }
+            Assert.IsTrue(asyncResult.IsCompleted);
+            Assert.AreEqual(testValue, cmd.Result);
+            Assert.AreEqual(0, cmd.ExitStatus);
+            Assert.IsNull(cmd.ExitSignal);
         }
 
         [TestMethod]
@@ -135,51 +124,33 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
                 #region Example SshCommand CreateCommand Execute ExtendedOutputStream
 
                 client.Connect();
-                var cmd = client.CreateCommand("echo 12345; echo 654321 >&2");
-                var result = cmd.Execute();
-
-                Console.Write(result);
+                using var cmd = client.CreateCommand("echo 12345; echo 654321 >&2");
+                using var reader = new StreamReader(cmd.ExtendedOutputStream);
 
-                var reader = new StreamReader(cmd.ExtendedOutputStream);
-                Console.WriteLine("DEBUG:");
-                Console.Write(reader.ReadToEnd());
+                Assert.AreEqual("12345\n", cmd.Execute());
+                Assert.AreEqual("654321\n", reader.ReadToEnd());
 
                 client.Disconnect();
 
                 #endregion
-
-                Assert.Inconclusive();
             }
         }
 
         [TestMethod]
-        [ExpectedException(typeof(SshOperationTimeoutException))]
         public void Test_Execute_Timeout()
         {
             using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
             {
                 #region Example SshCommand CreateCommand Execute CommandTimeout
                 client.Connect();
-                var cmd = client.CreateCommand("sleep 10s");
-                cmd.CommandTimeout = TimeSpan.FromSeconds(5);
-                cmd.Execute();
+                using var cmd = client.CreateCommand("sleep 10s");
+                cmd.CommandTimeout = TimeSpan.FromSeconds(2);
+                Assert.ThrowsException<SshOperationTimeoutException>(cmd.Execute);
                 client.Disconnect();
                 #endregion
             }
         }
 
-        [TestMethod]
-        public void Test_Execute_Infinite_Timeout()
-        {
-            using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
-            {
-                client.Connect();
-                var cmd = client.CreateCommand("sleep 10s");
-                cmd.Execute();
-                client.Disconnect();
-            }
-        }
-
         [TestMethod]
         public void Test_Execute_InvalidCommand()
         {
@@ -187,7 +158,7 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
             {
                 client.Connect();
 
-                var cmd = client.CreateCommand(";");
+                using var cmd = client.CreateCommand(";");
                 cmd.Execute();
                 if (string.IsNullOrEmpty(cmd.Error))
                 {
@@ -205,7 +176,7 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
             using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
             {
                 client.Connect();
-                var cmd = client.CreateCommand(";");
+                using var cmd = client.CreateCommand(";");
                 cmd.Execute();
                 if (string.IsNullOrEmpty(cmd.Error))
                 {
@@ -221,24 +192,6 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
             }
         }
 
-        [TestMethod]
-        public void Test_Execute_Command_with_ExtendedOutput()
-        {
-            using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
-            {
-                client.Connect();
-                var cmd = client.CreateCommand("echo 12345; echo 654321 >&2");
-                cmd.Execute();
-
-                //var extendedData = Encoding.ASCII.GetString(cmd.ExtendedOutputStream.ToArray());
-                var extendedData = new StreamReader(cmd.ExtendedOutputStream, Encoding.ASCII).ReadToEnd();
-                client.Disconnect();
-
-                Assert.AreEqual("12345\n", cmd.Result);
-                Assert.AreEqual("654321\n", extendedData);
-            }
-        }
-
         [TestMethod]
         public void Test_Execute_Command_Reconnect_Execute_Command()
         {
@@ -261,17 +214,12 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
         {
             using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
             {
-                #region Example SshCommand RunCommand ExitStatus
                 client.Connect();
 
-                var cmd = client.RunCommand("exit 128");
+                using var cmd = client.RunCommand("exit 128");
 
-                Console.WriteLine(cmd.ExitStatus);
-
-                client.Disconnect();
-                #endregion
-
-                Assert.IsTrue(cmd.ExitStatus == 128);
+                Assert.AreEqual(128, cmd.ExitStatus);
+                Assert.IsNull(cmd.ExitSignal);
             }
         }
 
@@ -282,61 +230,45 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
             {
                 client.Connect();
 
-                var cmd = client.CreateCommand("sleep 5s; echo 'test'");
-                var asyncResult = cmd.BeginExecute(null, null);
-                while (!asyncResult.IsCompleted)
-                {
-                    Thread.Sleep(100);
-                }
-
-                cmd.EndExecute(asyncResult);
-
-                Assert.IsTrue(cmd.Result == "test\n");
-
-                client.Disconnect();
-            }
-        }
+                using var callbackCalled = new ManualResetEventSlim();
 
-        [TestMethod]
-        public void Test_Execute_Command_Asynchronously_With_Error()
-        {
-            using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
-            {
-                client.Connect();
+                using var cmd = client.CreateCommand("sleep 2s; echo 'test'");
+                var asyncResult = cmd.BeginExecute(new AsyncCallback((s) =>
+                {
+                    callbackCalled.Set();
+                }), state: null);
 
-                var cmd = client.CreateCommand("sleep 5s; ;");
-                var asyncResult = cmd.BeginExecute(null, null);
                 while (!asyncResult.IsCompleted)
                 {
                     Thread.Sleep(100);
                 }
 
+                Assert.IsTrue(asyncResult.AsyncWaitHandle.WaitOne(0));
+
                 cmd.EndExecute(asyncResult);
 
-                Assert.IsFalse(string.IsNullOrEmpty(cmd.Error));
+                Assert.AreEqual("test\n", cmd.Result);
+                Assert.IsTrue(callbackCalled.Wait(TimeSpan.FromSeconds(1)));
 
                 client.Disconnect();
             }
         }
 
         [TestMethod]
-        public void Test_Execute_Command_Asynchronously_With_Callback()
+        public void Test_Execute_Command_Asynchronously_With_Error()
         {
             using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
             {
                 client.Connect();
 
-                using var callbackCalled = new ManualResetEventSlim();
+                using var cmd = client.CreateCommand("sleep 2s; ;");
+                var asyncResult = cmd.BeginExecute(null, null);
 
-                using var cmd = client.CreateCommand("sleep 5s; echo 'test'");
-                var asyncResult = cmd.BeginExecute(new AsyncCallback((s) =>
-                {
-                    callbackCalled.Set();
-                }), null);
+                Assert.IsTrue(asyncResult.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
 
                 cmd.EndExecute(asyncResult);
 
-                Assert.IsTrue(callbackCalled.Wait(TimeSpan.FromSeconds(1)));
+                Assert.IsFalse(string.IsNullOrEmpty(cmd.Error));
 
                 client.Disconnect();
             }
@@ -353,7 +285,7 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
                 int callbackThreadId = 0;
                 using var callbackCalled = new ManualResetEventSlim();
 
-                using var cmd = client.CreateCommand("sleep 5s; echo 'test'");
+                using var cmd = client.CreateCommand("sleep 2s; echo 'test'");
                 var asyncResult = cmd.BeginExecute(new AsyncCallback((s) =>
                 {
                     callbackThreadId = Thread.CurrentThread.ManagedThreadId;
@@ -379,7 +311,7 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
             using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
             {
                 client.Connect();
-                var cmd = client.CreateCommand("echo 12345");
+                using var cmd = client.CreateCommand("echo 12345");
                 cmd.Execute();
                 Assert.AreEqual("12345\n", cmd.Result);
                 cmd.Execute("echo 23456");
@@ -394,35 +326,22 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
             using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
             {
                 client.Connect();
-                var cmd = client.CreateCommand("ls -l");
-
-                Assert.IsTrue(string.IsNullOrEmpty(cmd.Result));
-                client.Disconnect();
-            }
-        }
-
-        [TestMethod]
-        public void Test_Get_Error_Without_Execution()
-        {
-            using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
-            {
-                client.Connect();
-                var cmd = client.CreateCommand("ls -l");
+                using var cmd = client.CreateCommand("ls -l");
 
-                Assert.IsTrue(string.IsNullOrEmpty(cmd.Error));
+                Assert.AreEqual(string.Empty, cmd.Result);
+                Assert.AreEqual(string.Empty, cmd.Error);
                 client.Disconnect();
             }
         }
 
         [WorkItem(703), TestMethod]
-        [ExpectedException(typeof(ArgumentNullException))]
         public void Test_EndExecute_Before_BeginExecute()
         {
             using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
             {
                 client.Connect();
-                var cmd = client.CreateCommand("ls -l");
-                cmd.EndExecute(null);
+                using var cmd = client.CreateCommand("ls -l");
+                Assert.ThrowsException<ArgumentNullException>(() => cmd.EndExecute(null));
                 client.Disconnect();
             }
         }
@@ -442,15 +361,12 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
 
                 client.Connect();
 
-                var cmd = client.CreateCommand("sleep 15s;echo 123"); // Perform long running task
+                using var cmd = client.CreateCommand("sleep 2s;echo 123"); // Perform long running task
 
                 var asynch = cmd.BeginExecute();
 
-                while (!asynch.IsCompleted)
-                {
-                    //  Waiting for command to complete...
-                    Thread.Sleep(2000);
-                }
+                Assert.IsTrue(asynch.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+
                 result = cmd.EndExecute(asynch);
                 client.Disconnect();
 
@@ -461,30 +377,6 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
             }
         }
 
-        [TestMethod]
-        public void Test_Execute_Invalid_Command()
-        {
-            using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
-            {
-                #region Example SshCommand CreateCommand Error
-
-                client.Connect();
-
-                var cmd = client.CreateCommand(";");
-                cmd.Execute();
-                if (!string.IsNullOrEmpty(cmd.Error))
-                {
-                    Console.WriteLine(cmd.Error);
-                }
-
-                client.Disconnect();
-
-                #endregion
-
-                Assert.Inconclusive();
-            }
-        }
-
         [TestMethod]
 
         public void Test_MultipleThread_100_MultipleConnections()
@@ -542,13 +434,30 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
             }
         }
 
+        [TestMethod]
+        public void Test_ExecuteAsync_Dispose_CommandFinishes()
+        {
+            using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
+            {
+                client.Connect();
+
+                var cmd = client.CreateCommand("sleep 5s");
+                var asyncResult = cmd.BeginExecute(null, null);
+
+                cmd.Dispose();
+
+                Assert.IsTrue(asyncResult.AsyncWaitHandle.WaitOne(0));
+
+                Assert.ThrowsException<ObjectDisposedException>(() => cmd.EndExecute(asyncResult));
+            }
+        }
+
         private static bool ExecuteTestCommand(SshClient s)
         {
             var testValue = Guid.NewGuid().ToString();
-            var command = string.Format("echo {0}", testValue);
-            var cmd = s.CreateCommand(command);
+            var command = string.Format("echo -n {0}", testValue);
+            using var cmd = s.CreateCommand(command);
             var result = cmd.Execute();
-            result = result.Substring(0, result.Length - 1);    //  Remove \n character returned by command
             return result.Equals(testValue);
         }
     }

+ 0 - 4
test/Renci.SshNet.Tests/Classes/ShellStreamTest_ReadExpect.cs

@@ -390,10 +390,6 @@ namespace Renci.SshNet.Tests.Classes
 
             public bool SendExecRequest(string command) => throw new NotImplementedException();
 
-            public bool SendExitSignalRequest(string signalName, bool coreDumped, string errorMessage, string language) => throw new NotImplementedException();
-
-            public bool SendExitStatusRequest(uint exitStatus) => throw new NotImplementedException();
-
             public bool SendKeepAliveRequest() => throw new NotImplementedException();
 
             public bool SendLocalFlowRequest(bool clientCanDo) => throw new NotImplementedException();

+ 0 - 12
test/Renci.SshNet.Tests/Classes/SshCommandTest_Dispose.cs

@@ -64,24 +64,12 @@ namespace Renci.SshNet.Tests.Classes
             _channelSessionMock.Verify(p => p.Dispose(), Times.Once);
         }
 
-        [TestMethod]
-        public void OutputStreamShouldReturnNull()
-        {
-            Assert.IsNull(_sshCommand.OutputStream);
-        }
-
         [TestMethod]
         public void OutputStreamShouldHaveBeenDisposed()
         {
             Assert.AreEqual(-1, _outputStream.ReadByte());
         }
 
-        [TestMethod]
-        public void ExtendedOutputStreamShouldReturnNull()
-        {
-            Assert.IsNull(_sshCommand.ExtendedOutputStream);
-        }
-
         [TestMethod]
         public void ExtendedOutputStreamShouldHaveBeenDisposed()
         {

+ 1 - 2
test/Renci.SshNet.Tests/Classes/SshCommandTest_EndExecute_AsyncResultFromOtherInstance.cs

@@ -85,8 +85,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             Assert.IsNotNull(_actualException);
             Assert.IsNull(_actualException.InnerException);
-            Assert.AreEqual(string.Format("The {0} object was not returned from the corresponding asynchronous method on this class.", nameof(IAsyncResult)), _actualException.Message);
-            Assert.IsNull(_actualException.ParamName);
+            Assert.AreEqual("asyncResult", _actualException.ParamName);
         }
     }
 }

+ 2 - 13
test/Renci.SshNet.Tests/Classes/SshCommandTest_EndExecute_ChannelOpen.cs

@@ -94,20 +94,9 @@ namespace Renci.SshNet.Tests.Classes
         }
 
         [TestMethod]
-        public void EndExecuteShouldThrowArgumentExceptionWhenInvokedAgainWithSameAsyncResult()
+        public void EndExecuteShouldNotThrowWhenInvokedAgainWithSameAsyncResult()
         {
-            try
-            {
-                _sshCommand.EndExecute(_asyncResult);
-                Assert.Fail();
-            }
-            catch (ArgumentException ex)
-            {
-                Assert.AreEqual(typeof(ArgumentException), ex.GetType());
-                Assert.IsNull(ex.InnerException);
-                Assert.AreEqual("EndExecute can only be called once for each asynchronous operation.", ex.Message);
-                Assert.IsNull(ex.ParamName);
-            }
+            Assert.AreEqual(_sshCommand.Result, _sshCommand.EndExecute(_asyncResult));
         }
 
         [TestMethod]