Skip to content

Commit 7c9405f

Browse files
committed
Merge branch 'develop' into dependabot
2 parents 267317e + 6d94e97 commit 7c9405f

File tree

2 files changed

+46
-18
lines changed

2 files changed

+46
-18
lines changed

src/Renci.SshNet/SshCommand.cs

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,18 @@ public Task ExecuteAsync(CancellationToken cancellationToken = default)
300300

301301
if (cancellationToken.CanBeCanceled)
302302
{
303-
_tokenRegistration = cancellationToken.Register(static cmd => ((SshCommand)cmd!).CancelAsync(), this);
303+
_tokenRegistration = cancellationToken.Register(static cmd =>
304+
{
305+
try
306+
{
307+
((SshCommand)cmd!).CancelAsync();
308+
}
309+
catch
310+
{
311+
// Swallow exceptions which would otherwise be unhandled.
312+
}
313+
},
314+
this);
304315
}
305316

306317
return _tcs.Task;
@@ -437,33 +448,31 @@ public void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500)
437448
_cancellationRequested = true;
438449
Interlocked.MemoryBarrier(); // ensure fresh read in SetAsyncComplete (possibly unnecessary)
439450

440-
// Try to send the cancellation signal.
441-
if (_channel?.SendSignalRequest(forceKill ? "KILL" : "TERM") is null)
442-
{
443-
// Command has completed (in the meantime since the last check).
444-
return;
445-
}
446-
447-
// Having sent the "signal" message, we expect to receive "exit-signal"
448-
// and then a close message. But since a server may not implement signals,
449-
// we can't guarantee that, so we wait a short time for that to happen and
450-
// if it doesn't, just complete the task ourselves to unblock waiters.
451-
452451
try
453452
{
454-
if (_tcs.Task.Wait(millisecondsTimeout))
453+
// Try to send the cancellation signal.
454+
if (_channel?.SendSignalRequest(forceKill ? "KILL" : "TERM") is null)
455455
{
456+
// Command has completed (in the meantime since the last check).
456457
return;
457458
}
459+
460+
// Having sent the "signal" message, we expect to receive "exit-signal"
461+
// and then a close message. But since a server may not implement signals,
462+
// we can't guarantee that, so we wait a short time for that to happen and
463+
// if it doesn't, just complete the task ourselves to unblock waiters.
464+
465+
_ = _tcs.Task.Wait(millisecondsTimeout);
458466
}
459467
catch (AggregateException)
460468
{
461-
// We expect to be here if the server implements signals.
469+
// We expect to be here from the call to Wait if the server implements signals.
462470
// But we don't want to propagate the exception on the task from here.
463-
return;
464471
}
465-
466-
SetAsyncComplete();
472+
finally
473+
{
474+
SetAsyncComplete();
475+
}
467476
}
468477

469478
/// <summary>

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,25 @@ public async Task Test_ExecuteAsync_Timeout()
194194
}
195195
}
196196

197+
[TestMethod]
198+
[Timeout(15000)]
199+
public async Task Test_ExecuteAsync_Disconnect()
200+
{
201+
using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
202+
{
203+
client.Connect();
204+
using var cmd = client.CreateCommand("sleep 10s");
205+
cmd.CommandTimeout = TimeSpan.FromSeconds(2);
206+
207+
Task executeTask = cmd.ExecuteAsync();
208+
209+
client.Disconnect();
210+
211+
// Waiting for timeout is not optimal here, but better than hanging indefinitely.
212+
await Assert.ThrowsExceptionAsync<SshOperationTimeoutException>(() => executeTask);
213+
}
214+
}
215+
197216
[TestMethod]
198217
public void Test_Execute_InvalidCommand()
199218
{

0 commit comments

Comments
 (0)