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

Avoid overlapping SSH_FXP_OPEN and SSH_FXP_LSTAT requests for the same file as this causes a performance degradation on Sun SSH.
Switch to a single (sync) read-ahead if the file size is known, and we're starting to read (ahead) past that file size.
Harden against exceptions closing the file handle in Dispose

Fixes issue #292.

Gert Driesen 8 éve
szülő
commit
80ae79ca4c
26 módosított fájl, 1383 hozzáadás és 48 törlés
  1. 34 1
      src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj
  2. 95 0
      src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs
  3. 98 0
      src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs
  4. 98 0
      src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs
  5. 98 0
      src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize.cs
  6. 98 0
      src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize.cs
  7. 98 0
      src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize.cs
  8. 98 0
      src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanTenTimesGreaterThanChunkSize.cs
  9. 98 0
      src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero.cs
  10. 31 9
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead.cs
  11. 138 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsNotOpen.cs
  12. 141 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsOpen_BeginCloseThrowsException.cs
  13. 146 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsOpen_EndCloseThrowsException.cs
  14. 10 8
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_LastChunkBeforeEofIsComplete.cs
  15. 2 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_LastChunkBeforeEofIsPartial.cs
  16. 2 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsNotReached.cs
  17. 3 1
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsReached.cs
  18. 2 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadEndInvokeException_DiscardsFurtherReadAheads.cs
  19. 2 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadEndInvokeException_PreventsFurtherReadAheads.cs
  20. 2 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_ChunkAvailable.cs
  21. 2 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_NoChunkAvailable.cs
  22. 2 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReahAheadExceptionInBeginRead.cs
  23. 7 7
      src/Renci.SshNet.Tests/Common/SftpFileAttributesBuilder.cs
  24. 11 0
      src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
  25. 4 2
      src/Renci.SshNet/ServiceFactory.cs
  26. 63 20
      src/Renci.SshNet/Sftp/SftpFileReader.cs

+ 34 - 1
src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj

@@ -888,6 +888,30 @@
     <Compile Include="..\Renci.SshNet.Tests\Classes\Security\KeyHostAlgorithmTest.cs">
       <Link>Classes\Security\KeyHostAlgorithmTest.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs">
+      <Link>Classes\ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs">
+      <Link>Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs">
+      <Link>Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize.cs">
+      <Link>Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize.cs">
+      <Link>Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize.cs">
+      <Link>Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanTenTimesGreaterThanChunkSize.cs">
+      <Link>Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanTenTimesGreaterThanChunkSize.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero.cs">
+      <Link>Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet.Tests\Classes\SessionTest.cs">
       <Link>Classes\SessionTest.cs</Link>
     </Compile>
@@ -1083,6 +1107,15 @@
     <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead.cs">
       <Link>Classes\Sftp\SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileReaderTest_Dispose_SftpSessionIsNotOpen.cs">
+      <Link>Classes\Sftp\SftpFileReaderTest_Dispose_SftpSessionIsNotOpen.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileReaderTest_Dispose_SftpSessionIsOpen_BeginCloseThrowsException.cs">
+      <Link>Classes\Sftp\SftpFileReaderTest_Dispose_SftpSessionIsOpen_BeginCloseThrowsException.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileReaderTest_Dispose_SftpSessionIsOpen_EndCloseThrowsException.cs">
+      <Link>Classes\Sftp\SftpFileReaderTest_Dispose_SftpSessionIsOpen_EndCloseThrowsException.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileReaderTest_LastChunkBeforeEofIsComplete.cs">
       <Link>Classes\Sftp\SftpFileReaderTest_LastChunkBeforeEofIsComplete.cs</Link>
     </Compile>
@@ -1599,7 +1632,7 @@
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <ProjectExtensions>
     <VisualStudio>
-      <UserProperties ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
+      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 95 - 0
src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs

@@ -0,0 +1,95 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Common;
+using Renci.SshNet.Sftp;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException
+    {
+        private ServiceFactory _serviceFactory;
+        private Mock<ISftpSession> _sftpSessionMock;
+        private Mock<ISftpFileReader> _sftpFileReaderMock;
+        private uint _bufferSize;
+        private string _fileName;
+        private SftpOpenAsyncResult _openAsyncResult;
+        private byte[] _handle;
+        private SFtpStatAsyncResult _statAsyncResult;
+        private uint _chunkSize;
+        private ISftpFileReader _actual;
+
+        private void SetupData()
+        {
+            var random = new Random();
+
+            _bufferSize = (uint)random.Next(1, int.MaxValue);
+            _openAsyncResult = new SftpOpenAsyncResult(null, null);
+            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
+            _statAsyncResult = new SFtpStatAsyncResult(null, null);
+            _fileName = random.Next().ToString();
+            _chunkSize = (uint) random.Next(1, int.MaxValue);
+        }
+
+        private void CreateMocks()
+        {
+            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
+            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
+        }
+
+        private void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
+                            .Returns(_openAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndOpen(_openAsyncResult))
+                            .Returns(_handle);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginLStat(_fileName, null, null))
+                            .Returns(_statAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
+                            .Returns(_chunkSize);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndLStat(_statAsyncResult))
+                            .Throws(new SshException());
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 3, null))
+                            .Returns(_sftpFileReaderMock.Object);
+        }
+
+        private void Arrange()
+        {
+            SetupData();
+            CreateMocks();
+            SetupMocks();
+
+            _serviceFactory = new ServiceFactory();
+        }
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        private void Act()
+        {
+            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
+        }
+
+        [TestMethod]
+        public void CreateSftpFileReaderShouldReturnCreatedInstance()
+        {
+            Assert.IsNotNull(_actual);
+            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
+        }
+
+    }
+}

+ 98 - 0
src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs

