Forráskód Böngészése

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

* Fix sftp async methods not observing error conditions

* Update ISftpClient
Rob Hague 1 éve
szülő
commit
fbedaabb9c

+ 0 - 1
src/Renci.SshNet/ISftpClient.cs

@@ -56,7 +56,6 @@ namespace Renci.SshNet
         /// The timeout to wait until an operation completes. The default value is negative
         /// The timeout to wait until an operation completes. The default value is negative
         /// one (-1) milliseconds, which indicates an infinite timeout period.
         /// one (-1) milliseconds, which indicates an infinite timeout period.
         /// </value>
         /// </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>
         /// <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; }
         TimeSpan OperationTimeout { get; set; }
 
 

+ 3 - 3
src/Renci.SshNet/ISubsystemSession.cs

@@ -11,12 +11,12 @@ namespace Renci.SshNet
     internal interface ISubsystemSession : IDisposable
     internal interface ISubsystemSession : IDisposable
     {
     {
         /// <summary>
         /// <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>
         /// </summary>
         /// <value>
         /// <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>
         /// </value>
-        int OperationTimeout { get; }
+        int OperationTimeout { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets a value indicating whether this session is open.
         /// Gets a value indicating whether this session is open.

+ 238 - 295
src/Renci.SshNet/Sftp/SftpSession.cs

@@ -523,28 +523,24 @@ namespace Renci.SshNet.Sftp
         /// A task that represents the asynchronous <c>SSH_FXP_OPEN</c> request. The value of its
         /// 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.
         /// <see cref="Task{Task}.Result"/> contains the file handle of the specified path.
         /// </returns>
         /// </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);
             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>
         /// <summary>
@@ -651,8 +647,13 @@ namespace Renci.SshNet.Sftp
         /// <returns>
         /// <returns>
         /// A task that represents the asynchronous <c>SSH_FXP_CLOSE</c> request.
         /// A task that represents the asynchronous <c>SSH_FXP_CLOSE</c> request.
         /// </returns>
         /// </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);
             var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
 
 
             SendRequest(new SftpCloseRequest(ProtocolVersion,
             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>
         /// <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
         /// 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.
         /// array when the end of the file is reached.
         /// </returns>
         /// </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);
             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>
         /// <summary>
@@ -972,39 +959,35 @@ namespace Renci.SshNet.Sftp
         /// <returns>
         /// <returns>
         /// A task that represents the asynchronous <c>SSH_FXP_WRITE</c> request.
         /// A task that represents the asynchronous <c>SSH_FXP_WRITE</c> request.
         /// </returns>
         /// </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);
             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>
         /// <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
         /// 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.
         /// <see cref="Task{SftpFileAttributes}.Result"/> contains the file attributes of the specified path.
         /// </returns>
         /// </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);
             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>
         /// <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
         /// 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.
         /// <see cref="Task{Task}.Result"/> contains the file attributes of the specified handle.
         /// </returns>
         /// </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);
             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>
         /// <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
         /// 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.
         /// <see cref="Task{Task}.Result"/> contains the handle of the specified path.
         /// </returns>
         /// </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);
             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>
         /// <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"/>
         /// <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.
         /// of the file.
         /// </returns>
         /// </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);
             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>
         /// <summary>
@@ -1481,36 +1448,32 @@ namespace Renci.SshNet.Sftp
         /// <returns>
         /// <returns>
         /// A task that represents the asynchronous <c>SSH_FXP_REMOVE</c> request.
         /// A task that represents the asynchronous <c>SSH_FXP_REMOVE</c> request.
         /// </returns>
         /// </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);
             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>
         /// <summary>
@@ -1550,36 +1513,32 @@ namespace Renci.SshNet.Sftp
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
         /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</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>
         /// <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);
             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>
         /// <summary>
@@ -1614,37 +1573,33 @@ namespace Renci.SshNet.Sftp
         }
         }
 
 
         /// <inheritdoc />
         /// <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);
             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>
         /// <summary>
@@ -1691,37 +1646,33 @@ namespace Renci.SshNet.Sftp
             return result;
             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);
             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>
         /// <summary>
@@ -1921,37 +1872,33 @@ namespace Renci.SshNet.Sftp
         /// <returns>
         /// <returns>
         /// A task that represents the asynchronous <c>SSH_FXP_RENAME</c> request.
         /// A task that represents the asynchronous <c>SSH_FXP_RENAME</c> request.
         /// </returns>
         /// </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);
             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>
         /// <summary>
@@ -2149,32 +2096,28 @@ namespace Renci.SshNet.Sftp
         /// <see cref="Task{Task}.Result"/> contains the file system information for the specified
         /// <see cref="Task{Task}.Result"/> contains the file system information for the specified
         /// path.
         /// path.
         /// </returns>
         /// </returns>
-        public async Task<SftpFileSystemInformation> RequestStatVfsAsync(string path, CancellationToken cancellationToken)
+        public Task<SftpFileSystemInformation> RequestStatVfsAsync(string path, CancellationToken cancellationToken)
         {
         {
             if (ProtocolVersion < 3)
             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));
                 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);
             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>
         /// <summary>

+ 5 - 5
src/Renci.SshNet/SftpClient.cs

@@ -46,21 +46,21 @@ namespace Renci.SshNet
         /// The timeout to wait until an operation completes. The default value is negative
         /// The timeout to wait until an operation completes. The default value is negative
         /// one (-1) milliseconds, which indicates an infinite timeout period.
         /// one (-1) milliseconds, which indicates an infinite timeout period.
         /// </value>
         /// </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>
         /// <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
         public TimeSpan OperationTimeout
         {
         {
             get
             get
             {
             {
-                CheckDisposed();
-
                 return TimeSpan.FromMilliseconds(_operationTimeout);
                 return TimeSpan.FromMilliseconds(_operationTimeout);
             }
             }
             set
             set
             {
             {
-                CheckDisposed();
-
                 _operationTimeout = value.AsTimeout(nameof(OperationTimeout));
                 _operationTimeout = value.AsTimeout(nameof(OperationTimeout));
+
+                if (_sftpSession is { } sftpSession)
+                {
+                    sftpSession.OperationTimeout = _operationTimeout;
+                }
             }
             }
         }
         }
 
 

+ 56 - 7
src/Renci.SshNet/SubsystemSession.cs

@@ -2,6 +2,7 @@
 using System.Globalization;
 using System.Globalization;
 using System.Runtime.ExceptionServices;
 using System.Runtime.ExceptionServices;
 using System.Threading;
 using System.Threading;
+using System.Threading.Tasks;
 
 
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Channels;
 using Renci.SshNet.Channels;
@@ -29,13 +30,8 @@ namespace Renci.SshNet
         private EventWaitHandle _channelClosedWaitHandle = new ManualResetEvent(initialState: false);
         private EventWaitHandle _channelClosedWaitHandle = new ManualResetEvent(initialState: false);
         private bool _isDisposed;
         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>
         /// <summary>
         /// Occurs when an error occurred.
         /// 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>
         /// <summary>
         /// Blocks the current thread until the specified <see cref="WaitHandle"/> gets signaled, using a
         /// 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.
         /// 32-bit signed integer to specify the time interval in milliseconds.

+ 0 - 32
test/Renci.SshNet.Tests/Classes/SftpClientTest.cs

@@ -115,37 +115,5 @@ namespace Renci.SshNet.Tests.Classes
                 Assert.AreEqual("OperationTimeout", ex.ParamName);
                 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);
-            }
-        }
     }
     }
 }
 }

+ 268 - 0
test/Renci.SshNet.Tests/Classes/SftpClientTest_AsyncExceptions.cs

@@ -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)
+            {
+            }
+        }
+    }
+}