Bladeren bron

Added async methods for SSH_FXP_OPEN, SSH_FXP_REALPATH, SSH_FXP_STAT and SSH_FXP_LSTAT.
Improve SftpFileReader to no longer block when the session is broken, and improve performance by using async methods to open file and obtain file attributes.
Internally use an Int32 based timeout (expressed in milliseconds) to eliminate numerous conversions in hot paths.

Gert Driesen 8 jaren geleden
bovenliggende
commit
35233fc822
64 gewijzigde bestanden met toevoegingen van 1228 en 254 verwijderingen
  1. 14 1
      src/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj
  2. 7 3
      src/Renci.SshNet.Tests/Classes/NetConfClientTest_Dispose_Connected.cs
  3. 7 3
      src/Renci.SshNet.Tests/Classes/NetConfClientTest_Dispose_Disconnected.cs
  4. 7 3
      src/Renci.SshNet.Tests/Classes/NetConfClientTest_Dispose_Disposed.cs
  5. 5 2
      src/Renci.SshNet.Tests/Classes/NetConfClientTest_Finalize_Connected.cs
  6. 111 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead.cs
  7. 12 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_LastChunkBeforeEofIsComplete.cs
  8. 12 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_LastChunkBeforeEofIsPartial.cs
  9. 20 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsNotReached.cs
  10. 11 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsReached.cs
  11. 1 6
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadBeginReadException.cs
  12. 15 5
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadEndInvokeException_DiscardsFurtherReadAheads.cs
  13. 13 15
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadEndInvokeException_PreventsFurtherReadAheads.cs
  14. 123 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_ChunkAvailable.cs
  15. 107 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_NoChunkAvailable.cs
  16. 7 5
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReahAheadExceptionInBeginRead.cs
  17. 2 2
      src/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestRead.cs
  18. 2 2
      src/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestStatVfs.cs
  19. 3 3
      src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Connected.cs
  20. 3 3
      src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disconnected.cs
  21. 3 3
      src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disposed.cs
  22. 3 3
      src/Renci.SshNet.Tests/Classes/SftpClientTest_Finalize_Connected.cs
  23. 2 1
      src/Renci.SshNet.Tests/Classes/SubsystemSessionStub.cs
  24. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_Connect_Connected.cs
  25. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_Connect_Disconnected.cs
  26. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_Connect_Disposed.cs
  27. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_Disconnect_Connected.cs
  28. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_Disconnect_Disposed.cs
  29. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_Disconnect_NeverConnected.cs
  30. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_Dispose_Connected.cs
  31. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_Dispose_Disconnected.cs
  32. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_Dispose_Disposed.cs
  33. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_Dispose_NeverConnected.cs
  34. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_OnChannelDataReceived_Connected.cs
  35. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_OnChannelDataReceived_Disposed.cs
  36. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_OnChannelDataReceived_OnDataReceived_Exception.cs
  37. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_OnChannelException_Connected.cs
  38. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_OnChannelException_Disposed.cs
  39. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_OnSessionDisconnected_Connected.cs
  40. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_OnSessionDisconnected_Disposed.cs
  41. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_OnSessionErrorOccurred_Connected.cs
  42. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_OnSessionErrorOccurred_Disposed.cs
  43. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_SendData_Connected.cs
  44. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_SendData_Disposed.cs
  45. 2 2
      src/Renci.SshNet.Tests/Classes/SubsystemSession_SendData_NeverConnected.cs
  46. 4 1
      src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
  47. 89 6
      src/Renci.SshNet/Common/SemaphoreLight.cs
  48. 3 4
      src/Renci.SshNet/IServiceFactory.NET.cs
  49. 3 7
      src/Renci.SshNet/IServiceFactory.cs
  50. 11 3
      src/Renci.SshNet/ISubsystemSession.cs
  51. 15 3
      src/Renci.SshNet/NetConfClient.cs
  52. 2 2
      src/Renci.SshNet/Netconf/NetConfSession.cs
  53. 4 0
      src/Renci.SshNet/Renci.SshNet.csproj
  54. 3 4
      src/Renci.SshNet/ServiceFactory.NET.cs
  55. 32 4
      src/Renci.SshNet/ServiceFactory.cs
  56. 100 1
      src/Renci.SshNet/Sftp/ISftpSession.cs
  57. 12 0
      src/Renci.SshNet/Sftp/SFtpStatAsyncResult.cs
  58. 103 39
      src/Renci.SshNet/Sftp/SftpFileReader.cs
  59. 12 0
      src/Renci.SshNet/Sftp/SftpOpenAsyncResult.cs
  60. 12 0
      src/Renci.SshNet/Sftp/SftpOpenDirAsyncResult.cs
  61. 12 0
      src/Renci.SshNet/Sftp/SftpRealPathAsyncResult.cs
  62. 218 25
      src/Renci.SshNet/Sftp/SftpSession.cs
  63. 48 43
      src/Renci.SshNet/SftpClient.cs
  64. 11 8
      src/Renci.SshNet/SubsystemSession.cs

+ 14 - 1
src/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj

@@ -36,6 +36,7 @@
     <NoWarn>
     </NoWarn>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <LangVersion>5</LangVersion>
   </PropertyGroup>
   <PropertyGroup>
     <SignAssembly>true</SignAssembly>
@@ -883,12 +884,24 @@
     <Compile Include="..\Renci.SshNet\Sftp\SftpMessageTypes.cs">
       <Link>Sftp\SftpMessageTypes.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\Sftp\SftpOpenAsyncResult.cs">
+      <Link>Sftp\SftpOpenAsyncResult.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\Sftp\SftpOpenDirAsyncResult.cs">
+      <Link>Sftp\SftpOpenDirAsyncResult.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\Sftp\SftpReadAsyncResult.cs">
       <Link>Sftp\SftpReadAsyncResult.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\Sftp\SftpRealPathAsyncResult.cs">
+      <Link>Sftp\SftpRealPathAsyncResult.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\Sftp\SftpSession.cs">
       <Link>Sftp\SftpSession.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\Sftp\SFtpStatAsyncResult.cs">
+      <Link>Sftp\SFtpStatAsyncResult.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\Sftp\SftpSynchronizeDirectoriesAsyncResult.cs">
       <Link>Sftp\SftpSynchronizeDirectoriesAsyncResult.cs</Link>
     </Compile>
@@ -931,7 +944,7 @@
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <ProjectExtensions>
     <VisualStudio>
-      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" />
+      <UserProperties ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 7 - 3
src/Renci.SshNet.Tests/Classes/NetConfClientTest_Dispose_Connected.cs

@@ -1,6 +1,7 @@
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
 using Renci.SshNet.NetConf;
