Cancellation and timeouts
Every network operation in SshSharp is asynchronous and takes a CancellationToken. There are no separate timeout properties to
learn: a timeout is a token that cancels itself.
When to use
Always, for anything that talks to the network. A server that accepts the TCP connection and then says nothing would otherwise be waited for until the operating system gives up, which can take minutes.
Bound connecting
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(TimeSpan.FromSeconds(15)); // TCP, key exchange and authentication together
try
{
await using var client = await SshClient.ConnectAsync("example.com", 22, options, timeout.Token);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
throw new TimeoutException("example.com did not answer within 15 seconds.");
}
One token covers the TCP connection, the key exchange and authentication. With ssh_config, ConnectTimeout applies when
connecting by alias, per hop, and fails with SshException (Reason = Timeout) instead of OperationCanceledException.
Bound a command
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(TimeSpan.FromMinutes(2));
// `timeout` on the server stops the program too; cancelling only closes this side's channel.
var result = await client.RunCommandAsync("timeout 110 ./run-migrations.sh", timeout.Token);
Stop the program when you give up
Cancelling closes the command's channel on this side; the connection stays usable. Without a terminal, OpenSSH does not signal the
program, which runs on until it next writes output (and gets SIGPIPE) or finishes. To stop it, send a signal before the channel
closes:
await using var session = await client.StartCommandAsync("./long-job.sh", cancellationToken: cancellationToken);
try
{
await session.StandardOutput.CopyToAsync(Stream.Null, cancellationToken);
await session.WaitForExitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
// Ask the program to stop before the channel closes (OpenSSH delivers signals; some servers ignore them).
await session.SendSignalAsync("TERM", CancellationToken.None);
throw;
}
Stop when the connection ends
Link your token with client.ConnectionClosed, so waits end when either side gives up:
// Stop work when either the caller gives up or the connection ends.
using var both = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, client.ConnectionClosed);
await using var session = await client.StartCommandAsync("tail -F /var/log/app.log", cancellationToken: both.Token);
using var reader = new StreamReader(session.StandardOutput);
while (await reader.ReadLineAsync(both.Token) is { } line)
{
Console.WriteLine(line);
}
What is thrown
| What happened | Exception |
|---|---|
| Your token was cancelled | OperationCanceledException (or TaskCanceledException, a subclass) |
ConnectTimeout from ssh_config passed |
SshException, Reason = Timeout |
The server missed KeepAliveCountMax keep-alives |
SshException, Reason = Timeout |
| The connection ended during the operation | SshException, Reason = ConnectionClosed |
The DI factory's ConnectTimeout passed |
TimeoutException |
Notes and pitfalls
- Distinguish your timeout from the caller's cancellation: catch
OperationCanceledExceptionwhenthe caller's token is not cancelled, as in the first example. - Dispose
CancellationTokenSources withusing; a linked source keeps a registration on its parents until disposed. - Ping has no timeout of its own.
PingAsyncwaits for an answer until the connection ends; give it a token with a limit. - Disposal is not cancellation.
DisposeAsyncon a client ends every operation on it, which then fail withSshExceptionorObjectDisposedException. Cancel first when you want a cleanOperationCanceledException.