@@ -0,0 +1,98 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Sftp;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize
+    {
+        private ServiceFactory _serviceFactory;
+        private Mock<ISftpSession> _sftpSessionMock;
+        private Mock<ISftpFileReader> _sftpFileReaderMock;
+        private uint _bufferSize;
+        private string _fileName;
+        private SftpOpenAsyncResult _openAsyncResult;
+        private byte[] _handle;
+        private SFtpStatAsyncResult _statAsyncResult;
+        private uint _chunkSize;
+        private SftpFileAttributes _fileAttributes;
+        private long _fileSize;
+        private ISftpFileReader _actual;
+
+        private void SetupData()
+        {
+            var random = new Random();
+
+            _bufferSize = (uint) random.Next(1, int.MaxValue);
+            _openAsyncResult = new SftpOpenAsyncResult(null, null);
+            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
+            _statAsyncResult = new SFtpStatAsyncResult(null, null);
+            _fileName = random.Next().ToString();
+            _chunkSize = (uint) random.Next(1000, 5000);
+            _fileSize = (_chunkSize * 6) - 10;
+            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
+        }
+
+        private void CreateMocks()
+        {
+            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
+            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
+        }
+
+        private void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
+                            .Returns(_openAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndOpen(_openAsyncResult))
+                            .Returns(_handle);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginLStat(_fileName, null, null))
+                            .Returns(_statAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
+                            .Returns(_chunkSize);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndLStat(_statAsyncResult))
+                            .Returns(_fileAttributes);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 7, _fileSize))
+                            .Returns(_sftpFileReaderMock.Object);
+        }
+
+        private void Arrange()
+        {
+            SetupData();
+            CreateMocks();
+            SetupMocks();
+
+            _serviceFactory = new ServiceFactory();
+        }
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        private void Act()
+        {
+            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
+        }
+
+        [TestMethod]
+        public void CreateSftpFileReaderShouldReturnCreatedInstance()
+        {
+            Assert.IsNotNull(_actual);
+            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
+        }
+    }
+}

+ 98 - 0
src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs

@@ -0,0 +1,98 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Sftp;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize
+    {
+        private ServiceFactory _serviceFactory;
+        private Mock<ISftpSession> _sftpSessionMock;
+        private Mock<ISftpFileReader> _sftpFileReaderMock;
+        private uint _bufferSize;
+        private string _fileName;
+        private SftpOpenAsyncResult _openAsyncResult;
+        private byte[] _handle;
+        private SFtpStatAsyncResult _statAsyncResult;
+        private uint _chunkSize;
+        private SftpFileAttributes _fileAttributes;
+        private long _fileSize;
+        private ISftpFileReader _actual;
+
+        private void SetupData()
+        {
+            var random = new Random();
+
+            _bufferSize = (uint)random.Next(1, int.MaxValue);
+            _openAsyncResult = new SftpOpenAsyncResult(null, null);
+            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
+            _statAsyncResult = new SFtpStatAsyncResult(null, null);
+            _fileName = random.Next().ToString();
+            _chunkSize = (uint)random.Next(1000, int.MaxValue);
+            _fileSize = _chunkSize;
+            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
+        }
+
+        private void CreateMocks()
+        {
+            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
+            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
+        }
+
+        private void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
+                            .Returns(_openAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndOpen(_openAsyncResult))
+                            .Returns(_handle);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginLStat(_fileName, null, null))
+                            .Returns(_statAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
+                            .Returns(_chunkSize);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndLStat(_statAsyncResult))
+                            .Returns(_fileAttributes);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 2, _fileSize))
+                            .Returns(_sftpFileReaderMock.Object);
+        }
+
+        private void Arrange()
+        {
+            SetupData();
+            CreateMocks();
+            SetupMocks();
+
+            _serviceFactory = new ServiceFactory();
+        }
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        private void Act()
+        {
+            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
+        }
+
+        [TestMethod]
+        public void CreateSftpFileReaderShouldReturnCreatedInstance()
+        {
+            Assert.IsNotNull(_actual);
+            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
+        }
+    }
+}

+ 98 - 0
src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize.cs

@@ -0,0 +1,98 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Sftp;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize
+    {
+        private ServiceFactory _serviceFactory;
+        private Mock<ISftpSession> _sftpSessionMock;
+        private Mock<ISftpFileReader> _sftpFileReaderMock;
+        private uint _bufferSize;
+        private string _fileName;
+        private SftpOpenAsyncResult _openAsyncResult;
+        private byte[] _handle;
+        private SFtpStatAsyncResult _statAsyncResult;
+        private uint _chunkSize;
+        private SftpFileAttributes _fileAttributes;
+        private long _fileSize;
+        private ISftpFileReader _actual;
+
+        private void SetupData()
+        {
+            var random = new Random();
+
+            _bufferSize = (uint) random.Next(1, int.MaxValue);
+            _openAsyncResult = new SftpOpenAsyncResult(null, null);
+            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
+            _statAsyncResult = new SFtpStatAsyncResult(null, null);
+            _fileName = random.Next().ToString();
+            _chunkSize = (uint) random.Next(1000, 5000);
+            _fileSize = _chunkSize * 5;
+            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
+        }
+
+        private void CreateMocks()
+        {
+            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
+            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
+        }
+
+        private void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
+                            .Returns(_openAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndOpen(_openAsyncResult))
+                            .Returns(_handle);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginLStat(_fileName, null, null))
+                            .Returns(_statAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
+                            .Returns(_chunkSize);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndLStat(_statAsyncResult))
+                            .Returns(_fileAttributes);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 6, _fileSize))
+                            .Returns(_sftpFileReaderMock.Object);
+        }
+
+        private void Arrange()
+        {
+            SetupData();
+            CreateMocks();
+            SetupMocks();
+
+            _serviceFactory = new ServiceFactory();
+        }
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        private void Act()
+        {
+            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
+        }
+
+        [TestMethod]
+        public void CreateSftpFileReaderShouldReturnCreatedInstance()
+        {
+            Assert.IsNotNull(_actual);
+            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
+        }
+    }
+}