+using System;
 
 namespace Renci.SshNet.Tests.Classes
 {
@@ -9,9 +10,10 @@ namespace Renci.SshNet.Tests.Classes
     {
         private Mock<IServiceFactory> _serviceFactoryMock;
         private Mock<ISession> _sessionMock;
+        private Mock<INetConfSession> _netConfSessionMock;
         private NetConfClient _netConfClient;
         private ConnectionInfo _connectionInfo;
-        private Mock<INetConfSession> _netConfSessionMock;
+        private int _operationTimeout;
 
         [TestInitialize]
         public void Setup()
@@ -32,7 +34,9 @@ namespace Renci.SshNet.Tests.Classes
             _netConfSessionMock = new Mock<INetConfSession>(MockBehavior.Strict);
 
             _connectionInfo = new ConnectionInfo("host", "user", new NoneAuthenticationMethod("userauth"));
+            _operationTimeout = new Random().Next(1000, 10000);
             _netConfClient = new NetConfClient(_connectionInfo, false, _serviceFactoryMock.Object);
+            _netConfClient.OperationTimeout = TimeSpan.FromMilliseconds(_operationTimeout);
 
             var sequence = new MockSequence();
             _serviceFactoryMock.InSequence(sequence)
@@ -40,7 +44,7 @@ namespace Renci.SshNet.Tests.Classes
                 .Returns(_sessionMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.Connect());
             _serviceFactoryMock.InSequence(sequence)
-                .Setup(p => p.CreateNetConfSession(_sessionMock.Object, _netConfClient.OperationTimeout))
+                .Setup(p => p.CreateNetConfSession(_sessionMock.Object, _operationTimeout))
                 .Returns(_netConfSessionMock.Object);
             _netConfSessionMock.InSequence(sequence).Setup(p => p.Connect());
             _sessionMock.InSequence(sequence).Setup(p => p.OnDisconnecting());
@@ -59,7 +63,7 @@ namespace Renci.SshNet.Tests.Classes
         [TestMethod]
         public void CreateNetConfSessionOnServiceFactoryShouldBeInvokedOnce()
         {
-            _serviceFactoryMock.Verify(p => p.CreateNetConfSession(_sessionMock.Object, _netConfClient.OperationTimeout), Times.Once);
+            _serviceFactoryMock.Verify(p => p.CreateNetConfSession(_sessionMock.Object, _operationTimeout), Times.Once);
         }
 
         [TestMethod]

+ 7 - 3
src/Renci.SshNet.Tests/Classes/NetConfClientTest_Dispose_Disconnected.cs

@@ -1,6 +1,7 @@
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
 using Renci.SshNet.NetConf;
+using System;
 
 namespace Renci.SshNet.Tests.Classes
 {
@@ -9,9 +10,10 @@ namespace Renci.SshNet.Tests.Classes
     {
         private Mock<IServiceFactory> _serviceFactoryMock;
         private Mock<ISession> _sessionMock;
+        private Mock<INetConfSession> _netConfSessionMock;
         private NetConfClient _netConfClient;
         private ConnectionInfo _connectionInfo;
-        private Mock<INetConfSession> _netConfSessionMock;
+        private int _operationTimeout;
 
         [TestInitialize]
         public void Setup()
@@ -32,7 +34,9 @@ namespace Renci.SshNet.Tests.Classes
             _netConfSessionMock = new Mock<INetConfSession>(MockBehavior.Strict);
 
             _connectionInfo = new ConnectionInfo("host", "user", new NoneAuthenticationMethod("userauth"));
+            _operationTimeout = new Random().Next(1000, 10000);
             _netConfClient = new NetConfClient(_connectionInfo, false, _serviceFactoryMock.Object);
+            _netConfClient.OperationTimeout = TimeSpan.FromMilliseconds(_operationTimeout);
 
             var sequence = new MockSequence();
             _serviceFactoryMock.InSequence(sequence)
@@ -40,7 +44,7 @@ namespace Renci.SshNet.Tests.Classes
                 .Returns(_sessionMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.Connect());
             _serviceFactoryMock.InSequence(sequence)
-                .Setup(p => p.CreateNetConfSession(_sessionMock.Object, _netConfClient.OperationTimeout))
+                .Setup(p => p.CreateNetConfSession(_sessionMock.Object, _operationTimeout))
                 .Returns(_netConfSessionMock.Object);
             _netConfSessionMock.InSequence(sequence).Setup(p => p.Connect());
             _sessionMock.InSequence(sequence).Setup(p => p.OnDisconnecting());
@@ -61,7 +65,7 @@ namespace Renci.SshNet.Tests.Classes
         [TestMethod]
         public void CreateNetConfSessionOnServiceFactoryShouldBeInvokedOnce()
         {
-            _serviceFactoryMock.Verify(p => p.CreateNetConfSession(_sessionMock.Object, _netConfClient.OperationTimeout), Times.Once);
+            _serviceFactoryMock.Verify(p => p.CreateNetConfSession(_sessionMock.Object, _operationTimeout), Times.Once);
         }
 
         [TestMethod]

+ 7 - 3
src/Renci.SshNet.Tests/Classes/NetConfClientTest_Dispose_Disposed.cs

@@ -1,6 +1,7 @@
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
 using Renci.SshNet.NetConf;
+using System;
 
 namespace Renci.SshNet.Tests.Classes
 {
@@ -9,9 +10,10 @@ namespace Renci.SshNet.Tests.Classes
     {
         private Mock<IServiceFactory> _serviceFactoryMock;
         private Mock<ISession> _sessionMock;
+        private Mock<INetConfSession> _netConfSessionMock;
         private NetConfClient _netConfClient;
         private ConnectionInfo _connectionInfo;
-        private Mock<INetConfSession> _netConfSessionMock;
+        private int _operationTimeout;
 
         [TestInitialize]
         public void Setup()
@@ -32,7 +34,9 @@ namespace Renci.SshNet.Tests.Classes
             _netConfSessionMock = new Mock<INetConfSession>(MockBehavior.Strict);
 
             _connectionInfo = new ConnectionInfo("host", "user", new NoneAuthenticationMethod("userauth"));
+            _operationTimeout = new Random().Next(1000, 10000);
             _netConfClient = new NetConfClient(_connectionInfo, false, _serviceFactoryMock.Object);
+            _netConfClient.OperationTimeout = TimeSpan.FromMilliseconds(_operationTimeout);
 
             var sequence = new MockSequence();
             _serviceFactoryMock.InSequence(sequence)
@@ -40,7 +44,7 @@ namespace Renci.SshNet.Tests.Classes
                 .Returns(_sessionMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.Connect());
             _serviceFactoryMock.InSequence(sequence)
-                .Setup(p => p.CreateNetConfSession(_sessionMock.Object, _netConfClient.OperationTimeout))
+                .Setup(p => p.CreateNetConfSession(_sessionMock.Object, _operationTimeout))
                 .Returns(_netConfSessionMock.Object);
             _netConfSessionMock.InSequence(sequence).Setup(p => p.Connect());
             _sessionMock.InSequence(sequence).Setup(p => p.OnDisconnecting());
@@ -60,7 +64,7 @@ namespace Renci.SshNet.Tests.Classes
         [TestMethod]
         public void CreateNetConfSessionOnServiceFactoryShouldBeInvokedOnce()
         {
-            _serviceFactoryMock.Verify(p => p.CreateNetConfSession(_sessionMock.Object, _netConfClient.OperationTimeout), Times.Once);
+            _serviceFactoryMock.Verify(p => p.CreateNetConfSession(_sessionMock.Object, _operationTimeout), Times.Once);
         }
 
         [TestMethod]

+ 5 - 2
src/Renci.SshNet.Tests/Classes/NetConfClientTest_Finalize_Connected.cs

@@ -10,9 +10,10 @@ namespace Renci.SshNet.Tests.Classes
     {
         private Mock<IServiceFactory> _serviceFactoryMock;
         private Mock<ISession> _sessionMock;
+        private Mock<INetConfSession> _netConfSessionMock;
         private NetConfClient _netConfClient;
         private ConnectionInfo _connectionInfo;
-        private Mock<INetConfSession> _netConfSessionMock;
+        private int _operationTimeout;
 
         [TestInitialize]
         public void Setup()
@@ -28,7 +29,9 @@ namespace Renci.SshNet.Tests.Classes
             _netConfSessionMock = new Mock<INetConfSession>(MockBehavior.Loose);
 
             _connectionInfo = new ConnectionInfo("host", "user", new NoneAuthenticationMethod("userauth"));
+            _operationTimeout = new Random().Next(1000, 10000);
             _netConfClient = new NetConfClient(_connectionInfo, false, _serviceFactoryMock.Object);
+            _netConfClient.OperationTimeout = TimeSpan.FromMilliseconds(_operationTimeout);
 
             var sequence = new MockSequence();
             _serviceFactoryMock.InSequence(sequence)
@@ -36,7 +39,7 @@ namespace Renci.SshNet.Tests.Classes
                 .Returns(_sessionMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.Connect());
             _serviceFactoryMock.InSequence(sequence)
-                .Setup(p => p.CreateNetConfSession(_sessionMock.Object, _netConfClient.OperationTimeout))
+                .Setup(p => p.CreateNetConfSession(_sessionMock.Object, _operationTimeout))
                 .Returns(_netConfSessionMock.Object);
             _netConfSessionMock.InSequence(sequence).Setup(p => p.Connect());
 

+ 111 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead.cs

@@ -0,0 +1,111 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Sftp;
+using System;
+using System.Diagnostics;
+using System.Threading;
+using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    public class SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private MockSequence _seq;
+        private byte[] _handle;
+        private int _fileSize;
+        private int _operationTimeout;
+        private SftpFileReader _reader;
+        private ObjectDisposedException _actualException;
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 5);
+            _fileSize = 5000;
+            _operationTimeout = random.Next();
+        }
+
+        protected override void SetupMocks()
+        {
+            _seq = new MockSequence();
+
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
+                           .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
+            SftpSessionMock.InSequence(_seq).Setup(p => p.RequestClose(_handle));
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 1, _fileSize);
+        }
+
+        protected override void Act()
+        {
+            ThreadAbstraction.ExecuteThread(() =>
+            {
+                Thread.Sleep(500);
+                _reader.Dispose();
+            });
+
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void ReadShouldHaveThrownObjectDisposedException()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.AreEqual(typeof(SftpFileReader).FullName, _actualException.ObjectName);
+        }
+
+        [TestMethod]
+        public void ReadAfterDisposeShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual(typeof(SftpFileReader).FullName, ex.ObjectName);
+            }
+        }
+
+        [TestMethod]
+        public void DisposeShouldCloseHandleAndCompleteImmediately()
+        {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.RequestClose(_handle));
+
+            var stopwatch = Stopwatch.StartNew();
+            _reader.Dispose();
+            stopwatch.Stop();
+
+            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
+
+            SftpSessionMock.Verify(p => p.RequestClose(_handle), Times.Once);
+        }
+    }
+}

+ 12 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_LastChunkBeforeEofIsComplete.cs

@@ -4,6 +4,7 @@ using Renci.SshNet.Common;
 using Renci.SshNet.Sftp;
 using System;
 using System.Diagnostics;
+using System.Threading;
 using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
 
 namespace Renci.SshNet.Tests.Classes.Sftp
@@ -16,6 +17,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private MockSequence _seq;
         private byte[] _handle;
         private int _fileSize;
+        private int _operationTimeout;
         private byte[] _chunk1;
         private byte[] _chunk2;
         private byte[] _chunk3;
@@ -34,12 +36,16 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             _chunk2 = CreateByteArray(random, ChunkLength - 10);
             _chunk3 = new byte[0];
             _fileSize = _chunk1.Length + _chunk2.Length;
+            _operationTimeout = random.Next();
         }
 
         protected override void SetupMocks()
         {
             _seq = new MockSequence();
 
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -48,6 +54,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 asyncResult.SetAsCompleted(_chunk1, false);
                             })
                             .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -56,6 +65,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 asyncResult.SetAsCompleted(_chunk2, false);
                             })
                             .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>

+ 12 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_LastChunkBeforeEofIsPartial.cs

@@ -4,6 +4,7 @@ using Renci.SshNet.Common;
 using Renci.SshNet.Sftp;
 using System;
 using System.Diagnostics;
+using System.Threading;
 using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
 
 namespace Renci.SshNet.Tests.Classes.Sftp
@@ -16,6 +17,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private MockSequence _seq;
         private byte[] _handle;
         private int _fileSize;
+        private int _operationTimeout;
         private byte[] _chunk1;
         private byte[] _chunk2;
         private byte[] _chunk3;
@@ -33,12 +35,16 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             _chunk2 = CreateByteArray(random, ChunkLength);
             _chunk3 = new byte[0];
             _fileSize = _chunk1.Length + _chunk2.Length;
+            _operationTimeout = random.Next();
         }
 
         protected override void SetupMocks()
         {
             _seq = new MockSequence();
 
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -47,6 +53,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 asyncResult.SetAsCompleted(_chunk1, false);
                             })
                             .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -55,6 +64,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 asyncResult.SetAsCompleted(_chunk2, false);
                             })
                             .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>

+ 20 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsNotReached.cs

@@ -17,6 +17,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private MockSequence _seq;
         private byte[] _handle;
         private int _fileSize;
+        private int _operationTimeout;
         private byte[] _chunk1;
         private byte[] _chunk2;
         private byte[] _chunk2CatchUp1;
@@ -61,12 +62,16 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             _chunk5BeginRead = new ManualResetEvent(false);
             _chunk6BeginRead = new ManualResetEvent(false);
             _fileSize = _chunk1.Length + _chunk2.Length + _chunk2CatchUp1.Length + _chunk2CatchUp2.Length + _chunk3.Length + _chunk4.Length + _chunk5.Length;
+            _operationTimeout = random.Next();
         }
 
         protected override void SetupMocks()
         {
             _seq = new MockSequence();
 
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -76,6 +81,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 asyncResult.SetAsCompleted(_chunk1, false);
                             })
                             .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -85,6 +93,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 asyncResult.SetAsCompleted(_chunk2, false);
                             })
                             .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -94,6 +105,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 asyncResult.SetAsCompleted(_chunk3, false);
                             })
                             .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 3 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -103,6 +117,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 asyncResult.SetAsCompleted(_chunk4, false);
                             })
                             .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 4 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -118,6 +135,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.RequestRead(_handle, 2 * ChunkLength - 7, 7))
                             .Returns(_chunk2CatchUp2);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 5 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>

+ 11 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsReached.cs

@@ -17,6 +17,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private MockSequence _seq;
         private byte[] _handle;
         private int _fileSize;
+        private int _operationTimeout;
         private byte[] _chunk1;
         private byte[] _chunk2;
         private byte[] _chunk2CatchUp;
@@ -43,12 +44,16 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             _chunk2BeginRead = new ManualResetEvent(false);
             _chunk3BeginRead = new ManualResetEvent(false);
             _fileSize = _chunk1.Length + _chunk2.Length + _chunk2CatchUp.Length + _chunk3.Length;
+            _operationTimeout = random.Next();
         }
 
         protected override void SetupMocks()
         {
             _seq = new MockSequence();
 
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -58,6 +63,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 asyncResult.SetAsCompleted(_chunk1, false);
                             })
                             .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -67,6 +75,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 asyncResult.SetAsCompleted(_chunk2, false);
                             })
                             .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>

+ 1 - 6
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadBeginReadException.cs

@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
+namespace Renci.SshNet.Tests.Classes.Sftp
 {
     class SftpFileReaderTest_ReadAheadBeginReadException
     {

+ 15 - 5
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadEndInvokeException_DiscardsFurtherReadAheads.cs

@@ -18,6 +18,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private MockSequence _seq;
         private byte[] _handle;
         private int _fileSize;
+        private int _operationTimeout;
         private byte[] _chunk1;
         private byte[] _chunk3;
         private ManualResetEvent _readAheadChunk3Completed;
@@ -33,6 +34,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             _chunk1 = CreateByteArray(random, ChunkLength);
             _chunk3 = CreateByteArray(random, ChunkLength);
             _fileSize = 3 * ChunkLength;
+            _operationTimeout = random.Next();
 
             _readAheadChunk3Completed = new ManualResetEvent(false);
 
@@ -43,6 +45,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         {
             _seq = new MockSequence();
 
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                            .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -51,6 +56,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                asyncResult.SetAsCompleted(_chunk1, false);
                            })
                            .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -66,6 +74,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 });
                             })
                            .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -73,7 +84,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 var asyncResult = new SftpReadAsyncResult(callback, state);
                                 asyncResult.SetAsCompleted(_chunk3, false);
 
-                                // signal that we've com^meted the read-ahead for chunk3
+                                // signal that we've completed the read-ahead for chunk3
                                 _readAheadChunk3Completed.Set();
                             })
                             .Returns((SftpReadAsyncResult)null);
@@ -115,17 +126,16 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         }
 
         [TestMethod]
