Skip to content

Commit db3d7e8

Browse files
authored
Fix CancelAsync Cause Deadlock (#1345)
* Fix CancelAsync Cause Deadlock * Fix CancelAsync Cause Deadlock * Support manual cancelling if exit-signal does not cancel * Fix switch with duplicate case * Revert wait exit response, use existing OperationCancelledException * Not executing callback when command is cancelled
1 parent 3e6fc4f commit db3d7e8

File tree

2 files changed

+81
-19
lines changed

2 files changed

+81
-19
lines changed

src/Renci.SshNet/SshCommand.cs

+36-17
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ public class SshCommand : IDisposable
2626
private CommandAsyncResult _asyncResult;
2727
private AsyncCallback _callback;
2828
private EventWaitHandle _sessionErrorOccuredWaitHandle;
29+
private EventWaitHandle _commandCancelledWaitHandle;
2930
private Exception _exception;
3031
private StringBuilder _result;
3132
private StringBuilder _error;
3233
private bool _hasError;
3334
private bool _isDisposed;
35+
private bool _isCancelled;
3436
private ChannelInputStream _inputStream;
3537
private TimeSpan _commandTimeout;
3638

@@ -84,7 +86,7 @@ public TimeSpan CommandTimeout
8486
/// <returns>
8587
/// The stream that can be used to transfer data to the command's input stream.
8688
/// </returns>
87-
#pragma warning disable CA1859 // Use concrete types when possible for improved performance
89+
#pragma warning disable CA1859 // Use concrete types when possible for improved performance
8890
public Stream CreateInputStream()
8991
#pragma warning restore CA1859 // Use concrete types when possible for improved performance
9092
{
@@ -186,7 +188,7 @@ internal SshCommand(ISession session, string commandText, Encoding encoding)
186188
_encoding = encoding;
187189
CommandTimeout = Timeout.InfiniteTimeSpan;
188190
_sessionErrorOccuredWaitHandle = new AutoResetEvent(initialState: false);
189-
191+
_commandCancelledWaitHandle = new AutoResetEvent(initialState: false);
190192
_session.Disconnected += Session_Disconnected;
191193
_session.ErrorOccured += Session_ErrorOccured;
192194
}
@@ -249,11 +251,11 @@ public IAsyncResult BeginExecute(AsyncCallback callback, object state)
249251

250252
// Create new AsyncResult object
251253
_asyncResult = new CommandAsyncResult
252-
{
253-
AsyncWaitHandle = new ManualResetEvent(initialState: false),
254-
IsCompleted = false,
255-
AsyncState = state,
256-
};
254+
{
255+
AsyncWaitHandle = new ManualResetEvent(initialState: false),
256+
IsCompleted = false,
257+
AsyncState = state,
258+
};
257259

258260
if (_channel is not null)
259261
{
@@ -349,20 +351,25 @@ public string EndExecute(IAsyncResult asyncResult)
349351

350352
commandAsyncResult.EndCalled = true;
351353

352-
return Result;
354+
if (!_isCancelled)
355+
{
356+
return Result;
357+
}
358+
359+
SetAsyncComplete();
360+
throw new OperationCanceledException();
353361
}
354362
}
355363

356364
/// <summary>
357365
/// Cancels command execution in asynchronous scenarios.
358366
/// </summary>
359-
public void CancelAsync()
367+
/// <param name="forceKill">if true send SIGKILL instead of SIGTERM.</param>
368+
public void CancelAsync(bool forceKill = false)
360369
{
361-
if (_channel is not null && _channel.IsOpen && _asyncResult is not null)
362-
{
363-
// TODO: check with Oleg if we shouldn't dispose the channel and uninitialize it ?
364-
_channel.Dispose();
365-
}
370+
var signal = forceKill ? "KILL" : "TERM";
371+
_ = _channel?.SendExitSignalRequest(signal, coreDumped: false, "Command execution has been cancelled.", "en");
372+
_ = _commandCancelledWaitHandle?.Set();
366373
}
367374

368375
/// <summary>
@@ -430,14 +437,14 @@ private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
430437
_ = _sessionErrorOccuredWaitHandle.Set();
431438
}
432439