+ 98 - 0
src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize.cs

@@ -0,0 +1,98 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Sftp;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize
+    {
+        private ServiceFactory _serviceFactory;
+        private Mock<ISftpSession> _sftpSessionMock;
+        private Mock<ISftpFileReader> _sftpFileReaderMock;
+        private uint _bufferSize;
+        private string _fileName;
+        private SftpOpenAsyncResult _openAsyncResult;
+        private byte[] _handle;
+        private SFtpStatAsyncResult _statAsyncResult;
+        private uint _chunkSize;
+        private SftpFileAttributes _fileAttributes;
+        private long _fileSize;
+        private ISftpFileReader _actual;
+
+        private void SetupData()
+        {
+            var random = new Random();
+
+            _bufferSize = (uint)random.Next(1, int.MaxValue);
+            _openAsyncResult = new SftpOpenAsyncResult(null, null);
+            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
+            _statAsyncResult = new SFtpStatAsyncResult(null, null);
+            _fileName = random.Next().ToString();
+            _chunkSize = (uint)random.Next(1000, int.MaxValue);
+            _fileSize = _chunkSize - random.Next(1, 10);
+            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
+        }
+
+        private void CreateMocks()
+        {
+            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
+            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
+        }
+
+        private void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
+                            .Returns(_openAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndOpen(_openAsyncResult))
+                            .Returns(_handle);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginLStat(_fileName, null, null))
+                            .Returns(_statAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
+                            .Returns(_chunkSize);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndLStat(_statAsyncResult))
+                            .Returns(_fileAttributes);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 2, _fileSize))
+                            .Returns(_sftpFileReaderMock.Object);
+        }
+
+        private void Arrange()
+        {
+            SetupData();
+            CreateMocks();
+            SetupMocks();
+
+            _serviceFactory = new ServiceFactory();
+        }
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        private void Act()
+        {
+            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
+        }
+
+        [TestMethod]
+        public void CreateSftpFileReaderShouldReturnCreatedInstance()
+        {
+            Assert.IsNotNull(_actual);
+            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
+        }
+    }
+}

+ 98 - 0
src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize.cs

@@ -0,0 +1,98 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Sftp;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize
+    {
+        private ServiceFactory _serviceFactory;
+        private Mock<ISftpSession> _sftpSessionMock;
+        private Mock<ISftpFileReader> _sftpFileReaderMock;
+        private uint _bufferSize;
+        private string _fileName;
+        private SftpOpenAsyncResult _openAsyncResult;
+        private byte[] _handle;
+        private SFtpStatAsyncResult _statAsyncResult;
+        private uint _chunkSize;
+        private SftpFileAttributes _fileAttributes;
+        private long _fileSize;
+        private ISftpFileReader _actual;
+
+        private void SetupData()
+        {
+            var random = new Random();
+
+            _bufferSize = (uint)random.Next(1, int.MaxValue);
+            _openAsyncResult = new SftpOpenAsyncResult(null, null);
+            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
+            _statAsyncResult = new SFtpStatAsyncResult(null, null);
+            _fileName = random.Next().ToString();
+            _chunkSize = (uint)random.Next(1000, 5000);
+            _fileSize = (_chunkSize * 5) + 10;
+            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
+        }
+
+        private void CreateMocks()
+        {
+            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
+            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
+        }
+
+        private void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
+                            .Returns(_openAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndOpen(_openAsyncResult))
+                            .Returns(_handle);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginLStat(_fileName, null, null))
+                            .Returns(_statAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
+                            .Returns(_chunkSize);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndLStat(_statAsyncResult))
+                            .Returns(_fileAttributes);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 7, _fileSize))
+                            .Returns(_sftpFileReaderMock.Object);
+        }
+
+        private void Arrange()
+        {
+            SetupData();
+            CreateMocks();
+            SetupMocks();
+
+            _serviceFactory = new ServiceFactory();
+        }
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        private void Act()
+        {
+            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
+        }
+
+        [TestMethod]
+        public void CreateSftpFileReaderShouldReturnCreatedInstance()
+        {
+            Assert.IsNotNull(_actual);
+            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
+        }
+    }
+}

+ 98 - 0
src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanTenTimesGreaterThanChunkSize.cs

@@ -0,0 +1,98 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Sftp;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanTenTimesGreaterThanChunkSize
+    {
+        private ServiceFactory _serviceFactory;
+        private Mock<ISftpSession> _sftpSessionMock;
+        private Mock<ISftpFileReader> _sftpFileReaderMock;
+        private uint _bufferSize;
+        private string _fileName;
+        private SftpOpenAsyncResult _openAsyncResult;
+        private byte[] _handle;
+        private SFtpStatAsyncResult _statAsyncResult;
+        private uint _chunkSize;
+        private SftpFileAttributes _fileAttributes;
+        private long _fileSize;
+        private ISftpFileReader _actual;
+
+        private void SetupData()
+        {
+            var random = new Random();
+
+            _bufferSize = (uint)random.Next(1, int.MaxValue);
+            _openAsyncResult = new SftpOpenAsyncResult(null, null);
+            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
+            _statAsyncResult = new SFtpStatAsyncResult(null, null);
+            _fileName = random.Next().ToString();
+            _chunkSize = (uint) random.Next(1000, 5000);
+            _fileSize = _chunkSize * random.Next(11, 50);
+            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
+        }
+
+        private void CreateMocks()
+        {
+            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
+            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
+        }
+
+        private void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
+                            .Returns(_openAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndOpen(_openAsyncResult))
+                            .Returns(_handle);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginLStat(_fileName, null, null))
+                            .Returns(_statAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
+                            .Returns(_chunkSize);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndLStat(_statAsyncResult))
+                            .Returns(_fileAttributes);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 10, _fileSize))
+                            .Returns(_sftpFileReaderMock.Object);
+        }
+
+        private void Arrange()
+        {
+            SetupData();
+            CreateMocks();
+            SetupMocks();
+
+            _serviceFactory = new ServiceFactory();
+        }
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        private void Act()
+        {
+            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
+        }
+
+        [TestMethod]
+        public void CreateSftpFileReaderShouldReturnCreatedInstance()
+        {
+            Assert.IsNotNull(_actual);
+            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
+        }
+    }
+}