-        public void ReadAfterReadAheadExceptionShouldThrowObjectDisposedException()
+        public void ReadAfterReadAheadExceptionShouldRethrowExceptionThatOccurredInReadAhead()
         {
             try
             {
                 _reader.Read();
                 Assert.Fail();
             }
-            catch (ObjectDisposedException ex)
+            catch (SshException ex)
             {
-                Assert.IsNull(ex.InnerException);
-                Assert.AreEqual(typeof(SftpFileReader).FullName, ex.ObjectName);
+                Assert.AreSame(_exception, ex);
             }
         }
 

+ 13 - 15
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadEndInvokeException_PreventsFurtherReadAheads.cs

@@ -18,6 +18,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private MockSequence _seq;
         private byte[] _handle;
         private int _fileSize;
+        private int _operationTimeout;
         private byte[] _chunk1;
         private byte[] _chunk3;
         private SftpFileReader _reader;
@@ -34,6 +35,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             _chunk1 = CreateByteArray(random, ChunkLength);
             _chunk3 = CreateByteArray(random, ChunkLength);
             _fileSize = 3 * _chunk1.Length;
+            _operationTimeout = random.Next();
 
             _readAheadChunk2 = new ManualResetEvent(false);
             _readChunk2 = new ManualResetEvent(false);
@@ -45,6 +47,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         {
             _seq = new MockSequence();
 
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                            .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -53,6 +58,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                asyncResult.SetAsCompleted(_chunk1, false);
                            })
                            .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                             .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
@@ -71,18 +79,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 });
                             })
                            .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
             SftpSessionMock.InSequence(_seq)
-                            .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                            {
-                                // this chunk should never be read
-                                Thread.Sleep(20000);
-
-                                var asyncResult = new SftpReadAsyncResult(callback, state);
-                                asyncResult.SetAsCompleted(_chunk3, false);
-                            })
-                            .Returns((SftpReadAsyncResult)null);
-
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
         }
 
         protected override void Arrange()
@@ -122,17 +121,16 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         }
 
         [TestMethod]
-        public void ReadAfterReadAheadExceptionShouldThrowObjectDisposedException()
+        public void ReadAfterReadAheadExceptionShouldRethrowExceptionThatOccurredInReadAhead()
         {
             try
             {
                 _reader.Read();
                 Assert.Fail();
             }
-            catch (ObjectDisposedException ex)
+            catch (SshException ex)
             {
-                Assert.IsNull(ex.InnerException);
-                Assert.AreEqual(typeof(SftpFileReader).FullName, ex.ObjectName);
+                Assert.AreSame(_exception, ex);
             }
         }
 

+ 123 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_ChunkAvailable.cs

@@ -0,0 +1,123 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+using Renci.SshNet.Sftp;
+using System;
+using System.Diagnostics;
+using System.Threading;
+using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    public class SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_ChunkAvailable : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private MockSequence _seq;
+        private byte[] _handle;
+        private int _fileSize;
+        private int _operationTimeout;
+        private byte[] _chunk1;
+        private byte[] _chunk2;
+        private SftpFileReader _reader;
+        private SshException _exception;
+        private ManualResetEvent _exceptionSignaled;
+        private SshException _actualException;
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 5);
+            _chunk1 = CreateByteArray(random, ChunkLength);
+            _chunk2 = CreateByteArray(random, ChunkLength);
+            _fileSize = _chunk1.Length + _chunk2.Length + 1;
+            _operationTimeout = random.Next();
+
+            _exception = new SshException();
+            _exceptionSignaled = new ManualResetEvent(false);
+        }
+
+        protected override void SetupMocks()
+        {
+            _seq = new MockSequence();
+
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
+                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
+                            {
+                                var asyncResult = new SftpReadAsyncResult(callback, state);
+                                asyncResult.SetAsCompleted(_chunk1, false);
+                            })
+                           .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout))
+                           .Callback(() => _exceptionSignaled.Set())
+                           .Throws(_exception);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 2, _fileSize);
+        }
+
+        protected override void Act()
+        {
+            // wait for the exception to be signaled by the second call to WaitOnHandle
+            _exceptionSignaled.WaitOne(5000);
+            // allow a little time to allow SftpFileReader to process exception
+            Thread.Sleep(100);
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (SshException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void ReadShouldHaveRethrownExceptionThrownByWaitOnHandle()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.AreSame(_exception, _actualException);
+        }
+
+        [TestMethod]
+        public void ReadShouldRethrowExceptionThrownByWaitOnHandle()
+        {
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (SshException ex)
+            {
+                Assert.AreSame(_exception, ex);
+            }
+        }
+
+        [TestMethod]
+        public void DisposeShouldCloseHandleAndCompleteImmediately()
+        {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.RequestClose(_handle));
+
+            var stopwatch = Stopwatch.StartNew();
+            _reader.Dispose();
+            stopwatch.Stop();
+
+            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
+
+            SftpSessionMock.Verify(p => p.RequestClose(_handle), Times.Once);
+        }
+    }
+}

+ 107 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_NoChunkAvailable.cs

@@ -0,0 +1,107 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+using Renci.SshNet.Sftp;
+using System;
+using System.Diagnostics;
+using System.Threading;
+using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    public class SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_NoChunkAvailable : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private MockSequence _seq;
+        private byte[] _handle;
+        private int _fileSize;
+        private int _operationTimeout;
+        private SftpFileReader _reader;
+        private SshException _exception;
+        private SshException _actualException;
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 5);
+            _fileSize = random.Next();
+            _operationTimeout = random.Next();
+
+            _exception = new SshException();
+        }
+
+        protected override void SetupMocks()
+        {
+            _seq = new MockSequence();
+
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout));
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
+                           .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitOnHandle(It.IsAny<WaitHandle>(), _operationTimeout))
+                           .Throws(_exception);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 1, _fileSize);
+        }
+
+        protected override void Act()
+        {
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (SshException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void ReadShouldHaveRethrownExceptionThrownByWaitOnHandle()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.AreSame(_exception, _actualException);
+        }
+
+        [TestMethod]
+        public void ReadShouldRethrowExceptionThrownByWaitOnHandle()
+        {
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (SshException ex)
+            {
+                Assert.AreSame(_exception, ex);
+            }
+        }
+
+        [TestMethod]
+        public void DisposeShouldCloseHandleAndCompleteImmediately()
+        {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.RequestClose(_handle));
+
+            var stopwatch = Stopwatch.StartNew();
+            _reader.Dispose();
+            stopwatch.Stop();
+
+            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
+
+            SftpSessionMock.Verify(p => p.RequestClose(_handle), Times.Once);
+        }
+    }
+}

+ 7 - 5
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ExceptionInReadAhead.cs → src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReahAheadExceptionInBeginRead.cs

@@ -10,17 +10,18 @@ using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
 namespace Renci.SshNet.Tests.Classes.Sftp
 {
     [TestClass]
-    public class SftpFileReaderTest_Read_ExceptionInReadAhead : SftpFileReaderTestBase
+    public class SftpFileReaderTest_Read_ReahAheadExceptionInBeginRead : SftpFileReaderTestBase
     {
         private const int ChunkLength = 32 * 1024;
 
         private MockSequence _seq;
         private byte[] _handle;
         private int _fileSize;
+        private int _operationTimeout;
         private byte[] _chunk1;
         private byte[] _chunk2;
         private SftpFileReader _reader;
-        private ManualResetEvent _readAhead;
+        private ManualResetEvent _readAheadChunk3;
         private ManualResetEvent _readChunk3;
         private SshException _exception;
         private SshException _actualException;
@@ -33,8 +34,9 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             _chunk1 = CreateByteArray(random, ChunkLength);
             _chunk2 = CreateByteArray(random, ChunkLength);
             _fileSize = _chunk1.Length + _chunk2.Length + 1;
+            _operationTimeout = random.Next();
 
-            _readAhead = new ManualResetEvent(false);
+            _readAheadChunk3 = new ManualResetEvent(false);
             _readChunk3 = new ManualResetEvent(false);
 
             _exception = new SshException();
@@ -64,7 +66,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                             .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
                             .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
                             {
-                                _readAhead.Set();
+                                _readAheadChunk3.Set();
                                 _readChunk3.WaitOne(TimeSpan.FromSeconds(5));
                                 // sleep a short time to make sure the client is in the blocking wait
                                 Thread.Sleep(500);
@@ -85,7 +87,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             _reader.Read();
 
             // wait until we've the SftpFileReader has starting reading ahead chunk 3
-            Assert.IsTrue(_readAhead.WaitOne(TimeSpan.FromSeconds(5)));
+            Assert.IsTrue(_readAheadChunk3.WaitOne(TimeSpan.FromSeconds(5)));
             // signal that we are about to read chunk 3
             _readChunk3.Set();
 

+ 2 - 2
src/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestRead.cs

@@ -17,7 +17,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private Mock<IServiceFactory> _serviceFactoryMock;
         private Mock<IChannelSession> _channelSessionMock;
         private SftpSession _sftpSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private byte[] _actual;
         private byte[] _expected;
         private Encoding _encoding;
@@ -33,7 +33,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         {
             var random = new Random();
 
-            _operationTimeout = TimeSpan.FromMilliseconds(random.Next(100, 500));
+            _operationTimeout = random.Next(100, 500);
             _expected = new byte[random.Next(30, 50)];
             _encoding = Encoding.UTF8;
             random.NextBytes(_expected);

+ 2 - 2
src/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestStatVfs.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private Mock<IChannelSession> _channelSessionMock;
         private Mock<IServiceFactory> _serviceFactoryMock;
         private SftpSession _sftpSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private SftpFileSytemInformation _actual;
         private Encoding _encoding;
 
@@ -33,7 +33,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         {
             var random = new Random();
 
-            _operationTimeout = TimeSpan.FromMilliseconds(random.Next(100, 500));
+            _operationTimeout = random.Next(100, 500);
             _encoding = Encoding.UTF8;
 
             _bAvail = (ulong) random.Next(0, int.MaxValue);

+ 3 - 3
src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Connected.cs

@@ -13,7 +13,7 @@ namespace Renci.SshNet.Tests.Classes
         private SftpClient _sftpClient;
         private ConnectionInfo _connectionInfo;
         private Mock<ISftpSession> _sftpSessionMock;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
 
         [TestInitialize]
         public void Setup()
@@ -34,9 +34,9 @@ namespace Renci.SshNet.Tests.Classes
             _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
 
             _connectionInfo = new ConnectionInfo("host", "user", new NoneAuthenticationMethod("userauth"));
-            _operationTimeout = TimeSpan.FromSeconds(new Random().Next(1, 10));
+            _operationTimeout = new Random().Next(1000, 10000);
             _sftpClient = new SftpClient(_connectionInfo, false, _serviceFactoryMock.Object);
-            _sftpClient.OperationTimeout = _operationTimeout;
+            _sftpClient.OperationTimeout = TimeSpan.FromMilliseconds(_operationTimeout);
 
             var sequence = new MockSequence();
             _serviceFactoryMock.InSequence(sequence)

+ 3 - 3
src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disconnected.cs

@@ -13,7 +13,7 @@ namespace Renci.SshNet.Tests.Classes
         private SftpClient _sftpClient;
         private ConnectionInfo _connectionInfo;
         private Mock<ISftpSession> _sftpSessionMock;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
 
         [TestInitialize]
         public void Setup()
@@ -34,9 +34,9 @@ namespace Renci.SshNet.Tests.Classes
             _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
 
             _connectionInfo = new ConnectionInfo("host", "user", new NoneAuthenticationMethod("userauth"));
-            _operationTimeout = TimeSpan.FromSeconds(new Random().Next(1, 10));
+            _operationTimeout = new Random().Next(1000, 10000);
             _sftpClient = new SftpClient(_connectionInfo, false, _serviceFactoryMock.Object);
-            _sftpClient.OperationTimeout = _operationTimeout;
+            _sftpClient.OperationTimeout = TimeSpan.FromMilliseconds(_operationTimeout);
 
             var sequence = new MockSequence();
             _serviceFactoryMock.InSequence(sequence)

+ 3 - 3
src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disposed.cs

@@ -13,7 +13,7 @@ namespace Renci.SshNet.Tests.Classes
         private SftpClient _sftpClient;
         private ConnectionInfo _connectionInfo;
         private Mock<ISftpSession> _sftpSessionMock;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
 
         [TestInitialize]
         public void Setup()
@@ -34,9 +34,9 @@ namespace Renci.SshNet.Tests.Classes
             _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
 
             _connectionInfo = new ConnectionInfo("host", "user", new NoneAuthenticationMethod("userauth"));
-            _operationTimeout = TimeSpan.FromSeconds(new Random().Next(1, 10));
+            _operationTimeout = new Random().Next(1000, 10000);
             _sftpClient = new SftpClient(_connectionInfo, false, _serviceFactoryMock.Object);
-            _sftpClient.OperationTimeout = _operationTimeout;
+            _sftpClient.OperationTimeout = TimeSpan.FromMilliseconds(_operationTimeout);
 
             var sequence = new MockSequence();
             _serviceFactoryMock.InSequence(sequence)

+ 3 - 3
src/Renci.SshNet.Tests/Classes/SftpClientTest_Finalize_Connected.cs

@@ -14,7 +14,7 @@ namespace Renci.SshNet.Tests.Classes
         private SftpClient _sftpClient;
         private ConnectionInfo _connectionInfo;
         private Mock<ISftpSession> _sftpSessionMock;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
 
         [TestInitialize]
         public void Setup()
@@ -30,9 +30,9 @@ namespace Renci.SshNet.Tests.Classes
             _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
 
             _connectionInfo = new ConnectionInfo("host", "user", new NoneAuthenticationMethod("userauth"));
-            _operationTimeout = TimeSpan.FromSeconds(new Random().Next(1, 10));
+            _operationTimeout = new Random().Next(1000, 10000);
             _sftpClient = new SftpClient(_connectionInfo, false, _serviceFactoryMock.Object);
-            _sftpClient.OperationTimeout = _operationTimeout;
+            _sftpClient.OperationTimeout = TimeSpan.FromMilliseconds(_operationTimeout);
 
             _serviceFactoryMock.Setup(p => p.CreateSession(_connectionInfo))
                 .Returns(_sessionMock.Object);

+ 2 - 1
src/Renci.SshNet.Tests/Classes/SubsystemSessionStub.cs

@@ -10,7 +10,8 @@ namespace Renci.SshNet.Tests.Classes
     {
         private int _onChannelOpenInvocationCount;
 
-        public SubsystemSessionStub(ISession session, string subsystemName, TimeSpan operationTimeout, Encoding encoding) : base(session, subsystemName, operationTimeout, encoding)
+        public SubsystemSessionStub(ISession session, string subsystemName, int operationTimeout, Encoding encoding)
+            : base(session, subsystemName, operationTimeout, encoding)
         {
             OnDataReceivedInvocations = new List<ChannelDataEventArgs>();
         }

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_Connect_Connected.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -34,7 +34,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_Connect_Disconnected.cs

@@ -17,7 +17,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelAfterDisconnectMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -34,7 +34,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_Connect_Disposed.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -33,7 +33,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_Disconnect_Connected.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -33,7 +33,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_Disconnect_Disposed.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -32,7 +32,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_Disconnect_NeverConnected.cs

@@ -14,7 +14,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<ISession> _sessionMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -30,7 +30,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_Dispose_Connected.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -32,7 +32,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_Dispose_Disconnected.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -32,7 +32,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_Dispose_Disposed.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -32,7 +32,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_Dispose_NeverConnected.cs

@@ -15,7 +15,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<ISession> _sessionMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -31,7 +31,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_OnChannelDataReceived_Connected.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -34,7 +34,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_OnChannelDataReceived_Disposed.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -33,7 +33,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_OnChannelDataReceived_OnDataReceived_Exception.cs

@@ -17,7 +17,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -35,7 +35,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_OnChannelException_Connected.cs

@@ -17,7 +17,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -35,7 +35,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_OnChannelException_Disposed.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -33,7 +33,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_OnSessionDisconnected_Connected.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -33,7 +33,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_OnSessionDisconnected_Disposed.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -32,7 +32,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_OnSessionErrorOccurred_Connected.cs

@@ -17,7 +17,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -35,7 +35,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_OnSessionErrorOccurred_Disposed.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -33,7 +33,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_SendData_Connected.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -34,7 +34,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_SendData_Disposed.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -33,7 +33,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 2 - 2
src/Renci.SshNet.Tests/Classes/SubsystemSession_SendData_NeverConnected.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet.Tests.Classes
         private Mock<IChannelSession> _channelMock;
         private string _subsystemName;
         private SubsystemSessionStub _subsystemSession;
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
         private Encoding _encoding;
         private IList<EventArgs> _disconnectedRegister;
         private IList<ExceptionEventArgs> _errorOccurredRegister;
@@ -35,7 +35,7 @@ namespace Renci.SshNet.Tests.Classes
         {
             var random = new Random();
             _subsystemName = random.Next().ToString(CultureInfo.InvariantCulture);
-            _operationTimeout = TimeSpan.FromSeconds(30);
+            _operationTimeout = 30000;
             _encoding = Encoding.UTF8;
             _disconnectedRegister = new List<EventArgs>();
             _errorOccurredRegister = new List<ExceptionEventArgs>();

+ 4 - 1
src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj

@@ -341,6 +341,7 @@
     <Compile Include="Classes\SftpClientTest_Dispose_Disposed.cs" />
     <Compile Include="Classes\SftpClientTest_Finalize_Connected.cs" />
     <Compile Include="Classes\Sftp\SftpFileReaderTestBase.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead.cs" />
     <Compile Include="Classes\Sftp\SftpFileReaderTest_LastChunkBeforeEofIsComplete.cs" />
     <Compile Include="Classes\Sftp\SftpFileReaderTest_LastChunkBeforeEofIsPartial.cs" />
     <Compile Include="Classes\Sftp\SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsNotReached.cs" />
@@ -350,7 +351,9 @@
     <Compile Include="Classes\Sftp\SftpFileReaderTest_ReadAheadBeginReadException.cs" />
     <Compile Include="Classes\Sftp\SftpFileReaderTest_ReadBackBeginReadException.cs" />
     <Compile Include="Classes\Sftp\SftpFileReaderTest_ReadBackEndInvokeException.cs" />
-    <Compile Include="Classes\Sftp\SftpFileReaderTest_Read_ExceptionInReadAhead.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_Read_ReahAheadExceptionInBeginRead.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_ChunkAvailable.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_NoChunkAvailable.cs" />
     <Compile Include="Classes\Sftp\SftpFileStreamTest_CanRead_Closed_FileAccessRead.cs" />
     <Compile Include="Classes\Sftp\SftpFileStreamTest_CanRead_Closed_FileAccessReadWrite.cs" />
     <Compile Include="Classes\Sftp\SftpFileStreamTest_CanRead_Closed_FileAccessWrite.cs" />

+ 89 - 6
src/Renci.SshNet/Common/SemaphoreLight.cs

@@ -6,9 +6,10 @@ namespace Renci.SshNet.Common
     /// <summary>
     /// Light implementation of SemaphoreSlim.
     /// </summary>
-    public class SemaphoreLight
+    public class SemaphoreLight : IDisposable
     {
         private readonly object _lock = new object();
+        private ManualResetEvent _waitHandle;
 
         private int _currentCount;
 
@@ -31,6 +32,34 @@ namespace Renci.SshNet.Common
         /// </summary>
         public int CurrentCount { get { return _currentCount; } }
 
+        /// <summary>
+        /// Returns a <see cref="WaitHandle"/> that can be used to wait on the semaphore.
+        /// </summary>
+        /// <value>
+        /// A <see cref="WaitHandle"/> that can be used to wait on the semaphore.
+        /// </value>
+        /// <remarks>
+        /// A successful wait on the <see cref="AvailableWaitHandle"/> does not imply a successful
+        /// wait on the <see cref="SemaphoreLight"/> itself. It should be followed by a true wait
+        /// on the semaphore.
+        /// </remarks>
+        public WaitHandle AvailableWaitHandle
+        {
+            get
+            {
+                if (_waitHandle != null)
+                    return _waitHandle;
+
+                lock (_lock)
+                {
+                    if (_waitHandle == null)
+                        _waitHandle = new ManualResetEvent(_currentCount > 0);
+                }
+
+                return _waitHandle;
+            }
+        }
+
         /// <summary>
         /// Exits the <see cref="SemaphoreLight"/> once.
         /// </summary>
@@ -44,19 +73,27 @@ namespace Renci.SshNet.Common
         /// Exits the <see cref="SemaphoreLight"/> a specified number of times.
         /// </summary>
         /// <param name="releaseCount">The number of times to exit the semaphore.</param>
-        /// <returns>The previous count of the <see cref="SemaphoreLight"/>.</returns>
+        /// <returns>
+        /// The previous count of the <see cref="SemaphoreLight"/>.
+        /// </returns>
         public int Release(int releaseCount)
         {
-            var oldCount = _currentCount;
-
             lock (_lock)
             {
+                var oldCount = _currentCount;
+
                 _currentCount += releaseCount;
 
+                // signal waithandle when the original semaphore count was zero
+                if (_waitHandle != null && oldCount == 0)
+                {
+                    _waitHandle.Set();
+                }
+
                 Monitor.Pulse(_lock);
-            }
 
-            return oldCount;
+                return oldCount;
+            }
         }
 
         /// <summary>
@@ -73,6 +110,12 @@ namespace Renci.SshNet.Common
 
                 _currentCount--;
 
+                // unsignal waithandle when the semaphore count is zero
+                if (_waitHandle != null && _currentCount == 0)
+                {
+                    _waitHandle.Reset();
+                }
+
                 Monitor.Pulse(_lock);
             }
         }
@@ -143,10 +186,50 @@ namespace Renci.SshNet.Common
 
                 _currentCount--;
 
+                // unsignal waithandle when the semaphore count is zero
+                if (_waitHandle != null && _currentCount == 0)
+                {
+                    _waitHandle.Reset();
+                }
+
                 Monitor.Pulse(_lock);
 
                 return true;
             }
         }
+
+        /// <summary>
+        /// Finalizes the current <see cref="SemaphoreLight"/>.
+        /// </summary>
+        ~SemaphoreLight()
+        {
+            Dispose(false);
+        }
+
+        /// <summary>
+        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Releases unmanaged and - optionally - managed resources
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                var waitHandle = _waitHandle;
+                if (waitHandle != null)
+                {
+                    waitHandle.Dispose();
+                    _waitHandle = null;
+                }
+            }
+        }
     }
 }

+ 3 - 4
src/Renci.SshNet/IServiceFactory.NET.cs

@@ -1,5 +1,4 @@
-using System;
-using Renci.SshNet.NetConf;
+using Renci.SshNet.NetConf;
 
 namespace Renci.SshNet
 {
@@ -10,10 +9,10 @@ namespace Renci.SshNet
         /// and with the specified operation timeout.
         /// </summary>
         /// <param name="session">The <see cref="ISession"/> to create the <see cref="INetConfSession"/> in.</param>
-        /// <param name="operationTimeout">The operation timeout.</param>
+        /// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.</param>
         /// <returns>
         /// An <see cref="INetConfSession"/>.
         /// </returns>
-        INetConfSession CreateNetConfSession(ISession session, TimeSpan operationTimeout);
+        INetConfSession CreateNetConfSession(ISession session, int operationTimeout);
     }
 }

+ 3 - 7
src/Renci.SshNet/IServiceFactory.cs

@@ -29,12 +29,12 @@ namespace Renci.SshNet
         /// the specified operation timeout and encoding.
         /// </summary>
         /// <param name="session">The <see cref="ISession"/> to create the <see cref="ISftpSession"/> in.</param>
-        /// <param name="operationTimeout">The operation timeout.</param>
+        /// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.</param>
         /// <param name="encoding">The encoding.</param>
         /// <returns>
         /// An <see cref="ISftpSession"/>.
         /// </returns>