433-
private void Channel_Closed(object sender, ChannelEventArgs e)
440+
private void SetAsyncComplete()
434441
{
435442
OutputStream?.Flush();
436443
ExtendedOutputStream?.Flush();
437444

438445
_asyncResult.IsCompleted = true;
439446

440-
if (_callback is not null)
447+
if (_callback is not null && !_isCancelled)
441448
{
442449
// Execute callback on different thread
443450
ThreadAbstraction.ExecuteThread(() => _callback(_asyncResult));
@@ -446,6 +453,11 @@ private void Channel_Closed(object sender, ChannelEventArgs e)
446453
_ = ((EventWaitHandle) _asyncResult.AsyncWaitHandle).Set();
447454
}
448455

456+
private void Channel_Closed(object sender, ChannelEventArgs e)
457+
{
458+
SetAsyncComplete();
459+
}
460+
449461
private void Channel_RequestReceived(object sender, ChannelRequestEventArgs e)
450462
{
451463
if (e.Info is ExitStatusRequestInfo exitStatusInfo)
@@ -506,7 +518,8 @@ private void WaitOnHandle(WaitHandle waitHandle)
506518
var waitHandles = new[]
507519
{
508520
_sessionErrorOccuredWaitHandle,
509-
waitHandle
521+
waitHandle,
522+
_commandCancelledWaitHandle
510523
};
511524

512525
var signaledElement = WaitHandle.WaitAny(waitHandles, CommandTimeout);
@@ -518,6 +531,9 @@ private void WaitOnHandle(WaitHandle waitHandle)
518531
case 1:
519532
// Specified waithandle was signaled
520533
break;
534+
case 2:
535+
_isCancelled = true;
536+
break;
521537
case WaitHandle.WaitTimeout:
522538
throw new SshOperationTimeoutException(string.Format(CultureInfo.CurrentCulture, "Command '{0}' has timed out.", CommandText));
523539
default:
@@ -620,6 +636,9 @@ protected virtual void Dispose(bool disposing)
620636
_sessionErrorOccuredWaitHandle = null;
621637
}
622638

639+
_commandCancelledWaitHandle?.Dispose();
640+
_commandCancelledWaitHandle = null;
641+
623642
_isDisposed = true;
624643
}
625644
}

test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs

+45-2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,49 @@ public void Test_Execute_SingleCommand()
5151
}
5252
}
5353

54+
[TestMethod]
55+
[Timeout(5000)]
56+
public void Test_CancelAsync_Unfinished_Command()
57+
{
58+
using var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password);
59+
#region Example SshCommand CancelAsync Unfinished Command Without Sending exit-signal
60+
client.Connect();
61+
var testValue = Guid.NewGuid().ToString();
62+
var command = $"sleep 15s; echo {testValue}";
63+
using var cmd = client.CreateCommand(command);
64+
var asyncResult = cmd.BeginExecute();
65+
cmd.CancelAsync();
66+
Assert.ThrowsException<OperationCanceledException>(() => cmd.EndExecute(asyncResult));
67+
Assert.IsTrue(asyncResult.IsCompleted);
68+
client.Disconnect();
69+
Assert.AreEqual(string.Empty, cmd.Result.Trim());
70+
#endregion
71+
}
72+
73+
[TestMethod]
74+
public async Task Test_CancelAsync_Finished_Command()
75+
{
76+
using var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password);
77+
#region Example SshCommand CancelAsync Finished Command
78+
client.Connect();
79+
var testValue = Guid.NewGuid().ToString();
80+
var command = $"echo {testValue}";
81+
using var cmd = client.CreateCommand(command);
82+
var asyncResult = cmd.BeginExecute();
83+
while (!asyncResult.IsCompleted)
84+
{
85+
await Task.Delay(200);
86+
}
87+
88+
cmd.CancelAsync();
89+
cmd.EndExecute(asyncResult);
90+
client.Disconnect();
91+
92+
Assert.IsTrue(asyncResult.IsCompleted);
93+
Assert.AreEqual(testValue, cmd.Result.Trim());
94+
#endregion
95+
}
96+
5497
[TestMethod]
5598
public void Test_Execute_OutputStream()
5699
{
@@ -222,7 +265,7 @@ public void Test_Execute_Command_ExitStatus()
222265
client.Connect();
223266

224267
var cmd = client.RunCommand("exit 128");
225-
268+
226269
Console.WriteLine(cmd.ExitStatus);
227270

228271
client.Disconnect();
@@ -443,7 +486,7 @@ public void Test_Execute_Invalid_Command()
443486
}
444487

445488
[TestMethod]
446-
489+
447490
public void Test_MultipleThread_100_MultipleConnections()
448491
{
449492
try

0 commit comments

Comments
 (0)