+ 98 - 0
src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero.cs

@@ -0,0 +1,98 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Sftp;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero
+    {
+        private ServiceFactory _serviceFactory;
+        private Mock<ISftpSession> _sftpSessionMock;
+        private Mock<ISftpFileReader> _sftpFileReaderMock;
+        private uint _bufferSize;
+        private string _fileName;
+        private SftpOpenAsyncResult _openAsyncResult;
+        private byte[] _handle;
+        private SFtpStatAsyncResult _statAsyncResult;
+        private uint _chunkSize;
+        private long _fileSize;
+        private SftpFileAttributes _fileAttributes;
+        private ISftpFileReader _actual;
+
+        private void SetupData()
+        {
+            var random = new Random();
+
+            _bufferSize = (uint) random.Next(1, int.MaxValue);
+            _openAsyncResult = new SftpOpenAsyncResult(null, null);
+            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
+            _statAsyncResult = new SFtpStatAsyncResult(null, null);
+            _fileName = random.Next().ToString();
+            _chunkSize = (uint) random.Next(1, int.MaxValue);
+            _fileSize = 0L;
+            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
+        }
+
+        private void CreateMocks()
+        {
+            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
+            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
+        }
+
+        private void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
+                            .Returns(_openAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndOpen(_openAsyncResult))
+                            .Returns(_handle);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.BeginLStat(_fileName, null, null))
+                            .Returns(_statAsyncResult);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
+                            .Returns(_chunkSize);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.EndLStat(_statAsyncResult))
+                            .Returns(_fileAttributes);
+            _sftpSessionMock.InSequence(seq)
+                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 1, _fileSize))
+                            .Returns(_sftpFileReaderMock.Object);
+        }
+
+        private void Arrange()
+        {
+            SetupData();
+            CreateMocks();
+            SetupMocks();
+
+            _serviceFactory = new ServiceFactory();
+        }
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        private void Act()
+        {
+            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
+        }
+
+        [TestMethod]
+        public void CreateSftpFileReaderShouldReturnCreatedInstance()
+        {
+            Assert.IsNotNull(_actual);
+            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
+        }
+    }
+}

+ 31 - 9
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead.cs

@@ -25,6 +25,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private SftpCloseAsyncResult _closeAsyncResult;
         private SftpFileReader _reader;
         private ObjectDisposedException _actualException;
+        private AsyncCallback _readAsyncCallback;
         private EventWaitHandle _disposeCompleted;
 
         [TestCleanup]
@@ -46,6 +47,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             _operationTimeout = random.Next(10000, 20000);
             _closeAsyncResult = new SftpCloseAsyncResult(null, null);
             _disposeCompleted = new ManualResetEvent(false);
+            _readAsyncCallback = null;
         }
 
         protected override void SetupMocks()
@@ -55,24 +57,36 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             SftpSessionMock.InSequence(_seq)
                            .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
                            .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                           {
-                               _waitHandleArray[0] = disposingWaitHandle;
-                               _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                               return _waitHandleArray;
-                           });
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
+                                {
+                                    _waitHandleArray[0] = disposingWaitHandle;
+                                    _waitHandleArray[1] = semaphoreAvailableWaitHandle;
+                                    return _waitHandleArray;
+                                });
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.OperationTimeout)
+                           .Returns(_operationTimeout);
             SftpSessionMock.InSequence(_seq)
                            .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
                            .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
             SftpSessionMock.InSequence(_seq)
                            .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                           .Returns((SftpReadAsyncResult)null);
+                           .Returns<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
+                                {
+                                    _readAsyncCallback = callback;
+                                    return null;
+                                });
             SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
             SftpSessionMock.InSequence(_seq)
                            .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
                            .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.IsOpen)
+                           .Returns(true);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.BeginClose(_handle, null, null))
+                           .Returns(_closeAsyncResult);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.EndClose(_closeAsyncResult));
         }
 
         protected override void Arrange()
@@ -147,5 +161,13 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
             SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
         }
+
+        [TestMethod]
+        public void InvokeOfReadAheadCallbackShouldCompleteImmediately()
+        {
+            Assert.IsNotNull(_readAsyncCallback);
+
+            _readAsyncCallback(new SftpReadAsyncResult(null, null));
+        }
     }
 }

+ 138 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsNotOpen.cs