-        ISftpSession CreateSftpSession(ISession session, TimeSpan operationTimeout, Encoding encoding);
+        ISftpSession CreateSftpSession(ISession session, int operationTimeout, Encoding encoding);
 
         /// <summary>
         /// Create a new <see cref="PipeStream"/>.
@@ -58,10 +58,6 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">No key exchange algorithm is supported by both client and server.</exception>
         IKeyExchange CreateKeyExchange(IDictionary<string, Type> clientAlgorithms, string[] serverAlgorithms);
 
-        ISftpFileReader CreateSftpFileReader(byte[] handle,
-                                             ISftpSession sftpSession,
-                                             uint chunkSize,
-                                             int maxPendingReads,
-                                             long? fileSize);
+        ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize);
     }
 }

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

@@ -9,6 +9,14 @@ namespace Renci.SshNet
     /// </summary>
     internal interface ISubsystemSession : IDisposable
     {
+        /// <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>
+        int OperationTimeout { get; }
+
         /// <summary>
         /// Gets a value indicating whether this session is open.
         /// </summary>
@@ -33,10 +41,10 @@ namespace Renci.SshNet
         /// Waits a specified time for a given <see cref="WaitHandle"/> to get signaled.
         /// </summary>
         /// <param name="waitHandle">The handle to wait for.</param>
-        /// <param name="operationTimeout">The time to wait for <paramref name="waitHandle"/> to get signaled.</param>
+        /// <param name="millisecondsTimeout">The number of millieseconds wait for <paramref name="waitHandle"/> to get signaled, or -1 to wait indefinitely.</param>
         /// <exception cref="SshException">The connection was closed by the server.</exception>
         /// <exception cref="SshException">The channel was closed.</exception>
-        /// <exception cref="SshOperationTimeoutException">The handle did not get signaled within the specified <paramref name="operationTimeout"/>.</exception>
-        void WaitOnHandle(WaitHandle waitHandle, TimeSpan operationTimeout);
+        /// <exception cref="SshOperationTimeoutException">The handle did not get signaled within the specified timeout.</exception>
+        void WaitOnHandle(WaitHandle waitHandle, int millisecondsTimeout);
     }
 }

+ 15 - 3
src/Renci.SshNet/NetConfClient.cs

@@ -13,6 +13,8 @@ namespace Renci.SshNet
     /// </summary>
     public class NetConfClient : BaseClient
     {
+        private int _operationTimeout;
+
         /// <summary>
         /// Holds <see cref="INetConfSession"/> instance that used to communicate to the server
         /// </summary>
@@ -25,7 +27,17 @@ namespace Renci.SshNet
         /// The timeout to wait until an operation completes. The default value is negative
         /// one (-1) milliseconds, which indicates an infinite time-out period.
         /// </value>
-        public TimeSpan OperationTimeout { get; set; }
+        public TimeSpan OperationTimeout {
+            get { return TimeSpan.FromMilliseconds(_operationTimeout); }
+            set
+            {
+                var timeoutInMilliseconds = value.TotalMilliseconds;
+                if (timeoutInMilliseconds < -1d || timeoutInMilliseconds > int.MaxValue)
+                    throw new ArgumentOutOfRangeException("timeout", "The timeout must represent a value between -1 and Int32.MaxValue, inclusive.");
+
+                _operationTimeout = (int) timeoutInMilliseconds;
+            }
+        }
 
         #region Constructors
 
@@ -127,7 +139,7 @@ namespace Renci.SshNet
         internal NetConfClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory)
             : base(connectionInfo, ownsConnectionInfo, serviceFactory)
         {
-            OperationTimeout = SshNet.Session.InfiniteTimeSpan;
+            _operationTimeout = SshNet.Session.Infinite;
             AutomaticMessageIdHandling = true;
         }
 
@@ -207,7 +219,7 @@ namespace Renci.SshNet
         {
             base.OnConnected();
 
-            _netConfSession = ServiceFactory.CreateNetConfSession(Session, OperationTimeout);
+            _netConfSession = ServiceFactory.CreateNetConfSession(Session, _operationTimeout);
             _netConfSession.Connect();
         }
 

+ 2 - 2
src/Renci.SshNet/Netconf/NetConfSession.cs

@@ -33,8 +33,8 @@ namespace Renci.SshNet.NetConf
         /// Initializes a new instance of the <see cref="NetConfSession"/> class.
         /// </summary>
         /// <param name="session">The session.</param>
-        /// <param name="operationTimeout">The operation timeout.</param>
-        public NetConfSession(ISession session, TimeSpan operationTimeout)
+        /// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.</param>
+        public NetConfSession(ISession session, int operationTimeout)
             : base(session, "netconf", operationTimeout, Encoding.UTF8)
         {
             ClientCapabilities = new XmlDocument();

+ 4 - 0
src/Renci.SshNet/Renci.SshNet.csproj

@@ -428,10 +428,14 @@
     <Compile Include="Sftp\SftpMessageTypes.cs">
       <SubType>Code</SubType>
     </Compile>
+    <Compile Include="Sftp\SftpOpenAsyncResult.cs" />
+    <Compile Include="Sftp\SftpOpenDirAsyncResult.cs" />
     <Compile Include="Sftp\SftpReadAsyncResult.cs" />
+    <Compile Include="Sftp\SftpRealPathAsyncResult.cs" />
     <Compile Include="Sftp\SftpSession.cs">
       <SubType>Code</SubType>
     </Compile>
+    <Compile Include="Sftp\SFtpStatAsyncResult.cs" />
     <Compile Include="Sftp\SftpSynchronizeDirectoriesAsyncResult.cs" />
     <Compile Include="Sftp\SftpUploadAsyncResult.cs" />
     <Compile Include="Sftp\StatusCodes.cs">

+ 3 - 4
src/Renci.SshNet/ServiceFactory.NET.cs

@@ -1,5 +1,4 @@
-using System;
-using Renci.SshNet.NetConf;
+using Renci.SshNet.NetConf;
 
 namespace Renci.SshNet
 {
@@ -10,11 +9,11 @@ namespace Renci.SshNet
         /// and with the specified operation timeout.
         /// </summary>
         /// <param name="session">The <see cref="ISession"/> to create the <see cref="INetConfSession"/> in.</param>
-        /// <param name="operationTimeout">The operation timeout.</param>
+        /// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.</param>
         /// <returns>
         /// An <see cref="INetConfSession"/>.
         /// </returns>
-        public INetConfSession CreateNetConfSession(ISession session, TimeSpan operationTimeout)
+        public INetConfSession CreateNetConfSession(ISession session, int operationTimeout)
         {
             return new NetConfSession(session, operationTimeout);
         }

+ 32 - 4
src/Renci.SshNet/ServiceFactory.cs

@@ -6,6 +6,7 @@ using Renci.SshNet.Common;
 using Renci.SshNet.Messages.Transport;
 using Renci.SshNet.Security;
 using Renci.SshNet.Sftp;
+using Renci.SshNet.Abstractions;
 
 namespace Renci.SshNet
 {
@@ -43,12 +44,12 @@ namespace Renci.SshNet
         /// the specified operation timeout and encoding.
         /// </summary>
         /// <param name="session">The <see cref="ISession"/> to create the <see cref="ISftpSession"/> in.</param>
-        /// <param name="operationTimeout">The operation timeout.</param>
+        /// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.</param>
         /// <param name="encoding">The encoding.</param>
         /// <returns>
         /// An <see cref="ISftpSession"/>.
         /// </returns>
-        public ISftpSession CreateSftpSession(ISession session, TimeSpan operationTimeout, Encoding encoding)
+        public ISftpSession CreateSftpSession(ISession session, int operationTimeout, Encoding encoding)
         {
             return new SftpSession(session, operationTimeout, encoding, this);
         }
@@ -97,9 +98,36 @@ namespace Renci.SshNet
             return keyExchangeAlgorithmType.CreateInstance<IKeyExchange>();
         }
 
-        public ISftpFileReader CreateSftpFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize)
+        public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize)
         {
-            return new SftpFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
+            const int DefaultMaxPendingReads = 3;
+
+            var openAsyncResult = sftpSession.BeginOpen(fileName, Flags.Read, null, null);
+            var statAsyncResult = sftpSession.BeginLStat(fileName, null, null);
+
+            long? fileSize;
+            int maxPendingReads;
+
+            var chunkSize = sftpSession.CalculateOptimalReadLength(bufferSize);
+            var handle = sftpSession.EndOpen(openAsyncResult);
+
+            // fallback to a default maximum of pending reads when remote server does not allow us to obtain
+            // the attributes of the file
+            try
+            {
+                var fileAttributes = sftpSession.EndLStat(statAsyncResult);
+                fileSize = fileAttributes.Size;
+                maxPendingReads = Math.Min(20, (int) Math.Ceiling((double) fileAttributes.Size / chunkSize) + 1);
+            }
+            catch (SshException ex)
+            {
+                fileSize = null;
+                maxPendingReads = DefaultMaxPendingReads;
+
+                DiagnosticAbstraction.Log(string.Format("Failed to obtain size of file. Allowing maximum {0} pending reads: {1}", maxPendingReads, ex));
+            }
+
+            return sftpSession.CreateFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
         }
     }
 }

+ 100 - 1
src/Renci.SshNet/Sftp/ISftpSession.cs

@@ -48,6 +48,37 @@ namespace Renci.SshNet.Sftp
         /// </returns>
         SftpFileAttributes RequestFStat(byte[] handle, bool nullOnError);
 
+        /// <summary>
+        /// Performs SSH_FXP_STAT request.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="nullOnError">if set to <c>true</c> returns null instead of throwing an exception.</param>
+        /// <returns>
+        /// File attributes
+        /// </returns>
+        SftpFileAttributes RequestStat(string path, bool nullOnError = false);
+
+        /// <summary>
+        /// Performs SSH_FXP_STAT request
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginOpen(string, Flags, AsyncCallback, object)"/> completes.</param>
+        /// <param name="state">An object that contains any additional user-defined data.</param>
+        /// <returns>
+        /// A <see cref="SftpOpenAsyncResult"/> that represents the asynchronous call.
+        /// </returns>
+        SFtpStatAsyncResult BeginStat(string path, AsyncCallback callback, object state);
+
+        /// <summary>
+        /// Handles the end of an asynchronous read.
+        /// </summary>
+        /// <param name="asyncResult">An <see cref="SFtpStatAsyncResult"/> that represents an asynchronous call.</param>
+        /// <returns>
+        /// The file attributes.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
+        SftpFileAttributes EndStat(SFtpStatAsyncResult asyncResult);
+
         /// <summary>
         /// Performs SSH_FXP_LSTAT request.
         /// </summary>
@@ -57,6 +88,27 @@ namespace Renci.SshNet.Sftp
         /// </returns>
         SftpFileAttributes RequestLStat(string path);
 
+        /// <summary>
+        /// Performs SSH_FXP_LSTAT request.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginLStat(string, AsyncCallback, object)"/> completes.</param>
+        /// <param name="state">An object that contains any additional user-defined data.</param>
+        /// <returns>
+        /// A <see cref="SFtpStatAsyncResult"/> that represents the asynchronous call.
+        /// </returns>
+        SFtpStatAsyncResult BeginLStat(string path, AsyncCallback callback, object state);
+
+        /// <summary>
+        /// Handles the end of an asynchronous SSH_FXP_LSTAT request.
+        /// </summary>
+        /// <param name="asyncResult">An <see cref="SFtpStatAsyncResult"/> that represents an asynchronous call.</param>
+        /// <returns>
+        /// The file attributes.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
+        SftpFileAttributes EndLStat(SFtpStatAsyncResult asyncResult);
+
         /// <summary>
         /// Performs SSH_FXP_MKDIR request.
         /// </summary>
@@ -72,6 +124,32 @@ namespace Renci.SshNet.Sftp
         /// <returns>File handle.</returns>
         byte[] RequestOpen(string path, Flags flags, bool nullOnError = false);
 
