From f7f50a42403040991fc8e209c8dcb219e78e2b77 Mon Sep 17 00:00:00 2001 From: zeotuan Date: Thu, 7 Mar 2024 11:37:55 +1100 Subject: [PATCH 1/6] Fix CancelAsync Cause Deadlock --- src/Renci.SshNet/SshCommand.cs | 21 ++++++++------ .../OldIntegrationTests/SshCommandTest.cs | 28 +++++++++++++++++-- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index b348c4ec9..0614d9439 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -26,6 +26,7 @@ public class SshCommand : IDisposable private CommandAsyncResult _asyncResult; private AsyncCallback _callback; private EventWaitHandle _sessionErrorOccuredWaitHandle; + private EventWaitHandle _commandCancelledWaitHandle; private Exception _exception; private StringBuilder _result; private StringBuilder _error; @@ -186,7 +187,7 @@ internal SshCommand(ISession session, string commandText, Encoding encoding) _encoding = encoding; CommandTimeout = Session.InfiniteTimeSpan; _sessionErrorOccuredWaitHandle = new AutoResetEvent(initialState: false); - + _commandCancelledWaitHandle = new AutoResetEvent(initialState: false); _session.Disconnected += Session_Disconnected; _session.ErrorOccured += Session_ErrorOccured; } @@ -356,13 +357,12 @@ public string EndExecute(IAsyncResult asyncResult) /// /// Cancels command execution in asynchronous scenarios. /// - public void CancelAsync() + /// if true send SIGKILL instead of SIGTERM. + public void CancelAsync(bool forceKill = false) { - if (_channel is not null && _channel.IsOpen && _asyncResult is not null) - { - // TODO: check with Oleg if we shouldn't dispose the channel and uninitialize it ? - _channel.Dispose(); - } + var signal = forceKill ? "KILL" : "TERM"; + _ = _channel?.SendExitSignalRequest(signal, coreDumped: false, "Command execution has been cancelled.", "en"); + _ = _commandCancelledWaitHandle.Set(); } /// @@ -506,6 +506,7 @@ private void WaitOnHandle(WaitHandle waitHandle) var waitHandles = new[] { _sessionErrorOccuredWaitHandle, + _commandCancelledWaitHandle, waitHandle }; @@ -515,7 +516,8 @@ private void WaitOnHandle(WaitHandle waitHandle) case 0: ExceptionDispatchInfo.Capture(_exception).Throw(); break; - case 1: + case 1: // Command cancelled + case 2: // Specified waithandle was signaled break; case WaitHandle.WaitTimeout: @@ -620,6 +622,9 @@ protected virtual void Dispose(bool disposing) _sessionErrorOccuredWaitHandle = null; } + _commandCancelledWaitHandle?.Dispose(); + _commandCancelledWaitHandle = null; + _isDisposed = true; } } diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs index aefe1d6d0..6ca0be6a2 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs @@ -51,6 +51,30 @@ public void Test_Execute_SingleCommand() } } + [TestMethod] + [Timeout(5000)] + public void Test_CancelAsync_Running_Command() + { + using var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password); + #region Example SshCommand CancelAsync + client.Connect(); + var testValue = Guid.NewGuid().ToString(); + var command = $"sleep 10s; echo {testValue}"; + using var cmd = client.CreateCommand(command); + try + { + var asyncResult = cmd.BeginExecute(); + cmd.CancelAsync(); + cmd.EndExecute(asyncResult); + } + catch (OperationCanceledException) + { + } + client.Disconnect(); + Assert.AreNotEqual(cmd.Result.Trim(), testValue); + #endregion + } + [TestMethod] public void Test_Execute_OutputStream() { @@ -222,7 +246,7 @@ public void Test_Execute_Command_ExitStatus() client.Connect(); var cmd = client.RunCommand("exit 128"); - + Console.WriteLine(cmd.ExitStatus); client.Disconnect(); @@ -443,7 +467,7 @@ public void Test_Execute_Invalid_Command() } [TestMethod] - + public void Test_MultipleThread_100_MultipleConnections() { try From 43a57e1229537b612c112c8b86feefebb7a8f342 Mon Sep 17 00:00:00 2001 From: zeotuan Date: Thu, 7 Mar 2024 11:37:55 +1100 Subject: [PATCH 2/6] Fix CancelAsync Cause Deadlock --- src/Renci.SshNet/SshCommand.cs | 21 ++++++++------ .../OldIntegrationTests/SshCommandTest.cs | 28 +++++++++++++++++-- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index b348c4ec9..0614d9439 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -26,6 +26,7 @@ public class SshCommand : IDisposable private CommandAsyncResult _asyncResult; private AsyncCallback _callback; private EventWaitHandle _sessionErrorOccuredWaitHandle; + private EventWaitHandle _commandCancelledWaitHandle; private Exception _exception; private StringBuilder _result; private StringBuilder _error; @@ -186,7 +187,7 @@ internal SshCommand(ISession session, string commandText, Encoding encoding) _encoding = encoding; CommandTimeout = Session.InfiniteTimeSpan; _sessionErrorOccuredWaitHandle = new AutoResetEvent(initialState: false); - + _commandCancelledWaitHandle = new AutoResetEvent(initialState: false); _session.Disconnected += Session_Disconnected; _session.ErrorOccured += Session_ErrorOccured; } @@ -356,13 +357,12 @@ public string EndExecute(IAsyncResult asyncResult) /// /// Cancels command execution in asynchronous scenarios. /// - public void CancelAsync() + /// if true send SIGKILL instead of SIGTERM. + public void CancelAsync(bool forceKill = false) { - if (_channel is not null && _channel.IsOpen && _asyncResult is not null) - { - // TODO: check with Oleg if we shouldn't dispose the channel and uninitialize it ? - _channel.Dispose(); - } + var signal = forceKill ? "KILL" : "TERM"; + _ = _channel?.SendExitSignalRequest(signal, coreDumped: false, "Command execution has been cancelled.", "en"); + _ = _commandCancelledWaitHandle.Set(); } /// @@ -506,6 +506,7 @@ private void WaitOnHandle(WaitHandle waitHandle) var waitHandles = new[] { _sessionErrorOccuredWaitHandle, + _commandCancelledWaitHandle, waitHandle }; @@ -515,7 +516,8 @@ private void WaitOnHandle(WaitHandle waitHandle) case 0: ExceptionDispatchInfo.Capture(_exception).Throw(); break; - case 1: + case 1: // Command cancelled + case 2: // Specified waithandle was signaled break; case WaitHandle.WaitTimeout: @@ -620,6 +622,9 @@ protected virtual void Dispose(bool disposing) _sessionErrorOccuredWaitHandle = null; } + _commandCancelledWaitHandle?.Dispose(); + _commandCancelledWaitHandle = null; + _isDisposed = true; } } diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs index aefe1d6d0..6ca0be6a2 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs @@ -51,6 +51,30 @@ public void Test_Execute_SingleCommand() } } + [TestMethod] + [Timeout(5000)] + public void Test_CancelAsync_Running_Command() + { + using var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password); + #region Example SshCommand CancelAsync + client.Connect(); + var testValue = Guid.NewGuid().ToString(); + var command = $"sleep 10s; echo {testValue}"; + using var cmd = client.CreateCommand(command); + try + { + var asyncResult = cmd.BeginExecute(); + cmd.CancelAsync(); + cmd.EndExecute(asyncResult); + } + catch (OperationCanceledException) + { + } + client.Disconnect(); + Assert.AreNotEqual(cmd.Result.Trim(), testValue); + #endregion + } + [TestMethod] public void Test_Execute_OutputStream() { @@ -222,7 +246,7 @@ public void Test_Execute_Command_ExitStatus() client.Connect(); var cmd = client.RunCommand("exit 128"); - + Console.WriteLine(cmd.ExitStatus); client.Disconnect(); @@ -443,7 +467,7 @@ public void Test_Execute_Invalid_Command() } [TestMethod] - + public void Test_MultipleThread_100_MultipleConnections() { try From 2c9ef236ddb4f73bf8d2dea341b760444983a3a5 Mon Sep 17 00:00:00 2001 From: zeotuan Date: Sun, 17 Mar 2024 15:16:45 +1100 Subject: [PATCH 3/6] Support manual cancelling if exit-signal does not cancel --- .../Common/SshOperationCancelledException.cs | 56 ++++++++++++++++++ src/Renci.SshNet/SshCommand.cs | 59 +++++++++++++++---- .../OldIntegrationTests/SshCommandTest.cs | 41 +++++++++---- 3 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 src/Renci.SshNet/Common/SshOperationCancelledException.cs diff --git a/src/Renci.SshNet/Common/SshOperationCancelledException.cs b/src/Renci.SshNet/Common/SshOperationCancelledException.cs new file mode 100644 index 000000000..b0a4b9ac6 --- /dev/null +++ b/src/Renci.SshNet/Common/SshOperationCancelledException.cs @@ -0,0 +1,56 @@ +using System; +#if NETFRAMEWORK +using System.Runtime.Serialization; +#endif // NETFRAMEWORK + +namespace Renci.SshNet.Common +{ + /// + /// The exception that is thrown when operation is timed out. + /// +#if NETFRAMEWORK + [Serializable] +#endif // NETFRAMEWORK + public class SshOperationCancelledException : SshException + { + /// + /// Initializes a new instance of the class. + /// + public SshOperationCancelledException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + public SshOperationCancelledException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The inner exception. + public SshOperationCancelledException(string message, Exception innerException) + : base(message, innerException) + { + } + +#if NETFRAMEWORK + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + /// The parameter is . + /// The class name is or is zero (0). + protected SshOperationCancelledException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +#endif // NETFRAMEWORK + } +} diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index 0614d9439..5c6cfc881 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -4,6 +4,7 @@ using System.Runtime.ExceptionServices; using System.Text; using System.Threading; +using System.Threading.Tasks; using Renci.SshNet.Abstractions; using Renci.SshNet.Channels; @@ -32,6 +33,7 @@ public class SshCommand : IDisposable private StringBuilder _error; private bool _hasError; private bool _isDisposed; + private bool _isCancelled; private ChannelInputStream _inputStream; private TimeSpan _commandTimeout; @@ -350,19 +352,49 @@ public string EndExecute(IAsyncResult asyncResult) commandAsyncResult.EndCalled = true; - return Result; + if (!_isCancelled) + { + return Result; + } + + SetAsyncComplete(); + throw new SshOperationCancelledException(); } } /// /// Cancels command execution in asynchronous scenarios. /// + /// should exit-signal be sent before attempting to close channel. /// if true send SIGKILL instead of SIGTERM. - public void CancelAsync(bool forceKill = false) + /// how long to wait before stop waiting for command and close the channel. + /// + /// Command Cancellation Task. + /// + /// + /// + /// After sending the exit-signal to the recipient, wait until either is exceeded + /// or the async result is signaled before signaling command cancellation. + /// If the exit-signal always results in the command being cancelled by the recipient, then + /// can be set to to wait until the async result is signaled. + /// + /// + public Task CancelAsync(bool signalBeforeClose = true, bool forceKill = false, TimeSpan timeout = default) { - var signal = forceKill ? "KILL" : "TERM"; - _ = _channel?.SendExitSignalRequest(signal, coreDumped: false, "Command execution has been cancelled.", "en"); - _ = _commandCancelledWaitHandle.Set(); + if (signalBeforeClose) + { + var signal = forceKill ? "KILL" : "TERM"; + _ = _channel?.SendExitSignalRequest(signal, coreDumped: false, "Command execution has been cancelled.", "en"); + } + + return Task.Run(() => + { + var signaledElement = WaitHandle.WaitAny(new[] { _asyncResult.AsyncWaitHandle }, timeout); + if (signaledElement == WaitHandle.WaitTimeout) + { + _ = _commandCancelledWaitHandle?.Set(); + } + }); } /// @@ -430,7 +462,7 @@ private void Session_ErrorOccured(object sender, ExceptionEventArgs e) _ = _sessionErrorOccuredWaitHandle.Set(); } - private void Channel_Closed(object sender, ChannelEventArgs e) + private void SetAsyncComplete() { OutputStream?.Flush(); ExtendedOutputStream?.Flush(); @@ -446,6 +478,11 @@ private void Channel_Closed(object sender, ChannelEventArgs e) _ = ((EventWaitHandle) _asyncResult.AsyncWaitHandle).Set(); } + private void Channel_Closed(object sender, ChannelEventArgs e) + { + SetAsyncComplete(); + } + private void Channel_RequestReceived(object sender, ChannelRequestEventArgs e) { if (e.Info is ExitStatusRequestInfo exitStatusInfo) @@ -506,8 +543,8 @@ private void WaitOnHandle(WaitHandle waitHandle) var waitHandles = new[] { _sessionErrorOccuredWaitHandle, - _commandCancelledWaitHandle, - waitHandle + waitHandle, + _commandCancelledWaitHandle }; var signaledElement = WaitHandle.WaitAny(waitHandles, CommandTimeout); @@ -516,10 +553,12 @@ private void WaitOnHandle(WaitHandle waitHandle) case 0: ExceptionDispatchInfo.Capture(_exception).Throw(); break; - case 1: // Command cancelled - case 2: + 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: diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs index 6ca0be6a2..323d7d93c 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs @@ -53,25 +53,44 @@ public void Test_Execute_SingleCommand() [TestMethod] [Timeout(5000)] - public void Test_CancelAsync_Running_Command() + public void Test_CancelAsync_Unfinished_Command() { using var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password); - #region Example SshCommand CancelAsync + #region Example SshCommand CancelAsync Unfinished Command Without Sending exit-signal client.Connect(); var testValue = Guid.NewGuid().ToString(); - var command = $"sleep 10s; echo {testValue}"; + var command = $"sleep 15s; echo {testValue}"; using var cmd = client.CreateCommand(command); - try - { - var asyncResult = cmd.BeginExecute(); - cmd.CancelAsync(); - cmd.EndExecute(asyncResult); - } - catch (OperationCanceledException) + var asyncResult = cmd.BeginExecute(); + _ = cmd.CancelAsync(signalBeforeClose: false); + Assert.ThrowsException(() => cmd.EndExecute(asyncResult)); + Assert.IsTrue(asyncResult.IsCompleted); + client.Disconnect(); + Assert.AreEqual(string.Empty, cmd.Result.Trim()); + #endregion + } + + [TestMethod] + public async Task Test_CancelAsync_Finished_Command() + { + using var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password); + #region Example SshCommand CancelAsync Finished Command Without Sending exit-signal + client.Connect(); + var testValue = Guid.NewGuid().ToString(); + var command = $"echo {testValue}"; + using var cmd = client.CreateCommand(command); + var asyncResult = cmd.BeginExecute(); + while (!asyncResult.IsCompleted) { + await Task.Delay(200); } + + _ = cmd.CancelAsync(signalBeforeClose: false); + cmd.EndExecute(asyncResult); client.Disconnect(); - Assert.AreNotEqual(cmd.Result.Trim(), testValue); + + Assert.IsTrue(asyncResult.IsCompleted); + Assert.AreEqual(testValue, cmd.Result.Trim()); #endregion } From f41861432bb621ca3922a3edea4ab730b0b79f50 Mon Sep 17 00:00:00 2001 From: Tuan Pham Date: Sun, 17 Mar 2024 15:32:06 +1100 Subject: [PATCH 4/6] Fix switch with duplicate case --- src/Renci.SshNet/SshCommand.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index c397c9502..b9d3476c8 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -553,8 +553,7 @@ private void WaitOnHandle(WaitHandle waitHandle) case 0: ExceptionDispatchInfo.Capture(_exception).Throw(); break; - case 1: // Command cancelled - case 2: + case 1: // Specified waithandle was signaled break; case 2: From 78a35ce2218e65ca15277eca4929e1464c761b00 Mon Sep 17 00:00:00 2001 From: zeotuan Date: Sun, 24 Mar 2024 14:12:21 +1100 Subject: [PATCH 5/6] Revert wait exit response, use existing OperationCancelledException --- .../Common/SshOperationCancelledException.cs | 56 ------------------- src/Renci.SshNet/SshCommand.cs | 35 ++---------- .../OldIntegrationTests/SshCommandTest.cs | 8 +-- 3 files changed, 9 insertions(+), 90 deletions(-) delete mode 100644 src/Renci.SshNet/Common/SshOperationCancelledException.cs diff --git a/src/Renci.SshNet/Common/SshOperationCancelledException.cs b/src/Renci.SshNet/Common/SshOperationCancelledException.cs deleted file mode 100644 index b0a4b9ac6..000000000 --- a/src/Renci.SshNet/Common/SshOperationCancelledException.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -#if NETFRAMEWORK -using System.Runtime.Serialization; -#endif // NETFRAMEWORK - -namespace Renci.SshNet.Common -{ - /// - /// The exception that is thrown when operation is timed out. - /// -#if NETFRAMEWORK - [Serializable] -#endif // NETFRAMEWORK - public class SshOperationCancelledException : SshException - { - /// - /// Initializes a new instance of the class. - /// - public SshOperationCancelledException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message. - public SshOperationCancelledException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The inner exception. - public SshOperationCancelledException(string message, Exception innerException) - : base(message, innerException) - { - } - -#if NETFRAMEWORK - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// The parameter is . - /// The class name is or is zero (0). - protected SshOperationCancelledException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } -#endif // NETFRAMEWORK - } -} diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index b9d3476c8..77a87ef06 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -4,7 +4,6 @@ using System.Runtime.ExceptionServices; using System.Text; using System.Threading; -using System.Threading.Tasks; using Renci.SshNet.Abstractions; using Renci.SshNet.Channels; @@ -358,43 +357,19 @@ public string EndExecute(IAsyncResult asyncResult) } SetAsyncComplete(); - throw new SshOperationCancelledException(); + throw new OperationCanceledException(); } } /// /// Cancels command execution in asynchronous scenarios. /// - /// should exit-signal be sent before attempting to close channel. /// if true send SIGKILL instead of SIGTERM. - /// how long to wait before stop waiting for command and close the channel. - /// - /// Command Cancellation Task. - /// - /// - /// - /// After sending the exit-signal to the recipient, wait until either is exceeded - /// or the async result is signaled before signaling command cancellation. - /// If the exit-signal always results in the command being cancelled by the recipient, then - /// can be set to to wait until the async result is signaled. - /// - /// - public Task CancelAsync(bool signalBeforeClose = true, bool forceKill = false, TimeSpan timeout = default) + public void CancelAsync(bool forceKill = false) { - if (signalBeforeClose) - { - var signal = forceKill ? "KILL" : "TERM"; - _ = _channel?.SendExitSignalRequest(signal, coreDumped: false, "Command execution has been cancelled.", "en"); - } - - return Task.Run(() => - { - var signaledElement = WaitHandle.WaitAny(new[] { _asyncResult.AsyncWaitHandle }, timeout); - if (signaledElement == WaitHandle.WaitTimeout) - { - _ = _commandCancelledWaitHandle?.Set(); - } - }); + var signal = forceKill ? "KILL" : "TERM"; + _ = _channel?.SendExitSignalRequest(signal, coreDumped: false, "Command execution has been cancelled.", "en"); + _ = _commandCancelledWaitHandle?.Set(); } /// diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs index 323d7d93c..4273339f4 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs @@ -62,8 +62,8 @@ public void Test_CancelAsync_Unfinished_Command() var command = $"sleep 15s; echo {testValue}"; using var cmd = client.CreateCommand(command); var asyncResult = cmd.BeginExecute(); - _ = cmd.CancelAsync(signalBeforeClose: false); - Assert.ThrowsException(() => cmd.EndExecute(asyncResult)); + cmd.CancelAsync(); + Assert.ThrowsException(() => cmd.EndExecute(asyncResult)); Assert.IsTrue(asyncResult.IsCompleted); client.Disconnect(); Assert.AreEqual(string.Empty, cmd.Result.Trim()); @@ -74,7 +74,7 @@ public void Test_CancelAsync_Unfinished_Command() public async Task Test_CancelAsync_Finished_Command() { using var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password); - #region Example SshCommand CancelAsync Finished Command Without Sending exit-signal + #region Example SshCommand CancelAsync Finished Command client.Connect(); var testValue = Guid.NewGuid().ToString(); var command = $"echo {testValue}"; @@ -85,7 +85,7 @@ public async Task Test_CancelAsync_Finished_Command() await Task.Delay(200); } - _ = cmd.CancelAsync(signalBeforeClose: false); + cmd.CancelAsync(); cmd.EndExecute(asyncResult); client.Disconnect(); From 38aea9b4f44c84d54bbd3c260f1708780017bbb1 Mon Sep 17 00:00:00 2001 From: zeotuan Date: Sun, 24 Mar 2024 14:31:05 +1100 Subject: [PATCH 6/6] Not executing callback when command is cancelled --- src/Renci.SshNet/SshCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index fccaf7225..076cb901a 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -444,7 +444,7 @@ private void SetAsyncComplete() _asyncResult.IsCompleted = true; - if (_callback is not null) + if (_callback is not null && !_isCancelled) { // Execute callback on different thread ThreadAbstraction.ExecuteThread(() => _callback(_asyncResult));