@@ -0,0 +1,138 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+#if !FEATURE_EVENTWAITHANDLE_DISPOSE
+using Renci.SshNet.Common;
+#endif // !FEATURE_EVENTWAITHANDLE_DISPOSE
+using Renci.SshNet.Sftp;
+using System;
+using System.Threading;
+using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    public class SftpFileReaderTest_Dispose_SftpSessionIsNotOpen : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private MockSequence _seq;
+        private byte[] _handle;
+        private int _fileSize;
+        private WaitHandle[] _waitHandleArray;
+        private int _operationTimeout;
+        private SftpFileReader _reader;
+        private AsyncCallback _readAsyncCallback;
+        private ManualResetEvent _beginReadInvoked;
+        private EventWaitHandle _disposeCompleted;
+
+        [TestCleanup]
+        public void TearDown()
+        {
+            if (_beginReadInvoked != null)
+            {
+                _beginReadInvoked.Dispose();
+            }
+
+            if (_disposeCompleted != null)
+            {
+                _disposeCompleted.Dispose();
+            }
+        }
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 5);
+            _fileSize = 5000;
+            _waitHandleArray = new WaitHandle[2];
+            _operationTimeout = random.Next(10000, 20000);
+            _beginReadInvoked = new ManualResetEvent(false);
+            _disposeCompleted = new ManualResetEvent(false);
+            _readAsyncCallback = null;
+        }
+
+        protected override void SetupMocks()
+        {
+            _seq = new MockSequence();
+
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
+                           .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
+                                {
+                                    _waitHandleArray[0] = disposingWaitHandle;
+                                    _waitHandleArray[1] = semaphoreAvailableWaitHandle;
+                                    return _waitHandleArray;
+                                });
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.OperationTimeout)
+                           .Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
+                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
+                           .Callback(() =>
+                                {
+                                    // harden test by making sure that we've invoked BeginRead before Dispose is invoked
+                                    _beginReadInvoked.Set();
+                                })
+                           .Returns<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
+                                {
+                                    _readAsyncCallback = callback;
+                                    return null;
+                                })
+                           .Callback(() =>
+                                {
+                                    // wait until Dispose has been invoked on reader to allow us to harden test, and
+                                    // verify whether Dispose will prevent us from entering the read-ahead loop again
+                                    _waitHandleArray[0].WaitOne();
+                                });
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.IsOpen)
+                           .Returns(false);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 1, _fileSize);
+        }
+
+        protected override void Act()
+        {
+            Assert.IsTrue(_beginReadInvoked.WaitOne(5000));
+            _reader.Dispose();
+        }
+
+        [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 InvokeOfReadAheadCallbackShouldCompleteImmediately()
+        {
+            Assert.IsNotNull(_readAsyncCallback);
+
+            _readAsyncCallback(new SftpReadAsyncResult(null, null));
+        }
+
+        [TestMethod]
+        public void BeginCloseOnSftpSessionShouldNeverHaveBeenInvoked()
+        {
+            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Never);
+        }
+    }
+}

+ 141 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsOpen_BeginCloseThrowsException.cs

@@ -0,0 +1,141 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+#if !FEATURE_EVENTWAITHANDLE_DISPOSE
+using Renci.SshNet.Common;
+#endif // !FEATURE_EVENTWAITHANDLE_DISPOSE
+using Renci.SshNet.Sftp;
+using System;
+using System.Threading;
+using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    public class SftpFileReaderTest_Dispose_SftpSessionIsOpen_BeginCloseThrowsException : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private MockSequence _seq;
+        private byte[] _handle;
+        private int _fileSize;
+        private WaitHandle[] _waitHandleArray;
+        private int _operationTimeout;
+        private SftpFileReader _reader;
+        private AsyncCallback _readAsyncCallback;
+        private ManualResetEvent _beginReadInvoked;
+        private EventWaitHandle _disposeCompleted;
+
+        [TestCleanup]
+        public void TearDown()
+        {
+            if (_beginReadInvoked != null)
+            {
+                _beginReadInvoked.Dispose();
+            }
+
+            if (_disposeCompleted != null)
+            {
+                _disposeCompleted.Dispose();
+            }
+        }
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 5);
+            _fileSize = 5000;
+            _waitHandleArray = new WaitHandle[2];
+            _operationTimeout = random.Next(10000, 20000);
+            _beginReadInvoked = new ManualResetEvent(false);
+            _disposeCompleted = new ManualResetEvent(false);
+            _readAsyncCallback = null;
+        }
+
+        protected override void SetupMocks()
+        {
+            _seq = new MockSequence();
+
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
+                           .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
+                                {
+                                    _waitHandleArray[0] = disposingWaitHandle;
+                                    _waitHandleArray[1] = semaphoreAvailableWaitHandle;
+                                    return _waitHandleArray;
+                                });
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.OperationTimeout)
+                           .Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
+                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
+                           .Callback(() =>
+                                {
+                                    // harden test by making sure that we've invoked BeginRead before Dispose is invoked
+                                    _beginReadInvoked.Set();
+                                })
+                           .Returns<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
+                                {
+                                    _readAsyncCallback = callback;
+                                    return null;
+                                })
+                            .Callback(() =>
+                                {
+                                    // wait until Dispose has been invoked on reader to allow us to harden test, and
+                                    // verify whether Dispose will prevent us from entering the read-ahead loop again
+                                    _waitHandleArray[0].WaitOne();
+                                });
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.IsOpen)
+                           .Returns(true);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.BeginClose(_handle, null, null))
+                           .Throws(new SshException());
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 1, _fileSize);
+        }
+
+        protected override void Act()
+        {
+            Assert.IsTrue(_beginReadInvoked.WaitOne(5000));
+            _reader.Dispose();
+        }
+
+        [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 InvokeOfReadAheadCallbackShouldCompleteImmediately()
+        {
+            Assert.IsNotNull(_readAsyncCallback);
+
+            _readAsyncCallback(new SftpReadAsyncResult(null, null));
+        }
+
+        [TestMethod]
+        public void BeginCloseOnSftpSessionShouldHaveBeenInvokedOnce()
+        {
+            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
+        }
+    }
+}

+ 146 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsOpen_EndCloseThrowsException.cs