+        /// <summary>
+        /// Performs SSH_FXP_OPEN request
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="flags">The flags.</param>
+        /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginOpen(string, Flags, AsyncCallback, object)"/> completes.</param>
+        /// <param name="state">An object that contains any additional user-defined data.</param>
+        /// <returns>
+        /// A <see cref="SftpOpenAsyncResult"/> that represents the asynchronous call.
+        /// </returns>
+        SftpOpenAsyncResult BeginOpen(string path, Flags flags, AsyncCallback callback, object state);
+
+        /// <summary>
+        /// Handles the end of an asynchronous read.
+        /// </summary>
+        /// <param name="asyncResult">An <see cref="SftpOpenAsyncResult"/> that represents an asynchronous call.</param>
+        /// <returns>
+        /// A <see cref="byte"/> array representing a file handle.
+        /// </returns>
+        /// <remarks>
+        /// If all available data has been read, the <see cref="EndOpen(SftpOpenAsyncResult)"/> method completes
+        /// immediately and returns zero bytes.
+        /// </remarks>
+        /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
+        byte[] EndOpen(SftpOpenAsyncResult asyncResult);
+
         /// <summary>
         /// Performs SSH_FXP_OPENDIR request
         /// </summary>
@@ -130,6 +208,27 @@ namespace Renci.SshNet.Sftp
         /// <returns></returns>
         KeyValuePair<string, SftpFileAttributes>[] RequestReadDir(byte[] handle);
 
+        /// <summary>
+        /// Performs SSH_FXP_REALPATH request.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginRealPath(string, AsyncCallback, object)"/> completes.</param>
+        /// <param name="state">An object that contains any additional user-defined data.</param>
+        /// <returns>
+        /// A <see cref="SftpRealPathAsyncResult"/> that represents the asynchronous call.
+        /// </returns>
+        SftpRealPathAsyncResult BeginRealPath(string path, AsyncCallback callback, object state);
+
+        /// <summary>
+        /// Handles the end of an asynchronous SSH_FXP_REALPATH request.
+        /// </summary>
+        /// <param name="asyncResult">An <see cref="SftpRealPathAsyncResult"/> that represents an asynchronous call.</param>
+        /// <returns>
+        /// The absolute path.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
+        string EndRealPath(SftpRealPathAsyncResult asyncResult);
+
         /// <summary>
         /// Performs SSH_FXP_REMOVE request.
         /// </summary>
@@ -224,6 +323,6 @@ namespace Renci.SshNet.Sftp
         /// </remarks>
         uint CalculateOptimalWriteLength(uint bufferSize, byte[] handle);
 
-        ISftpFileReader CreateFileReader(string fileName, uint bufferSize);
+        ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize);
     }
 }

+ 12 - 0
src/Renci.SshNet/Sftp/SFtpStatAsyncResult.cs

@@ -0,0 +1,12 @@
+using Renci.SshNet.Common;
+using System;
+
+namespace Renci.SshNet.Sftp
+{
+    internal class SFtpStatAsyncResult : AsyncResult<SftpFileAttributes>
+    {
+        public SFtpStatAsyncResult(AsyncCallback asyncCallback, object state) : base(asyncCallback, state)
+        {
+        }
+    }
+}

+ 103 - 39
src/Renci.SshNet/Sftp/SftpFileReader.cs

@@ -8,6 +8,8 @@ namespace Renci.SshNet.Sftp
 {
     internal class SftpFileReader : ISftpFileReader
     {
+        private const int ReadAheadWaitTimeoutInMilliseconds = 1000;
+
         private readonly byte[] _handle;
         private readonly ISftpSession _sftpSession;
         private readonly uint _chunkSize;
@@ -51,8 +53,8 @@ namespace Renci.SshNet.Sftp
         {
             _handle = handle;
             _sftpSession = sftpSession;
-            _fileSize = fileSize;
             _chunkSize = chunkSize;
+            _fileSize = fileSize;
             _semaphore = new SemaphoreLight(maxPendingReads);
             _queue = new Dictionary<int, BufferedRead>(maxPendingReads);
             _readLock = new object();
@@ -63,17 +65,17 @@ namespace Renci.SshNet.Sftp
 
         public byte[] Read()
         {
-            if (_exception != null || _disposed)
+            if (_disposed)
                 throw new ObjectDisposedException(GetType().FullName);
+            if (_exception != null)
+                throw _exception;
             if (_isEndOfFileRead)
                 throw new SshException("Attempting to read beyond the end of the file.");
 
+            BufferedRead nextChunk;
+
             lock (_readLock)
             {
-                BufferedRead nextChunk;
-
-                // TODO: break when we've reached file size and still haven't received an EOF ?
-
                 // wait until either the next chunk is avalable or an exception has occurred
                 while (!_queue.TryGetValue(_nextChunkIndex, out nextChunk) && _exception == null)
                 {
@@ -97,7 +99,7 @@ namespace Renci.SshNet.Sftp
                         // remove processed chunk
                         _queue.Remove(_nextChunkIndex);
                         // update offset
-                        _offset += (ulong)data.Length;
+                        _offset += (ulong) data.Length;
                         // move to next chunk
                         _nextChunkIndex++;
                     }
@@ -118,22 +120,30 @@ namespace Renci.SshNet.Sftp
                     // signal EOF to caller
                     return nextChunk.Data;
                 }
+            }
+
+
+            // when the server returned less bytes than requested (for the previous chunk)
+            // we'll synchronously request the remaining data
+            //
+            // due to the optimization above, we'll only get here in one of the following cases:
+            // - an EOF situation for files for which we were unable to obtain the file size
+            // - fewer bytes that requested were returned
+            // 
+            // according to the SSH specification, this last case should never happen for normal
+            // disk files (but can happen for device files).
+            //
+            // Important:
+            // to avoid a deadlock, this read must be done outside of the read lock
 
-                // when the server returned less bytes than requested (for the previous chunk)
-                // we'll synchronously request the remaining data
-                //
-                // due to the optimization above, we'll only get here in one of the following cases:
-                // - an EOF situation for files for which we were unable to obtain the file size
-                // - fewer bytes that requested were returned
-                // 
-                // according to the SSH specification, this last case should never happen for normal
-                // disk files (but can happen for device files).
-
-                var bytesToCatchUp = nextChunk.Offset - _offset;
-
-                // TODO: break loop and interrupt blocking wait in case of exception
-                var read = _sftpSession.RequestRead(_handle, _offset, (uint) bytesToCatchUp);
-                if (read.Length == 0)
+            var bytesToCatchUp = nextChunk.Offset - _offset;
+
+            // TODO: break loop and interrupt blocking wait in case of exception
+
+            var read = _sftpSession.RequestRead(_handle, _offset, (uint) bytesToCatchUp);
+            if (read.Length == 0)
+            {
+                lock (_readLock)
                 {
                     // a zero-length (EOF) response is only valid for the read-back when EOF has
                     // been signaled for the next read-ahead chunk
@@ -149,16 +159,20 @@ namespace Renci.SshNet.Sftp
 
                     // move reader to error state
                     _exception = new SshException("Unexpectedly reached end of file.");
-                    // unblock wait in read-ahead
-                    _semaphore.Release();
+                    // ensure we've not yet disposed the current instance
+                    if (_semaphore != null)
+                    {
+                        // unblock wait in read-ahead
+                        _semaphore.Release();
+                    }
                     // notify caller of error
                     throw _exception;
                 }
+            }
 
-                _offset += (uint) read.Length;
+            _offset += (uint) read.Length;
 
-                return read;
-            }
+            return read;
         }
 
         public void Dispose()
@@ -166,38 +180,88 @@ namespace Renci.SshNet.Sftp
             Dispose(true);
         }
 
+        /// <summary>
+        /// Releases unmanaged and - optionally - managed resources
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        /// <remarks>
+        /// Note that we will break the read-ahead loop, but will not interrupt the blocking wait
+        /// in that loop.
+        /// </remarks>
         protected void Dispose(bool disposing)
         {
+            if (_disposed)
+                return;
+
             if (disposing)
             {
+                // use this to break the read-ahead loop
+                _exception = new ObjectDisposedException(GetType().FullName);
+
                 var readAheadCompleted = _readAheadCompleted;
                 if (readAheadCompleted != null)
                 {
-                    if (!readAheadCompleted.WaitOne(TimeSpan.FromSeconds(1)))
-                    {
-                        DiagnosticAbstraction.Log("Read-ahead thread did not complete within time-out.");
-                    }
+                    readAheadCompleted.WaitOne();
                     readAheadCompleted.Dispose();
                     _readAheadCompleted = null;
                 }
 
-                _sftpSession.RequestClose(_handle);
+                // dispose semaphore in read lock to ensure we don't run into an ObjectDisposedException
+                // in Read()
+                lock (_readLock)
+                {
+                    if (_semaphore != null)
+                    {
+                        _semaphore.Dispose();
+                        _semaphore = null;
+                    }
+                }
 
-                _disposed = true;
+                _sftpSession.RequestClose(_handle);
             }
+
+            _disposed = true;
         }
 
         private void StartReadAhead()
         {
             ThreadAbstraction.ExecuteThread(() =>
             {
-                // TODO: take dispose into account
-                while (_exception == null)
+                while (!_endOfFileReceived && _exception == null)
                 {
-                    // TODO implement cancellation!?
-                    // TODO implement IDisposable to cancel the Wait in case the client never completes reading to EOF
-                    // TODO check if the BCL Semaphore unblocks wait on dispose (and mimick same behavior in our SemaphoreLight ?)
-                    _semaphore.Wait();
+                    // wait on either a release on the semaphore, or signaling of a broken session/connection
+                    try
+                    {
+                        _sftpSession.WaitOnHandle(_semaphore.AvailableWaitHandle, _sftpSession.OperationTimeout);
+                    }
+                    catch (Exception ex)
+                    {
+                        _exception = ex;
+
+                        // unblock the Read()
+                        lock (_readLock)
+                        {
+                            Monitor.Pulse(_readLock);
+                        }
+
+                        // break the read-ahead loop
+                        break;
+                    }
+
+                    // attempt to obtain the semaphore; this may time out when all semaphores are
+                    // in use due to pending read-aheads
+                    //
+                    // in general this only happens when the session is broken
+                    if (!_semaphore.Wait(ReadAheadWaitTimeoutInMilliseconds))
+                    {
+                        // unblock the Read()
+                        lock (_readLock)
+                        {
+                            Monitor.Pulse(_readLock);
+                        }
+                        // re-evaluate whether an exception occurred, and - if not - wait again
+                        continue;
+                    }
 
                     // don't bother reading any more chunks if we received EOF, or an exception has occurred
                     // while processing a chunk

+ 12 - 0
src/Renci.SshNet/Sftp/SftpOpenAsyncResult.cs

@@ -0,0 +1,12 @@
+using Renci.SshNet.Common;
+using System;
+
+namespace Renci.SshNet.Sftp
+{
+    internal class SftpOpenAsyncResult : AsyncResult<byte[]>
+    {
+        public SftpOpenAsyncResult(AsyncCallback asyncCallback, object state) : base(asyncCallback, state)
+        {
+        }
+    }
+}

+ 12 - 0
src/Renci.SshNet/Sftp/SftpOpenDirAsyncResult.cs

@@ -0,0 +1,12 @@
+using Renci.SshNet.Common;
+using System;
+
+namespace Renci.SshNet.Sftp
+{
+    internal class SftpOpenDirAsyncResult : AsyncResult<byte[]>
+    {
+        public SftpOpenDirAsyncResult(AsyncCallback asyncCallback, object state) : base(asyncCallback, state)
+        {
+        }
+    }
+}

+ 12 - 0
src/Renci.SshNet/Sftp/SftpRealPathAsyncResult.cs

@@ -0,0 +1,12 @@
+using Renci.SshNet.Common;
+using System;
+
+namespace Renci.SshNet.Sftp
+{
+    internal class SftpRealPathAsyncResult : AsyncResult<string>
+    {
+        public SftpRealPathAsyncResult(AsyncCallback asyncCallback, object state) : base(asyncCallback, state)
+        {
+        }
+    }
+}

+ 218 - 25
src/Renci.SshNet/Sftp/SftpSession.cs

@@ -50,7 +50,7 @@ namespace Renci.SshNet.Sftp
             }
         }
 
-        public SftpSession(ISession session, TimeSpan operationTimeout, Encoding encoding, IServiceFactory serviceFactory)
+        public SftpSession(ISession session, int operationTimeout, Encoding encoding, IServiceFactory serviceFactory)
             : base(session, "sftp", operationTimeout, encoding)
         {
             _serviceFactory = serviceFactory;
@@ -130,28 +130,9 @@ namespace Renci.SshNet.Sftp
             return string.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", canonizedPath, slash, pathParts[pathParts.Length - 1]);
         }
 
-        public ISftpFileReader CreateFileReader(string fileName, uint bufferSize)
+        public ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize)
         {
-            var handle = RequestOpen(fileName, Flags.Read);
-
-            long? fileSize;
-            int maxPendingReads;
-
-            var chunkSize = CalculateOptimalReadLength(bufferSize);
-
-            var fileAttributes = RequestFStat(handle, true);
-            if (fileAttributes == null)
-            {
-                fileSize = null;
-                maxPendingReads = 5;
-            }
-            else
-            {
-                fileSize = fileAttributes.Size;
-                maxPendingReads = Math.Min(10, (int)Math.Ceiling((double)fileAttributes.Size / chunkSize) + 1);
-            }
-
-            return _serviceFactory.CreateSftpFileReader(handle, this, chunkSize, maxPendingReads, fileSize);
+            return new SftpFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
         }
 
         internal string GetFullRemotePath(string path)
@@ -396,6 +377,62 @@ namespace Renci.SshNet.Sftp
             return handle;
         }
 
