Fix sftp async methods not observing error conditions (#1510)

* Fix sftp async methods not observing error conditions

* Update ISftpClient
This commit is contained in:
Rob Hague
2024-10-08 08:48:57 +02:00
committed by GitHub
parent 1a8839ec3e
commit fbedaabb9c
7 changed files with 570 additions and 343 deletions
-1
View File
@@ -56,7 +56,6 @@ namespace Renci.SshNet
/// The timeout to wait until an operation completes. The default value is negative
/// one (-1) milliseconds, which indicates an infinite timeout period.
/// </value>
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> represents a value that is less than -1 or greater than <see cref="int.MaxValue"/> milliseconds.</exception>
TimeSpan OperationTimeout { get; set; }
+3 -3
View File
@@ -11,12 +11,12 @@ namespace Renci.SshNet
internal interface ISubsystemSession : IDisposable
{
/// <summary>
/// Gets or set the number of seconds to wait for an operation to complete.
/// Gets or sets the number of milliseconds to wait for an operation to complete.
/// </summary>
/// <value>
/// The number of seconds to wait for an operation to complete, or <c>-1</c> to wait indefinitely.
/// The number of milliseconds to wait for an operation to complete, or <c>-1</c> to wait indefinitely.
/// </value>
int OperationTimeout { get; }
int OperationTimeout { get; set; }
/// <summary>
/// Gets a value indicating whether this session is open.
+238 -295
View File
@@ -523,28 +523,24 @@ namespace Renci.SshNet.Sftp
/// A task that represents the asynchronous <c>SSH_FXP_OPEN</c> request. The value of its
/// <see cref="Task{Task}.Result"/> contains the file handle of the specified path.
/// </returns>
public async Task<byte[]> RequestOpenAsync(string path, Flags flags, CancellationToken cancellationToken)
public Task<byte[]> RequestOpenAsync(string path, Flags flags, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<byte[]>(cancellationToken);
}
var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpOpenRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
flags,
response => tcs.TrySetResult(response.Handle),
response => tcs.TrySetException(GetSftpException(response))));
SendRequest(new SftpOpenRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
flags,
response => tcs.TrySetResult(response.Handle),
response => tcs.TrySetException(GetSftpException(response))));
return await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -651,8 +647,13 @@ namespace Renci.SshNet.Sftp
/// <returns>
/// A task that represents the asynchronous <c>SSH_FXP_CLOSE</c> request.
/// </returns>
public async Task RequestCloseAsync(byte[] handle, CancellationToken cancellationToken)
public Task RequestCloseAsync(byte[] handle, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpCloseRequest(ProtocolVersion,
@@ -670,17 +671,7 @@ namespace Renci.SshNet.Sftp
}
}));
// Only check for cancellation after the SftpCloseRequest was sent
cancellationToken.ThrowIfCancellationRequested();
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
_ = await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -875,38 +866,34 @@ namespace Renci.SshNet.Sftp
/// its <see cref="Task{Task}.Result"/> contains the data read from the file, or an empty
/// array when the end of the file is reached.
/// </returns>
public async Task<byte[]> RequestReadAsync(byte[] handle, ulong offset, uint length, CancellationToken cancellationToken)
public Task<byte[]> RequestReadAsync(byte[] handle, ulong offset, uint length, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<byte[]>(cancellationToken);
}
var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpReadRequest(ProtocolVersion,
NextRequestId,
handle,
offset,
length,
response => tcs.TrySetResult(response.Data),
response =>
SendRequest(new SftpReadRequest(ProtocolVersion,
NextRequestId,
handle,
offset,
length,
response => tcs.TrySetResult(response.Data),
response =>
{
if (response.StatusCode == StatusCodes.Eof)
{
if (response.StatusCode == StatusCodes.Eof)
{
_ = tcs.TrySetResult(Array.Empty<byte>());
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
_ = tcs.TrySetResult(Array.Empty<byte>());
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
return await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -972,39 +959,35 @@ namespace Renci.SshNet.Sftp
/// <returns>
/// A task that represents the asynchronous <c>SSH_FXP_WRITE</c> request.
/// </returns>
public async Task RequestWriteAsync(byte[] handle, ulong serverOffset, byte[] data, int offset, int length, CancellationToken cancellationToken)
public Task RequestWriteAsync(byte[] handle, ulong serverOffset, byte[] data, int offset, int length, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpWriteRequest(ProtocolVersion,
NextRequestId,
handle,
serverOffset,
data,
offset,
length,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
SendRequest(new SftpWriteRequest(ProtocolVersion,
NextRequestId,
handle,
serverOffset,
data,
offset,
length,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
_ = await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -1058,27 +1041,23 @@ namespace Renci.SshNet.Sftp
/// A task the represents the asynchronous <c>SSH_FXP_LSTAT</c> request. The value of its
/// <see cref="Task{SftpFileAttributes}.Result"/> contains the file attributes of the specified path.
/// </returns>
public async Task<SftpFileAttributes> RequestLStatAsync(string path, CancellationToken cancellationToken)
public Task<SftpFileAttributes> RequestLStatAsync(string path, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<SftpFileAttributes>(cancellationToken);
}
var tcs = new TaskCompletionSource<SftpFileAttributes>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpLStatRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.Attributes),
response => tcs.TrySetException(GetSftpException(response))));
SendRequest(new SftpLStatRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.Attributes),
response => tcs.TrySetException(GetSftpException(response))));
return await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -1191,26 +1170,22 @@ namespace Renci.SshNet.Sftp
/// A task that represents the asynchronous <c>SSH_FXP_FSTAT</c> request. The value of its
/// <see cref="Task{Task}.Result"/> contains the file attributes of the specified handle.
/// </returns>
public async Task<SftpFileAttributes> RequestFStatAsync(byte[] handle, CancellationToken cancellationToken)
public Task<SftpFileAttributes> RequestFStatAsync(byte[] handle, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<SftpFileAttributes>(cancellationToken);
}
var tcs = new TaskCompletionSource<SftpFileAttributes>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpFStatRequest(ProtocolVersion,
NextRequestId,
handle,
response => tcs.TrySetResult(response.Attributes),
response => tcs.TrySetException(GetSftpException(response))));
SendRequest(new SftpFStatRequest(ProtocolVersion,
NextRequestId,
handle,
response => tcs.TrySetResult(response.Attributes),
response => tcs.TrySetException(GetSftpException(response))));
return await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -1329,27 +1304,23 @@ namespace Renci.SshNet.Sftp
/// A task that represents the asynchronous <c>SSH_FXP_OPENDIR</c> request. The value of its
/// <see cref="Task{Task}.Result"/> contains the handle of the specified path.
/// </returns>
public async Task<byte[]> RequestOpenDirAsync(string path, CancellationToken cancellationToken)
public Task<byte[]> RequestOpenDirAsync(string path, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<byte[]>(cancellationToken);
}
var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpOpenDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.Handle),
response => tcs.TrySetException(GetSftpException(response))));
SendRequest(new SftpOpenDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.Handle),
response => tcs.TrySetException(GetSftpException(response))));
return await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -1410,36 +1381,32 @@ namespace Renci.SshNet.Sftp
/// <c>key</c> is the name of a file in the directory and the <c>value</c> is the <see cref="SftpFileAttributes"/>
/// of the file.
/// </returns>
public async Task<KeyValuePair<string, SftpFileAttributes>[]> RequestReadDirAsync(byte[] handle, CancellationToken cancellationToken)
public Task<KeyValuePair<string, SftpFileAttributes>[]> RequestReadDirAsync(byte[] handle, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<KeyValuePair<string, SftpFileAttributes>[]>(cancellationToken);
}
var tcs = new TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpReadDirRequest(ProtocolVersion,
NextRequestId,
handle,
response => tcs.TrySetResult(response.Files),
response =>
SendRequest(new SftpReadDirRequest(ProtocolVersion,
NextRequestId,
handle,
response => tcs.TrySetResult(response.Files),
response =>
{
if (response.StatusCode == StatusCodes.Eof)
{
if (response.StatusCode == StatusCodes.Eof)
{
_ = tcs.TrySetResult(null);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
_ = tcs.TrySetResult(null);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
return await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -1481,36 +1448,32 @@ namespace Renci.SshNet.Sftp
/// <returns>
/// A task that represents the asynchronous <c>SSH_FXP_REMOVE</c> request.
/// </returns>
public async Task RequestRemoveAsync(string path, CancellationToken cancellationToken)
public Task RequestRemoveAsync(string path, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpRemoveRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
SendRequest(new SftpRemoveRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
_ = await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -1550,36 +1513,32 @@ namespace Renci.SshNet.Sftp
/// <param name="path">The path.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous <c>SSH_FXP_MKDIR</c> operation.</returns>
public async Task RequestMkDirAsync(string path, CancellationToken cancellationToken = default)
public Task RequestMkDirAsync(string path, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpMkDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
SendRequest(new SftpMkDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
tcs.TrySetException(GetSftpException(response));
}
}));
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
_ = await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -1614,37 +1573,33 @@ namespace Renci.SshNet.Sftp
}
/// <inheritdoc />
public async Task RequestRmDirAsync(string path, CancellationToken cancellationToken = default)
public Task RequestRmDirAsync(string path, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpRmDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
SendRequest(new SftpRmDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
var exception = GetSftpException(response);
if (exception is not null)
{
var exception = GetSftpException(response);
if (exception is not null)
{
tcs.TrySetException(exception);
}
else
{
tcs.TrySetResult(true);
}
}));
_ = tcs.TrySetException(exception);
}
else
{
_ = tcs.TrySetResult(true);
}
}));
_ = await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -1691,37 +1646,33 @@ namespace Renci.SshNet.Sftp
return result;
}
internal async Task<KeyValuePair<string, SftpFileAttributes>[]> RequestRealPathAsync(string path, bool nullOnError, CancellationToken cancellationToken)
internal Task<KeyValuePair<string, SftpFileAttributes>[]> RequestRealPathAsync(string path, bool nullOnError, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<KeyValuePair<string, SftpFileAttributes>[]>(cancellationToken);
}
var tcs = new TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpRealPathRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.Files),
response =>
SendRequest(new SftpRealPathRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.Files),
response =>
{
if (nullOnError)
{
if (nullOnError)
{
_ = tcs.TrySetResult(null);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
_ = tcs.TrySetResult(null);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
return await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -1921,37 +1872,33 @@ namespace Renci.SshNet.Sftp
/// <returns>
/// A task that represents the asynchronous <c>SSH_FXP_RENAME</c> request.
/// </returns>
public async Task RequestRenameAsync(string oldPath, string newPath, CancellationToken cancellationToken)
public Task RequestRenameAsync(string oldPath, string newPath, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new SftpRenameRequest(ProtocolVersion,
NextRequestId,
oldPath,
newPath,
_encoding,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
SendRequest(new SftpRenameRequest(ProtocolVersion,
NextRequestId,
oldPath,
newPath,
_encoding,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
_ = await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
@@ -2149,32 +2096,28 @@ namespace Renci.SshNet.Sftp
/// <see cref="Task{Task}.Result"/> contains the file system information for the specified
/// path.
/// </returns>
public async Task<SftpFileSystemInformation> RequestStatVfsAsync(string path, CancellationToken cancellationToken)
public Task<SftpFileSystemInformation> RequestStatVfsAsync(string path, CancellationToken cancellationToken)
{
if (ProtocolVersion < 3)
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
}
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<SftpFileSystemInformation>(cancellationToken);
}
var tcs = new TaskCompletionSource<SftpFileSystemInformation>(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileSystemInformation>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileSystemInformation>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
#endif // NET || NETSTANDARD2_1_OR_GREATER
{
SendRequest(new StatVfsRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.GetReply<StatVfsReplyInfo>().Information),
response => tcs.TrySetException(GetSftpException(response))));
SendRequest(new StatVfsRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.GetReply<StatVfsReplyInfo>().Information),
response => tcs.TrySetException(GetSftpException(response))));
return await tcs.Task.ConfigureAwait(false);
}
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
/// <summary>
+5 -5
View File
@@ -46,21 +46,21 @@ namespace Renci.SshNet
/// The timeout to wait until an operation completes. The default value is negative
/// one (-1) milliseconds, which indicates an infinite timeout period.
/// </value>
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> represents a value that is less than -1 or greater than <see cref="int.MaxValue"/> milliseconds.</exception>
public TimeSpan OperationTimeout
{
get
{
CheckDisposed();
return TimeSpan.FromMilliseconds(_operationTimeout);
}
set
{
CheckDisposed();
_operationTimeout = value.AsTimeout(nameof(OperationTimeout));
if (_sftpSession is { } sftpSession)
{
sftpSession.OperationTimeout = _operationTimeout;
}
}
}
+56 -7
View File
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Channels;
@@ -29,13 +30,8 @@ namespace Renci.SshNet
private EventWaitHandle _channelClosedWaitHandle = new ManualResetEvent(initialState: false);
private bool _isDisposed;
/// <summary>
/// Gets or set the number of seconds to wait for an operation to complete.
/// </summary>
/// <value>
/// The number of seconds to wait for an operation to complete, or -1 to wait indefinitely.
/// </value>
public int OperationTimeout { get; private set; }
/// <inheritdoc/>
public int OperationTimeout { get; set; }
/// <summary>
/// Occurs when an error occurred.
@@ -250,6 +246,59 @@ namespace Renci.SshNet
}
}
protected async Task<T> WaitOnHandleAsync<T>(TaskCompletionSource<T> tcs, int millisecondsTimeout, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var errorOccuredReg = ThreadPool.RegisterWaitForSingleObject(
_errorOccuredWaitHandle,
(tcs, _) => ((TaskCompletionSource<T>)tcs).TrySetException(_exception),
state: tcs,
millisecondsTimeOutInterval: -1,
executeOnlyOnce: true);
var sessionDisconnectedReg = ThreadPool.RegisterWaitForSingleObject(
_sessionDisconnectedWaitHandle,
static (tcs, _) => ((TaskCompletionSource<T>)tcs).TrySetException(new SshException("Connection was closed by the server.")),
state: tcs,
millisecondsTimeOutInterval: -1,
executeOnlyOnce: true);
var channelClosedReg = ThreadPool.RegisterWaitForSingleObject(
_channelClosedWaitHandle,
static (tcs, _) => ((TaskCompletionSource<T>)tcs).TrySetException(new SshException("Channel was closed.")),
state: tcs,
millisecondsTimeOutInterval: -1,
executeOnlyOnce: true);
using var timeoutCts = new CancellationTokenSource(millisecondsTimeout);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
using var tokenReg = linkedCts.Token.Register(
static s =>
{
(var tcs, var cancellationToken) = ((TaskCompletionSource<T>, CancellationToken))s;
_ = tcs.TrySetCanceled(cancellationToken);
},
state: (tcs, cancellationToken),
useSynchronizationContext: false);
try
{
return await tcs.Task.ConfigureAwait(false);
}
catch (OperationCanceledException oce) when (timeoutCts.IsCancellationRequested)
{
throw new SshOperationTimeoutException("Operation has timed out.", oce);
}
finally
{
_ = errorOccuredReg.Unregister(waitObject: null);
_ = sessionDisconnectedReg.Unregister(waitObject: null);
_ = channelClosedReg.Unregister(waitObject: null);
}
}
/// <summary>
/// Blocks the current thread until the specified <see cref="WaitHandle"/> gets signaled, using a
/// 32-bit signed integer to specify the time interval in milliseconds.
@@ -115,37 +115,5 @@ namespace Renci.SshNet.Tests.Classes
Assert.AreEqual("OperationTimeout", ex.ParamName);
}
}
[TestMethod]
public void OperationTimeout_Disposed()
{
var connectionInfo = new PasswordConnectionInfo("host", 22, "admin", "pwd");
var target = new SftpClient(connectionInfo);
target.Dispose();
// getter
try
{
var actual = target.OperationTimeout;
Assert.Fail("Should have failed, but returned: " + actual);
}
catch (ObjectDisposedException ex)
{
Assert.IsNull(ex.InnerException);
Assert.AreEqual(typeof(SftpClient).FullName, ex.ObjectName);
}
// setter
try
{
target.OperationTimeout = TimeSpan.FromMilliseconds(5);
Assert.Fail();
}
catch (ObjectDisposedException ex)
{
Assert.IsNull(ex.InnerException);
Assert.AreEqual(typeof(SftpClient).FullName, ex.ObjectName);
}
}
}
}
@@ -0,0 +1,268 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
#if !NET8_0_OR_GREATER
using Renci.SshNet.Abstractions;
#endif
using Renci.SshNet.Channels;
using Renci.SshNet.Common;
using Renci.SshNet.Connection;
using Renci.SshNet.Messages;
using Renci.SshNet.Messages.Authentication;
using Renci.SshNet.Messages.Connection;
using Renci.SshNet.Sftp;
using Renci.SshNet.Sftp.Responses;
namespace Renci.SshNet.Tests.Classes
{
[TestClass]
public class SftpClientTest_AsyncExceptions
{
private MySession _session;
private SftpClient _client;
[TestInitialize]
public void Init()
{
var socketFactoryMock = new Mock<ISocketFactory>(MockBehavior.Strict);
var serviceFactoryMock = new Mock<IServiceFactory>(MockBehavior.Strict);
var connInfo = new PasswordConnectionInfo("host", "user", "pwd");
_session = new MySession(connInfo);
var concreteServiceFactory = new ServiceFactory();
serviceFactoryMock
.Setup(p => p.CreateSocketFactory())
.Returns(socketFactoryMock.Object);
serviceFactoryMock
.Setup(p => p.CreateSession(It.IsAny<ConnectionInfo>(), socketFactoryMock.Object))
.Returns(_session);
serviceFactoryMock
.Setup(p => p.CreateSftpResponseFactory())
.Returns(concreteServiceFactory.CreateSftpResponseFactory);
serviceFactoryMock
.Setup(p => p.CreateSftpSession(_session, It.IsAny<int>(), It.IsAny<Encoding>(), It.IsAny<ISftpResponseFactory>()))
.Returns(concreteServiceFactory.CreateSftpSession);
_client = new SftpClient(connInfo, false, serviceFactoryMock.Object);
_client.Connect();
}
[TestMethod]
public async Task Async_ObservesSessionDisconnected()
{
Task<SftpFileStream> openTask = _client.OpenAsync("path", FileMode.Create, FileAccess.Write, CancellationToken.None);
Assert.IsFalse(openTask.IsCompleted);
_session.InvokeDisconnected();
var ex = await Assert.ThrowsExceptionAsync<SshException>(() => openTask);
Assert.AreEqual("Connection was closed by the server.", ex.Message);
}
[TestMethod]
public async Task Async_ObservesChannelClosed()
{
Task<SftpFileStream> openTask = _client.OpenAsync("path", FileMode.Create, FileAccess.Write, CancellationToken.None);
Assert.IsFalse(openTask.IsCompleted);
_session.InvokeChannelCloseReceived();
var ex = await Assert.ThrowsExceptionAsync<SshException>(() => openTask);
Assert.AreEqual("Channel was closed.", ex.Message);
}
[TestMethod]
public async Task Async_ObservesCancellationToken()
{
using CancellationTokenSource cts = new();
Task<SftpFileStream> openTask = _client.OpenAsync("path", FileMode.Create, FileAccess.Write, cts.Token);
Assert.IsFalse(openTask.IsCompleted);
await cts.CancelAsync();
var ex = await Assert.ThrowsExceptionAsync<TaskCanceledException>(() => openTask);
Assert.AreEqual(cts.Token, ex.CancellationToken);
}
[TestMethod]
public async Task Async_ObservesOperationTimeout()
{
_client.OperationTimeout = TimeSpan.FromMilliseconds(250);
Task<SftpFileStream> openTask = _client.OpenAsync("path", FileMode.Create, FileAccess.Write, CancellationToken.None);
var ex = await Assert.ThrowsExceptionAsync<SshOperationTimeoutException>(() => openTask);
}
[TestMethod]
public async Task Async_ObservesErrorOccurred()
{
Task<SftpFileStream> openTask = _client.OpenAsync("path", FileMode.Create, FileAccess.Write, CancellationToken.None);
Assert.IsFalse(openTask.IsCompleted);
MyException ex = new("my exception");
_session.InvokeErrorOccurred(ex);
var ex2 = await Assert.ThrowsExceptionAsync<MyException>(() => openTask);
Assert.AreEqual(ex.Message, ex2.Message);
}
#pragma warning disable IDE0022 // Use block body for method
#pragma warning disable IDE0025 // Use block body for property
#pragma warning disable CS0067 // event is unused
private class MySession(ConnectionInfo connectionInfo) : ISession
{
public IConnectionInfo ConnectionInfo => connectionInfo;
public event EventHandler<MessageEventArgs<ChannelCloseMessage>> ChannelCloseReceived;
public event EventHandler<MessageEventArgs<ChannelDataMessage>> ChannelDataReceived;
public event EventHandler<MessageEventArgs<ChannelEofMessage>> ChannelEofReceived;
public event EventHandler<MessageEventArgs<ChannelExtendedDataMessage>> ChannelExtendedDataReceived;
public event EventHandler<MessageEventArgs<ChannelFailureMessage>> ChannelFailureReceived;
public event EventHandler<MessageEventArgs<ChannelOpenConfirmationMessage>> ChannelOpenConfirmationReceived;
public event EventHandler<MessageEventArgs<ChannelOpenFailureMessage>> ChannelOpenFailureReceived;
public event EventHandler<MessageEventArgs<ChannelOpenMessage>> ChannelOpenReceived;
public event EventHandler<MessageEventArgs<ChannelRequestMessage>> ChannelRequestReceived;
public event EventHandler<MessageEventArgs<ChannelSuccessMessage>> ChannelSuccessReceived;
public event EventHandler<MessageEventArgs<ChannelWindowAdjustMessage>> ChannelWindowAdjustReceived;
public event EventHandler<EventArgs> Disconnected;
public event EventHandler<ExceptionEventArgs> ErrorOccured;
public event EventHandler<SshIdentificationEventArgs> ServerIdentificationReceived;
public event EventHandler<HostKeyEventArgs> HostKeyReceived;
public event EventHandler<MessageEventArgs<RequestSuccessMessage>> RequestSuccessReceived;
public event EventHandler<MessageEventArgs<RequestFailureMessage>> RequestFailureReceived;
public event EventHandler<MessageEventArgs<BannerMessage>> UserAuthenticationBannerReceived;
public void InvokeDisconnected()
{
Disconnected?.Invoke(this, new EventArgs());
}
public void InvokeChannelCloseReceived()
{
ChannelCloseReceived?.Invoke(
this,
new MessageEventArgs<ChannelCloseMessage>(new ChannelCloseMessage(0)));
}
public void InvokeErrorOccurred(Exception ex)
{
ErrorOccured?.Invoke(this, new ExceptionEventArgs(ex));
}
public void SendMessage(Message message)
{
if (message is ChannelOpenMessage)
{
ChannelOpenConfirmationReceived?.Invoke(
this,
new MessageEventArgs<ChannelOpenConfirmationMessage>(
new ChannelOpenConfirmationMessage(0, int.MaxValue, int.MaxValue, 0)));
}
else if (message is ChannelRequestMessage)
{
ChannelSuccessReceived?.Invoke(
this,
new MessageEventArgs<ChannelSuccessMessage>(new ChannelSuccessMessage(0)));
}
else if (message is ChannelDataMessage dataMsg)
{
if (dataMsg.Data[sizeof(uint)] == (byte)SftpMessageTypes.Init)
{
ChannelDataReceived?.Invoke(
this,
new MessageEventArgs<ChannelDataMessage>(
new ChannelDataMessage(0, new SftpVersionResponse() { Version = 3 }.GetBytes())));
}
else if (dataMsg.Data[sizeof(uint)] == (byte)SftpMessageTypes.RealPath)
{
ChannelDataReceived?.Invoke(
this,
new MessageEventArgs<ChannelDataMessage>(
new ChannelDataMessage(0,
new SftpNameResponse(3, Encoding.UTF8)
{
ResponseId = 1,
Files = [new("thepath", new SftpFileAttributes(default, default, default, default, default, default, default))]
}.GetBytes())));
}
}
}
public bool IsConnected => false;
public SemaphoreSlim SessionSemaphore { get; } = new(1);
public IChannelSession CreateChannelSession() => new ChannelSession(this, 0, int.MaxValue, int.MaxValue);
public WaitHandle MessageListenerCompleted => throw new NotImplementedException();
public void Connect()
{
}
public Task ConnectAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
public IChannelDirectTcpip CreateChannelDirectTcpip() => throw new NotImplementedException();
public IChannelForwardedTcpip CreateChannelForwardedTcpip(uint remoteChannelNumber, uint remoteWindowSize, uint remoteChannelDataPacketSize)
=> throw new NotImplementedException();
public void Dispose()
{
}
public void OnDisconnecting()
{
}
public void Disconnect() => throw new NotImplementedException();
public void RegisterMessage(string messageName) => throw new NotImplementedException();
public bool TrySendMessage(Message message) => throw new NotImplementedException();
public WaitResult TryWait(WaitHandle waitHandle, TimeSpan timeout, out Exception exception) => throw new NotImplementedException();
public WaitResult TryWait(WaitHandle waitHandle, TimeSpan timeout) => throw new NotImplementedException();
public void UnRegisterMessage(string messageName) => throw new NotImplementedException();
public void WaitOnHandle(WaitHandle waitHandle)
{
}
public void WaitOnHandle(WaitHandle waitHandle, TimeSpan timeout) => throw new NotImplementedException();
}
[TestCleanup]
public void Cleanup() => _client?.Dispose();
#pragma warning disable
private class MyException : Exception
{
public MyException(string message) : base(message)
{
}
}
}
}