@@ -0,0 +1,146 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+#if !FEATURE_EVENTWAITHANDLE_DISPOSE
+using Renci.SshNet.Common;
+#endif // !FEATURE_EVENTWAITHANDLE_DISPOSE
+using Renci.SshNet.Sftp;
+using System;
+using System.Threading;
+using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    public class SftpFileReaderTest_Dispose_SftpSessionIsOpen_EndCloseThrowsException : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private MockSequence _seq;
+        private byte[] _handle;
+        private int _fileSize;
+        private WaitHandle[] _waitHandleArray;
+        private int _operationTimeout;
+        private SftpCloseAsyncResult _closeAsyncResult;
+        private SftpFileReader _reader;
+        private AsyncCallback _readAsyncCallback;
+        private ManualResetEvent _beginReadInvoked;
+        private EventWaitHandle _disposeCompleted;
+
+        [TestCleanup]
+        public void TearDown()
+        {
+            if (_beginReadInvoked != null)
+            {
+                _beginReadInvoked.Dispose();
+            }
+
+            if (_disposeCompleted != null)
+            {
+                _disposeCompleted.Dispose();
+            }
+        }
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 5);
+            _fileSize = 5000;
+            _waitHandleArray = new WaitHandle[2];
+            _operationTimeout = random.Next(10000, 20000);
+            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
+            _beginReadInvoked = new ManualResetEvent(false);
+            _disposeCompleted = new ManualResetEvent(false);
+            _readAsyncCallback = null;
+        }
+
+        protected override void SetupMocks()
+        {
+            _seq = new MockSequence();
+
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
+                           .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
+                                {
+                                    _waitHandleArray[0] = disposingWaitHandle;
+                                    _waitHandleArray[1] = semaphoreAvailableWaitHandle;
+                                    return _waitHandleArray;
+                                });
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.OperationTimeout)
+                           .Returns(_operationTimeout);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
+                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
+                           .Callback(() =>
+                                {
+                                    // harden test by making sure that we've invoked BeginRead before Dispose is invoked
+                                    _beginReadInvoked.Set();
+                                })
+                           .Returns<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
+                                {
+                                    _readAsyncCallback = callback;
+                                    return null;
+                                })
+                           .Callback(() =>
+                                {
+                                    // wait until Dispose has been invoked on reader to allow us to harden test, and
+                                    // verify whether Dispose will prevent us from entering the read-ahead loop again
+                                    _waitHandleArray[0].WaitOne();
+                                });
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.IsOpen)
+                           .Returns(true);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.BeginClose(_handle, null, null))
+                           .Returns(_closeAsyncResult);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.EndClose(_closeAsyncResult))
+                           .Throws(new SshException());
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 1, _fileSize);
+        }
+
+        protected override void Act()
+        {
+            Assert.IsTrue(_beginReadInvoked.WaitOne(5000));
+            _reader.Dispose();
+        }
+
+        [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 InvokeOfReadAheadCallbackShouldCompleteImmediately()
+        {
+            Assert.IsNotNull(_readAsyncCallback);
+
+            _readAsyncCallback(new SftpReadAsyncResult(null, null));
+        }
+
+        [TestMethod]
+        public void EndCloseOnSftpSessionShouldHaveBeenInvokedOnce()
+        {
+            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
+        }
+    }
+}

+ 10 - 8
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_LastChunkBeforeEofIsComplete.cs

@@ -19,6 +19,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private int _fileSize;
         private WaitHandle[] _waitHandleArray;
         private int _operationTimeout;
+        private SftpReadAsyncResult _readAsyncResultBeyondEof;
         private SftpCloseAsyncResult _closeAsyncResult;
         private byte[] _chunk1;
         private byte[] _chunk2;
@@ -41,6 +42,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             _waitHandleArray = new WaitHandle[2];
             _operationTimeout = random.Next(10000, 20000);
             _closeAsyncResult = new SftpCloseAsyncResult(null, null);
+            _readAsyncResultBeyondEof = new SftpReadAsyncResult(null, null);
         }
 
         protected override void SetupMocks()
@@ -78,19 +80,17 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                                 var asyncResult = new SftpReadAsyncResult(callback, state);
                                 asyncResult.SetAsCompleted(_chunk2, false);
                             })
-                            .Returns((SftpReadAsyncResult)null);
+                            .Returns((SftpReadAsyncResult) null);
             SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
             SftpSessionMock.InSequence(_seq)
                            .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
                            .Returns(() => WaitAny(_waitHandleArray, _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) =>
-                            {
-                                var asyncResult = new SftpReadAsyncResult(callback, state);
-                                asyncResult.SetAsCompleted(_chunk3, false);
-                            })
-                            .Returns((SftpReadAsyncResult)null);
+                           .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, null, It.IsAny<BufferedRead>()))
+                           .Returns(_readAsyncResultBeyondEof);
+            SftpSessionMock.InSequence(_seq)
+                           .Setup(p => p.EndRead(_readAsyncResultBeyondEof))
+                           .Returns(_chunk3);
         }
 
         protected override void Arrange()
@@ -146,6 +146,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         [TestMethod]
         public void DisposeShouldCloseHandleAndCompleteImmediately()
         {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
             SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
             SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
 
@@ -155,6 +156,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
 
             Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
 
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
             SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
             SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
         }

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

@@ -145,6 +145,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         [TestMethod]
         public void DisposeShouldCloseHandleAndCompleteImmediately()
         {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
             SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
             SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
 
@@ -154,6 +155,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
 
             Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
 
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
             SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
             SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
         }

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

@@ -284,6 +284,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         [TestMethod]
         public void DisposeShouldCloseHandleAndCompleteImmediately()
         {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
             SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
             SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
 
@@ -293,6 +294,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
 
             Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
 
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
             SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
             SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
         }

+ 3 - 1
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsReached.cs

@@ -111,7 +111,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         {
             base.Arrange();
 
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 3, _fileSize);
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 5, _fileSize);
         }
 
         protected override void Act()
@@ -174,6 +174,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         [TestMethod]
         public void DisposeShouldCloseHandleAndCompleteImmediately()
         {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
             SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
             SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
 
@@ -183,6 +184,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
 
             Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
 
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
             SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
             SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
         }

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

@@ -191,6 +191,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         [TestMethod]
         public void DisposeShouldCloseHandleAndCompleteImmediately()
         {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
             SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
             SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
 
@@ -200,6 +201,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
 
             Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
 
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
             SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
             SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
         }

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

@@ -152,6 +152,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         [TestMethod]
         public void DisposeShouldCloseHandleAndCompleteImmediately()
         {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
             SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
             SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
 
@@ -161,6 +162,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
 
             Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
 
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
             SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
             SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
         }

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