+        /// <summary>
+        /// Performs SSH_FXP_OPEN request
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="flags">The flags.</param>
+        /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginOpen(string, Flags, AsyncCallback, object)"/> completes.</param>
+        /// <param name="state">An object that contains any additional user-defined data.</param>
+        /// <returns>
+        /// A <see cref="SftpOpenAsyncResult"/> that represents the asynchronous call.
+        /// </returns>
+        public SftpOpenAsyncResult BeginOpen(string path, Flags flags, AsyncCallback callback, object state)
+        {
+            var asyncResult = new SftpOpenAsyncResult(callback, state);
+
+            var request = new SftpOpenRequest(ProtocolVersion, NextRequestId, path, Encoding, flags,
+                response =>
+                {
+                    asyncResult.SetAsCompleted(response.Handle, false);
+                },
+                response =>
+                {
+                    asyncResult.SetAsCompleted(GetSftpException(response), false);
+                });
+
+            SendRequest(request);
+
+            return asyncResult;
+        }
+
+        /// <summary>
+        /// Handles the end of an asynchronous read.
+        /// </summary>
+        /// <param name="asyncResult">An <see cref="SftpOpenAsyncResult"/> that represents an asynchronous call.</param>
+        /// <returns>
+        /// A <see cref="byte"/> array representing a file handle.
+        /// </returns>
+        /// <remarks>
+        /// If all available data has been read, the <see cref="EndOpen(SftpOpenAsyncResult)"/> method completes
+        /// immediately and returns zero bytes.
+        /// </remarks>
+        /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
+        public byte[] EndOpen(SftpOpenAsyncResult asyncResult)
+        {
+            if (asyncResult == null)
+                throw new ArgumentNullException("asyncResult");
+
+            if (asyncResult.EndInvokeCalled)
+                throw new InvalidOperationException("EndOpen has already been called.");
+
+            using (var waitHandle = asyncResult.AsyncWaitHandle)
+            {
+                WaitOnHandle(waitHandle, OperationTimeout);
+                return asyncResult.EndInvoke();
+            }
+        }
+
         /// <summary>
         /// Performs SSH_FXP_CLOSE request.
         /// </summary>
@@ -480,7 +517,11 @@ namespace Renci.SshNet.Sftp
             if (asyncResult.EndInvokeCalled)
                 throw new InvalidOperationException("EndRead has already been called.");
 
-            return asyncResult.EndInvoke();
+            using (var waitHandle = asyncResult.AsyncWaitHandle)
+            {
+                WaitOnHandle(waitHandle, OperationTimeout);
+                return asyncResult.EndInvoke();
+            }
         }
 
         /// <summary>
@@ -613,6 +654,56 @@ namespace Renci.SshNet.Sftp
             return attributes;
         }
 
+        /// <summary>
+        /// Performs SSH_FXP_LSTAT request.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginLStat(string, AsyncCallback, object)"/> completes.</param>
+        /// <param name="state">An object that contains any additional user-defined data.</param>
+        /// <returns>
+        /// A <see cref="SFtpStatAsyncResult"/> that represents the asynchronous call.
+        /// </returns>
+        public SFtpStatAsyncResult BeginLStat(string path, AsyncCallback callback, object state)
+        {
+            var asyncResult = new SFtpStatAsyncResult(callback, state);
+
+            var request = new SftpLStatRequest(ProtocolVersion, NextRequestId, path, Encoding,
+                response =>
+                {
+                    asyncResult.SetAsCompleted(response.Attributes, false);
+                },
+                response =>
+                {
+                    asyncResult.SetAsCompleted(GetSftpException(response), false);
+                });
+            SendRequest(request);
+
+            return asyncResult;
+        }
+
+        /// <summary>
+        /// Handles the end of an asynchronous SSH_FXP_LSTAT request.
+        /// </summary>
+        /// <param name="asyncResult">An <see cref="SFtpStatAsyncResult"/> that represents an asynchronous call.</param>
+        /// <returns>
+        /// The file attributes.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
+        public SftpFileAttributes EndLStat(SFtpStatAsyncResult asyncResult)
+        {
+            if (asyncResult == null)
+                throw new ArgumentNullException("asyncResult");
+
+            if (asyncResult.EndInvokeCalled)
+                throw new InvalidOperationException("EndLStat has already been called.");
+
+            using (var waitHandle = asyncResult.AsyncWaitHandle)
+            {
+                WaitOnHandle(waitHandle, OperationTimeout);
+                return asyncResult.EndInvoke();
+            }
+        }
+
         /// <summary>
         /// Performs SSH_FXP_FSTAT request.
         /// </summary>
@@ -880,7 +971,9 @@ namespace Renci.SshNet.Sftp
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="nullOnError">if set to <c>true</c> returns null instead of throwing an exception.</param>
-        /// <returns></returns>
+        /// <returns>
+        /// The absolute path.
+        /// </returns>
         internal KeyValuePair<string, SftpFileAttributes>[] RequestRealPath(string path, bool nullOnError = false)
         {
             SshException exception = null;
@@ -914,6 +1007,56 @@ namespace Renci.SshNet.Sftp
             return result;
         }
 
+        /// <summary>
+        /// Performs SSH_FXP_REALPATH request.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginRealPath(string, AsyncCallback, object)"/> completes.</param>
+        /// <param name="state">An object that contains any additional user-defined data.</param>
+        /// <returns>
+        /// A <see cref="SftpRealPathAsyncResult"/> that represents the asynchronous call.
+        /// </returns>
+        public SftpRealPathAsyncResult BeginRealPath(string path, AsyncCallback callback, object state)
+        {
+            var asyncResult = new SftpRealPathAsyncResult(callback, state);
+
+            var request = new SftpRealPathRequest(ProtocolVersion, NextRequestId, path, Encoding,
+                response =>
+                {
+                    asyncResult.SetAsCompleted(response.Files[0].Key, false);
+                },
+                response =>
+                {
+                    asyncResult.SetAsCompleted(GetSftpException(response), false);
+                });
+            SendRequest(request);
+
+            return asyncResult;
+        }
+
+        /// <summary>
+        /// Handles the end of an asynchronous SSH_FXP_REALPATH request.
+        /// </summary>
+        /// <param name="asyncResult">An <see cref="SftpRealPathAsyncResult"/> that represents an asynchronous call.</param>
+        /// <returns>
+        /// The absolute path.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
+        public string EndRealPath(SftpRealPathAsyncResult asyncResult)
+        {
+            if (asyncResult == null)
+                throw new ArgumentNullException("asyncResult");
+
+            if (asyncResult.EndInvokeCalled)
+                throw new InvalidOperationException("EndRealPath has already been called.");
+
+            using (var waitHandle = asyncResult.AsyncWaitHandle)
+            {
+                WaitOnHandle(waitHandle, OperationTimeout);
+                return asyncResult.EndInvoke();
+            }
+        }
+
         /// <summary>
         /// Performs SSH_FXP_STAT request.
         /// </summary>
@@ -922,7 +1065,7 @@ namespace Renci.SshNet.Sftp
         /// <returns>
         /// File attributes
         /// </returns>
-        internal SftpFileAttributes RequestStat(string path, bool nullOnError = false)
+        public SftpFileAttributes RequestStat(string path, bool nullOnError = false)
         {
             SshException exception = null;
 
@@ -955,6 +1098,56 @@ namespace Renci.SshNet.Sftp
             return attributes;
         }
 
+        /// <summary>
+        /// Performs SSH_FXP_STAT request
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginStat(string, AsyncCallback, object)"/> completes.</param>
+        /// <param name="state">An object that contains any additional user-defined data.</param>
+        /// <returns>
+        /// A <see cref="SFtpStatAsyncResult"/> that represents the asynchronous call.
+        /// </returns>
+        public SFtpStatAsyncResult BeginStat(string path, AsyncCallback callback, object state)
+        {
+            var asyncResult = new SFtpStatAsyncResult(callback, state);
+
+            var request = new SftpStatRequest(ProtocolVersion, NextRequestId, path, Encoding,
+                response =>
+                {
+                    asyncResult.SetAsCompleted(response.Attributes, false);
+                },
+                response =>
+                {
+                    asyncResult.SetAsCompleted(GetSftpException(response), false);
+                });
+            SendRequest(request);
+
+            return asyncResult;
+        }
+
+        /// <summary>
+        /// Handles the end of an asynchronous read.
+        /// </summary>
+        /// <param name="asyncResult">An <see cref="SFtpStatAsyncResult"/> that represents an asynchronous call.</param>
+        /// <returns>
+        /// The file attributes.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
+        public SftpFileAttributes EndStat(SFtpStatAsyncResult asyncResult)
+        {
+            if (asyncResult == null)
+                throw new ArgumentNullException("asyncResult");
+
+            if (asyncResult.EndInvokeCalled)
+                throw new InvalidOperationException("EndStat has already been called.");
+
+            using (var waitHandle = asyncResult.AsyncWaitHandle)
+            {
+                WaitOnHandle(waitHandle, OperationTimeout);
+                return asyncResult.EndInvoke();
+            }
+        }
+
         /// <summary>
         /// Performs SSH_FXP_RENAME request.
         /// </summary>

+ 48 - 43
src/Renci.SshNet/SftpClient.cs

@@ -1,15 +1,16 @@
 using System;
-using System.Linq;
+
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
-using Renci.SshNet.Sftp;
-using System.Text;
-using Renci.SshNet.Common;
 using System.Globalization;
-using System.Threading;
-using System.Diagnostics.CodeAnalysis;
+using System.Linq;
 using System.Net;
+using System.Text;
+using System.Threading;
 using Renci.SshNet.Abstractions;
