Table of Contents

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 OperationCanceledException when the caller's token is not cancelled, as in the first example.
  • Dispose CancellationTokenSources with using; a linked source keeps a registration on its parents until disposed.
  • Ping has no timeout of its own. PingAsync waits for an answer until the connection ends; give it a token with a limit.
  • Disposal is not cancellation. DisposeAsync on a client ends every operation on it, which then fail with SshException or ObjectDisposedException. Cancel first when you want a clean OperationCanceledException.

See also