@@ -122,6 +122,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         [TestMethod]
         public void DisposeShouldCloseHandleAndCompleteImmediately()
         {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
             SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
             SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
 
@@ -131,6 +132,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
 
             Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
 
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
             SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
             SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
         }

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

@@ -106,6 +106,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         [TestMethod]
         public void DisposeShouldCloseHandleAndCompleteImmediately()
         {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
             SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
             SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
 
@@ -115,6 +116,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
 
             Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
 
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
             SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
             SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
         }

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

@@ -150,6 +150,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         [TestMethod]
         public void DisposeShouldCloseHandleAndCompleteImmediately()
         {
+            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
             SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
             SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
 
@@ -159,6 +160,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
 
             Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
 
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
             SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
             SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
         }

+ 7 - 7
src/Renci.SshNet.Tests/Common/SftpFileAttributesBuilder.cs

@@ -12,7 +12,7 @@ namespace Renci.SshNet.Tests.Common
         private int? _userId;
         private int? _groupId;
         private uint? _permissions;
-        private IDictionary<string, string> _extensions;
+        private readonly IDictionary<string, string> _extensions;
 
         public SftpFileAttributesBuilder()
         {
@@ -64,17 +64,17 @@ namespace Renci.SshNet.Tests.Common
         public SftpFileAttributes Build()
         {
             if (_lastAccessTime == null)
-                throw new ArgumentException();
+                _lastAccessTime = DateTime.MinValue;
             if (_lastWriteTime == null)
-                throw new ArgumentException();
+                _lastWriteTime = DateTime.MinValue;
             if (_size == null)
-                throw new ArgumentException();
+                _size = 0;
             if (_userId == null)
-                throw new ArgumentException();
+                _userId = 0;
             if (_groupId == null)
-                throw new ArgumentException();
+                _groupId = 0;
             if (_permissions == null)
-                throw new ArgumentException();
+                _permissions = 0;
 
             return new SftpFileAttributes(_lastAccessTime.Value,
                                           _lastWriteTime.Value,

+ 11 - 0
src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj

@@ -262,6 +262,14 @@
     <Compile Include="Classes\Common\PortForwardEventArgsTest.cs" />
     <Compile Include="Classes\Compression\ZlibTest.cs" />
     <Compile Include="Classes\ConnectionInfoTest.cs" />
+    <Compile Include="Classes\ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs" />
+    <Compile Include="Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs" />
+    <Compile Include="Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs" />
+    <Compile Include="Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize.cs" />
+    <Compile Include="Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize.cs" />
+    <Compile Include="Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize.cs" />
+    <Compile Include="Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanTenTimesGreaterThanChunkSize.cs" />
+    <Compile Include="Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero.cs" />
     <Compile Include="Classes\SessionTest_Connected.cs" />
     <Compile Include="Classes\SessionTest_ConnectedBase.cs" />
     <Compile Include="Classes\SessionTest_Connected_ConnectionReset.cs" />
@@ -357,6 +365,9 @@
     <Compile Include="Classes\Sftp\SftpDataResponseBuilder.cs" />
     <Compile Include="Classes\Sftp\SftpFileReaderTestBase.cs" />
     <Compile Include="Classes\Sftp\SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_Dispose_SftpSessionIsNotOpen.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_Dispose_SftpSessionIsOpen_BeginCloseThrowsException.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_Dispose_SftpSessionIsOpen_EndCloseThrowsException.cs" />
     <Compile Include="Classes\Sftp\SftpFileReaderTest_LastChunkBeforeEofIsComplete.cs" />
     <Compile Include="Classes\Sftp\SftpFileReaderTest_LastChunkBeforeEofIsPartial.cs" />
     <Compile Include="Classes\Sftp\SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsNotReached.cs" />

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

@@ -103,7 +103,11 @@ namespace Renci.SshNet
         {
             const int defaultMaxPendingReads = 3;
 
+            // Issue #292: Avoid overlapping SSH_FXP_OPEN and SSH_FXP_LSTAT requests for the same file as this
+            // causes a performance degradation on Sun SSH
             var openAsyncResult = sftpSession.BeginOpen(fileName, Flags.Read, null, null);
+            var handle = sftpSession.EndOpen(openAsyncResult);
+
             var statAsyncResult = sftpSession.BeginLStat(fileName, null, null);
 
             long? fileSize;
@@ -127,8 +131,6 @@ namespace Renci.SshNet
                 DiagnosticAbstraction.Log(string.Format("Failed to obtain size of file. Allowing maximum {0} pending reads: {1}", maxPendingReads, ex));
             }
 
-            var handle = sftpSession.EndOpen(openAsyncResult);
-
             return sftpSession.CreateFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
         }
 

+ 63 - 20
src/Renci.SshNet/Sftp/SftpFileReader.cs

@@ -26,7 +26,6 @@ namespace Renci.SshNet.Sftp
         private int _readAheadChunkIndex;
         private ulong _readAheadOffset;
         private readonly ManualResetEvent _readAheadCompleted;
-        private ManualResetEvent _disposingWaitHandle;
         private int _nextChunkIndex;
 
         /// <summary>
@@ -40,6 +39,9 @@ namespace Renci.SshNet.Sftp
         private readonly SemaphoreLight _semaphore;
         private readonly object _readLock;
 
+        private readonly ManualResetEvent _disposingWaitHandle;
+        private bool _disposingOrDisposed;
+
         private Exception _exception;
 
         /// <summary>
@@ -69,7 +71,7 @@ namespace Renci.SshNet.Sftp
 
         public byte[] Read()
         {
-            if (_disposingWaitHandle == null)
+            if (_disposingOrDisposed)
                 throw new ObjectDisposedException(GetType().FullName);
             if (_exception != null)
                 throw _exception;
@@ -80,12 +82,14 @@ namespace Renci.SshNet.Sftp
 
             lock (_readLock)
             {
-                // wait until either the next chunk is avalable or an exception has occurred
+                // wait until either the next chunk is avalable, an exception has occurred or the current
+                // instance is already disposed
                 while (!_queue.TryGetValue(_nextChunkIndex, out nextChunk) && _exception == null)
                 {
                     Monitor.Wait(_readLock);
                 }
 
+                // throw when exception occured in read-ahead, or the current instance is already disposed
                 if (_exception != null)
                     throw _exception;
 
@@ -96,7 +100,7 @@ namespace Renci.SshNet.Sftp
                     // have we reached EOF?
                     if (data.Length == 0)
                     {
-                        // PERF: we do not bother updating internal state when we've EOF
+                        // PERF: we do not bother updating all of the internal state when we've reached EOF
 
                         _isEndOfFileRead = true;
                     }
@@ -159,7 +163,7 @@ namespace Renci.SshNet.Sftp
                     {
                         _isEndOfFileRead = true;
                         // ensure we've not yet disposed the current instance
-                        if (_semaphore != null)
+                        if (!_disposingOrDisposed)
                         {
                             // unblock wait in read-ahead
                             _semaphore.Release();
@@ -171,7 +175,7 @@ namespace Renci.SshNet.Sftp
                     // move reader to error state
                     _exception = new SshException("Unexpectedly reached end of file.");
                     // ensure we've not yet disposed the current instance
-                    if (_semaphore != null)
+                    if (!_disposingOrDisposed)
                     {
                         // unblock wait in read-ahead
                         _semaphore.Release();
@@ -203,9 +207,12 @@ namespace Renci.SshNet.Sftp
         /// <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 (_disposingWaitHandle == null)
+            if (_disposingOrDisposed)
                 return;
 
+            // transition to disposing state
+            _disposingOrDisposed = true;
+
             if (disposing)
             {
                 // record exception to break prevent future Read()
@@ -227,15 +234,21 @@ namespace Renci.SshNet.Sftp
                     Monitor.PulseAll(_readLock);
                 }
 
-                var closeAsyncResult = _sftpSession.BeginClose(_handle, null, null);
-
                 _readAheadCompleted.Dispose();
                 _disposingWaitHandle.Dispose();
 
-                _sftpSession.EndClose(closeAsyncResult);
-
-                // dereference the disposing waithandle to indicate that the current instance is disposed
-                _disposingWaitHandle = null;
+                if (_sftpSession.IsOpen)
+                {
+                    try
+                    {
+                        var closeAsyncResult = _sftpSession.BeginClose(_handle, null, null);
+                        _sftpSession.EndClose(closeAsyncResult);
+                    }
+                    catch (Exception ex)
+                    {
+                        DiagnosticAbstraction.Log("Failure closing handle: " + ex);
+                    }
+                }
             }
         }
 
@@ -269,16 +282,34 @@ namespace Renci.SshNet.Sftp
                         continue;
                     }
 
-                    // don't bother reading any more chunks if we received EOF, or an exception has occurred
-                    // while processing a chunk
+                    // don't bother reading any more chunks if we received EOF, an exception has occurred
+                    // or the current instance is disposed
                     if (_endOfFileReceived || _exception != null)
                         break;
 
                     // start reading next chunk
+                    var bufferedRead = new BufferedRead(_readAheadChunkIndex, _readAheadOffset);
+
                     try
                     {
-                        _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkSize, ReadCompleted,
-                                               new BufferedRead(_readAheadChunkIndex, _readAheadOffset));
+                        // even if we know the size of the file and have read up to EOF, we still want
+                        // to keep reading (ahead) until we receive zero bytes from the remote host as
+                        // we do not want to rely purely on the reported file size
+                        //
+                        // if the offset of the read-ahead chunk is greater than that file size, then
+                        // we can expect to be reading the last (zero-byte) chunk and switch to synchronous
+                        // mode to avoid having multiple read-aheads that read beyond EOF
+                        if (_fileSize != null && (long) _readAheadOffset > _fileSize.Value)
+                        {
+                            var asyncResult = _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkSize, null,
+                                                                     bufferedRead);
+                            var data = _sftpSession.EndRead(asyncResult);
+                            ReadCompletedCore(bufferedRead, data);
+                        }
+                        else
+                        {
+                            _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkSize, ReadCompleted, bufferedRead);
+                        }
                     }
                     catch (Exception ex)
                     {
@@ -288,7 +319,7 @@ namespace Renci.SshNet.Sftp
 
                     // advance read-ahead offset
                     _readAheadOffset += _chunkSize;
-
+                    // increment index of read-ahead chunk
                     _readAheadChunkIndex++;
                 }
 
@@ -319,13 +350,20 @@ namespace Renci.SshNet.Sftp
             }
             catch (Exception ex)
             {
-                _exception = ex;
+                Interlocked.CompareExchange(ref _exception, ex, null);
                 return false;
             }
         }
 
         private void ReadCompleted(IAsyncResult result)
         {
+            if (_disposingOrDisposed)
+            {
+                // skip further processing if we're disposing the current instance
+                // to avoid accessing disposed members
+                return;
+            }
+
             var readAsyncResult = (SftpReadAsyncResult) result;
 
             byte[] data;
@@ -343,6 +381,11 @@ namespace Renci.SshNet.Sftp
             // a read that completes with a zero-byte result signals EOF
             // but there may be pending reads before that read
             var bufferedRead = (BufferedRead) readAsyncResult.AsyncState;
+            ReadCompletedCore(bufferedRead, data);
+        }
+
+        private void ReadCompletedCore(BufferedRead bufferedRead, byte[] data)
+        {
             bufferedRead.Complete(data);
 
             lock (_readLock)
@@ -364,7 +407,7 @@ namespace Renci.SshNet.Sftp
 
         private void HandleFailure(Exception cause)
         {
-            _exception = cause;
+            Interlocked.CompareExchange(ref _exception, cause, null);
 
             // unblock read-ahead
             _semaphore.Release();