+using Renci.SshNet.Common;
+using Renci.SshNet.Sftp;
 
 namespace Renci.SshNet
 {
@@ -27,7 +28,7 @@ namespace Renci.SshNet
         /// <summary>
         /// Holds the operation timeout.
         /// </summary>
-        private TimeSpan _operationTimeout;
+        private int _operationTimeout;
 
         /// <summary>
         /// Holds the size of the buffer.
@@ -47,12 +48,18 @@ namespace Renci.SshNet
             get
             {
                 CheckDisposed();
-                return _operationTimeout;
+
+                return TimeSpan.FromMilliseconds(_operationTimeout);
             }
             set
             {
                 CheckDisposed();
-                _operationTimeout = value;
+
+                var timeoutInMilliseconds = value.TotalMilliseconds;
+                if (timeoutInMilliseconds < -1d || timeoutInMilliseconds > int.MaxValue)
+                    throw new ArgumentOutOfRangeException("timeout", "The timeout must represent a value between -1 and Int32.MaxValue, inclusive.");
+
+                _operationTimeout = (int) timeoutInMilliseconds;
             }
         }
 
@@ -230,7 +237,7 @@ namespace Renci.SshNet
         internal SftpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory)
             : base(connectionInfo, ownsConnectionInfo, serviceFactory)
         {
-            OperationTimeout = SshNet.Session.InfiniteTimeSpan;
+            _operationTimeout = SshNet.Session.Infinite;
             BufferSize = 1024 * 32;
         }
 
@@ -244,7 +251,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to change directory denied by remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void ChangeDirectory(string path)
         {
@@ -268,7 +275,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to change permission on the path(s) was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void ChangePermissions(string path, short mode)
         {
@@ -283,7 +290,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException"><paramref name="path"/> is <b>null</b> or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to create the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void CreateDirectory(string path)
         {
@@ -308,7 +315,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to delete the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void DeleteDirectory(string path)
         {
@@ -333,7 +340,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to delete the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void DeleteFile(string path)
         {
@@ -358,7 +365,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentNullException"><paramref name="oldPath"/> is <b>null</b>. <para>-or-</para> or <paramref name="newPath"/> is <b>null</b>.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to rename the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void RenameFile(string oldPath, string newPath)
         {
@@ -374,7 +381,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentNullException"><paramref name="oldPath" /> is <b>null</b>. <para>-or-</para> or <paramref name="newPath" /> is <b>null</b>.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to rename the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void RenameFile(string oldPath, string newPath, bool isPosix)
         {
@@ -411,7 +418,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException"><paramref name="path"/> is <b>null</b>. <para>-or-</para> <paramref name="linkPath"/> is <b>null</b> or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to create the symbolic link was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void SymbolicLink(string path, string linkPath)
         {
@@ -444,7 +451,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public IEnumerable<SftpFile> ListDirectory(string path, Action<int> listCallback = null)
         {
@@ -552,7 +559,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException"><paramref name="path"/> is <b>null</b> or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public bool Exists(string path)
         {
@@ -566,9 +573,9 @@ namespace Renci.SshNet
 
             var fullPath = _sftpSession.GetCanonicalPath(path);
 
-            // using SSH_FXP_REALPATH is not an alternative the SFTP specification has not always
-            // been clear on how the server should respond when the specified path is not present
-            // on the server:
+            // using SSH_FXP_REALPATH is not an alternative as the SFTP specification has not always
+            // been clear on how the server should respond when the specified path is not present on
+            // the server:
             // 
             // SSH 1 to 4:
             // No mention of how the server should respond if the path is not present on the server.
@@ -605,7 +612,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>/// 
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
@@ -629,7 +636,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException"><paramref name="path" /> is <b>null</b> or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
@@ -652,7 +659,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException"><paramref name="path" /> is <b>null</b> or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
@@ -724,7 +731,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="SftpPathNotFoundException">The path was not found on the remote host.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         public void EndDownloadFile(IAsyncResult asyncResult)
         {
             var ar = asyncResult as SftpDownloadAsyncResult;
@@ -746,7 +753,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException"><paramref name="path" /> is <b>null</b> or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to upload the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
@@ -767,7 +774,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException"><paramref name="path" /> is <b>null</b> or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to upload the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
@@ -798,7 +805,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException"><paramref name="path" /> is <b>null</b> or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// <para>
@@ -826,7 +833,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException"><paramref name="path" /> is <b>null</b> or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// <para>
@@ -856,7 +863,7 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentException"><paramref name="path" /> is <b>null</b> or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// <para>
@@ -949,7 +956,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="SftpPathNotFoundException">The directory of the file was not found on the remote host.</exception>
         /// <exception cref="SftpPermissionDeniedException">Permission to upload the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
-        /// <exception cref="SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         public void EndUploadFile(IAsyncResult asyncResult)
         {
             var ar = asyncResult as SftpUploadAsyncResult;
@@ -1498,7 +1505,7 @@ namespace Renci.SshNet
         /// Sets the date and time the specified file was last accessed.
         /// </summary>
         /// <param name="path">The file for which to set the access date and time information.</param>
-        /// <param name="lastAccessTime">A <see cref="System.DateTime"/> containing the value to set for the last access date and time of path. This value is expressed in local time.</param>
+        /// <param name="lastAccessTime">A <see cref="DateTime"/> containing the value to set for the last access date and time of path. This value is expressed in local time.</param>
         [Obsolete("Note: This method currently throws NotImplementedException because it has not yet been implemented.")]
         public void SetLastAccessTime(string path, DateTime lastAccessTime)
         {
@@ -1509,7 +1516,7 @@ namespace Renci.SshNet
         /// Sets the date and time, in coordinated universal time (UTC), that the specified file was last accessed.
         /// </summary>
         /// <param name="path">The file for which to set the access date and time information.</param>
-        /// <param name="lastAccessTimeUtc">A <see cref="System.DateTime"/> containing the value to set for the last access date and time of path. This value is expressed in UTC time.</param>
+        /// <param name="lastAccessTimeUtc">A <see cref="DateTime"/> containing the value to set for the last access date and time of path. This value is expressed in UTC time.</param>
         [Obsolete("Note: This method currently throws NotImplementedException because it has not yet been implemented.")]
         public void SetLastAccessTimeUtc(string path, DateTime lastAccessTimeUtc)
         {
@@ -1520,7 +1527,7 @@ namespace Renci.SshNet
         /// Sets the date and time that the specified file was last written to.
         /// </summary>
         /// <param name="path">The file for which to set the date and time information.</param>
-        /// <param name="lastWriteTime">A System.DateTime containing the value to set for the last write date and time of path. This value is expressed in local time.</param>
+        /// <param name="lastWriteTime">A <see cref="DateTime"/> containing the value to set for the last write date and time of path. This value is expressed in local time.</param>
         [Obsolete("Note: This method currently throws NotImplementedException because it has not yet been implemented.")]
         public void SetLastWriteTime(string path, DateTime lastWriteTime)
         {
@@ -1531,7 +1538,7 @@ namespace Renci.SshNet
         /// Sets the date and time, in coordinated universal time (UTC), that the specified file was last written to.
         /// </summary>
         /// <param name="path">The file for which to set the date and time information.</param>
-        /// <param name="lastWriteTimeUtc">A System.DateTime containing the value to set for the last write date and time of path. This value is expressed in UTC time.</param>
+        /// <param name="lastWriteTimeUtc">A <see cref="DateTime"/> containing the value to set for the last write date and time of path. This value is expressed in UTC time.</param>
         [Obsolete("Note: This method currently throws NotImplementedException because it has not yet been implemented.")]
         public void SetLastWriteTimeUtc(string path, DateTime lastWriteTimeUtc)
         {
@@ -1995,14 +2002,12 @@ namespace Renci.SshNet
 
             var fullPath = _sftpSession.GetCanonicalPath(path);
 
-            using (var fileReader = _sftpSession.CreateFileReader(fullPath, _bufferSize))
+            using (var fileReader = ServiceFactory.CreateSftpFileReader(fullPath, _sftpSession, _bufferSize))
             {
                 var totalBytesRead = 0UL;
 
                 while (true)
                 {
-                    // TODO: cancel read ahead when download is canceled by user
-
                     //  Cancel download
                     if (asyncResult != null && asyncResult.IsDownloadCanceled)
                         break;
@@ -2013,7 +2018,7 @@ namespace Renci.SshNet
 
                     output.Write(data, 0, data.Length);
 
-                    totalBytesRead += (ulong)data.Length;
+                    totalBytesRead += (ulong) data.Length;
 
                     if (downloadCallback != null)
                     {
@@ -2096,7 +2101,7 @@ namespace Renci.SshNet
                 else if (expectedResponses > 0)
                 {
                     //  Wait for expectedResponses to change
-                    _sftpSession.WaitOnHandle(responseReceivedWaitHandle, OperationTimeout);
+                    _sftpSession.WaitOnHandle(responseReceivedWaitHandle, _operationTimeout);
                 }
             } while (expectedResponses > 0 || bytesRead > 0);
 
@@ -2110,7 +2115,7 @@ namespace Renci.SshNet
         {
             base.OnConnected();
 
-            _sftpSession = ServiceFactory.CreateSftpSession(Session, OperationTimeout, ConnectionInfo.Encoding);
+            _sftpSession = ServiceFactory.CreateSftpSession(Session, _operationTimeout, ConnectionInfo.Encoding);
             _sftpSession.Connect();
         }
 

+ 11 - 8
src/Renci.SshNet/SubsystemSession.cs

@@ -22,9 +22,12 @@ namespace Renci.SshNet
         private EventWaitHandle _channelClosedWaitHandle = new ManualResetEvent(false);
 
         /// <summary>
-        /// Specifies a timeout to wait for operation to complete
+        /// Gets or set the number of seconds to wait for an operation to complete.
         /// </summary>
-        protected TimeSpan OperationTimeout { get; private set; }
+        /// <value>
+        /// The number of seconds to wait for an operation to complete, or -1 to wait indefinitely.
+        /// </value>
+        public int OperationTimeout { get; private set; }
 
         /// <summary>
         /// Occurs when an error occurred.
@@ -73,10 +76,10 @@ namespace Renci.SshNet
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="subsystemName">Name of the subsystem.</param>
-        /// <param name="operationTimeout">The operation timeout.</param>
+        /// <param name="operationTimeout">The number of milliseconds to wait for a given operation to complete, or -1 to wait indefinitely.</param>
         /// <param name="encoding">The character encoding to use.</param>
         /// <exception cref="ArgumentNullException"><paramref name="session" /> or <paramref name="subsystemName" /> or <paramref name="encoding"/> is <c>null</c>.</exception>
-        protected SubsystemSession(ISession session, string subsystemName, TimeSpan operationTimeout, Encoding encoding)
+        protected SubsystemSession(ISession session, string subsystemName, int operationTimeout, Encoding encoding)
         {
             if (session == null)
                 throw new ArgumentNullException("session");
@@ -208,11 +211,11 @@ namespace Renci.SshNet
         /// Waits a specified time for a given <see cref="WaitHandle"/> to get signaled.
         /// </summary>
         /// <param name="waitHandle">The handle to wait for.</param>
-        /// <param name="operationTimeout">The time to wait for <paramref name="waitHandle"/> to get signaled.</param>
+        /// <param name="millisecondsTimeout">To number of milliseconds to wait for <paramref name="waitHandle"/> to get signaled, or -1 to wait indefinitely.</param>
         /// <exception cref="SshException">The connection was closed by the server.</exception>
         /// <exception cref="SshException">The channel was closed.</exception>
-        /// <exception cref="SshOperationTimeoutException">The handle did not get signaled within the specified <paramref name="operationTimeout"/>.</exception>
-        public void WaitOnHandle(WaitHandle waitHandle, TimeSpan operationTimeout)
+        /// <exception cref="SshOperationTimeoutException">The handle did not get signaled within the specified timeout.</exception>
+        public void WaitOnHandle(WaitHandle waitHandle, int millisecondsTimeout)
         {
             var waitHandles = new[]
                 {
@@ -222,7 +225,7 @@ namespace Renci.SshNet
                     waitHandle
                 };
 
-            switch (WaitHandle.WaitAny(waitHandles, operationTimeout))
+            switch (WaitHandle.WaitAny(waitHandles, millisecondsTimeout))
             {
                 case 0:
                     throw _exception;