Преглед изворни кода

Improve exception handler for SftpFileReader.
Add more tests (WIP).

Gert Driesen пре 8 година
родитељ
комит
e937c9d841

+ 49 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTestBase.cs

@@ -0,0 +1,49 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Sftp;
+using System;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    public abstract class SftpFileReaderTestBase
+    {
+        internal Mock<ISftpSession> SftpSessionMock {  get; private set;}
+
+        protected abstract void SetupData();
+
+        protected void CreateMocks()
+        {
+            SftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
+        }
+
+        protected abstract void SetupMocks();
+
+        protected virtual void Arrange()
+        {
+            SetupData();
+            CreateMocks();
+            SetupMocks();
+        }
+
+        [TestInitialize]
+        public void SetUp()
+        {
+            Arrange();
+            Act();
+        }
+
+        protected abstract void Act();
+
+        protected static SftpFileAttributes CreateSftpFileAttributes(long size)
+        {
+            return new SftpFileAttributes(default(DateTime), default(DateTime), size, default(int), default(int), default(uint), null);
+        }
+
+        protected static byte[] CreateByteArray(Random random, int length)
+        {
+            var chunk = new byte[length];
+            random.NextBytes(chunk);
+            return chunk;
+        }
+    }
+}

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

@@ -0,0 +1,129 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+using Renci.SshNet.Sftp;
+using System;
+using System.Diagnostics;
+using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    public class SftpFileReaderTest_LastChunkBeforeEofIsComplete : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private byte[] _handle;
+        private int _fileSize;
+        private byte[] _chunk1;
+        private byte[] _chunk2;
+        private byte[] _chunk3;
+        private SftpFileReader _reader;
+        private byte[] _actualChunk1;
+        private byte[] _actualChunk2;
+        private byte[] _actualChunk3;
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 5);
+            _chunk1 = CreateByteArray(random, ChunkLength);
+            // chunk is less than the requested length, but - together with chunk 1 - contains all data up to the EOF
+            _chunk2 = CreateByteArray(random, ChunkLength - 10);
+            _chunk3 = new byte[0];
+            _fileSize = _chunk1.Length + _chunk2.Length;
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SftpSessionMock.InSequence(seq).Setup(p => p.RequestFStat(_handle)).Returns(CreateSftpFileAttributes(_fileSize));
+            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.BeginRead(_handle, 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(_chunk2, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+            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);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, 15);
+        }
+
+        protected override void Act()
+        {
+            _actualChunk1 = _reader.Read();
+            _actualChunk2 = _reader.Read();
+            _actualChunk3 = _reader.Read();
+        }
+
+        [TestMethod]
+        public void FirstReadShouldReturnChunk1()
+        {
+            Assert.IsNotNull(_actualChunk1);
+            Assert.AreSame(_chunk1, _actualChunk1);
+        }
+
+        [TestMethod]
+        public void SecondReadShouldReturnChunk2()
+        {
+            Assert.IsNotNull(_actualChunk2);
+            Assert.AreSame(_chunk2, _actualChunk2);
+        }
+
+        [TestMethod]
+        public void ThirdReadShouldReturnChunk3()
+        {
+            Assert.IsNotNull(_actualChunk3);
+            Assert.AreSame(_chunk3, _actualChunk3);
+        }
+
+        [TestMethod]
+        public void ReadAfterEndOfFileShouldThrowSshException()
+        {
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (SshException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual("Attempting to read beyond the end of the file.", ex.Message);
+            }
+        }
+
+        [TestMethod]
+        public void DisposeShouldCompleteImmediately()
+        {
+            var stopwatch = Stopwatch.StartNew();
+            _reader.Dispose();
+            stopwatch.Stop();
+
+            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
+        }
+    }
+}

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

@@ -0,0 +1,129 @@
+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_LastChunkBeforeEofIsPartial : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private byte[] _handle;
+        private int _fileSize;
+        private byte[] _chunk1;
+        private byte[] _chunk2;
+        private byte[] _chunk3;
+        private SftpFileReader _reader;
+        private byte[] _actualChunk1;
+        private byte[] _actualChunk2;
+        private byte[] _actualChunk3;
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 5);
+            _chunk1 = CreateByteArray(random, ChunkLength);
+            _chunk2 = CreateByteArray(random, ChunkLength);
+            _chunk3 = new byte[0];
+            _fileSize = _chunk1.Length + _chunk2.Length;
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SftpSessionMock.InSequence(seq).Setup(p => p.RequestFStat(_handle)).Returns(CreateSftpFileAttributes(_chunk1.Length + _chunk2.Length));
+            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.BeginRead(_handle, 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(_chunk2, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+            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);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, 15);
+        }
+
+        protected override void Act()
+        {
+            _actualChunk1 = _reader.Read();
+            _actualChunk2 = _reader.Read();
+            _actualChunk3 = _reader.Read();
+        }
+
+        [TestMethod]
+        public void FirstReadShouldReturnChunk1()
+        {
+            Assert.IsNotNull(_actualChunk1);
+            Assert.AreSame(_chunk1, _actualChunk1);
+        }
+
+        [TestMethod]
+        public void SecondReadShouldReturnChunk2()
+        {
+            Assert.IsNotNull(_actualChunk2);
+            Assert.AreSame(_chunk2, _actualChunk2);
+        }
+
+        [TestMethod]
+        public void ThirdReadShouldReturnChunk3()
+        {
+            Assert.IsNotNull(_actualChunk3);
+            Assert.AreSame(_chunk3, _actualChunk3);
+        }
+
+        [TestMethod]
+        public void ReadAfterEndOfFileShouldThrowSshException()
+        {
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (SshException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual("Attempting to read beyond the end of the file.", ex.Message);
+            }
+        }
+
+        [TestMethod]
+        public void DisposeShouldCompleteImmediately()
+        {
+            var stopwatch = Stopwatch.StartNew();
+            _reader.Dispose();
+            stopwatch.Stop();
+
+            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
+        }
+    }
+}

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

@@ -0,0 +1,251 @@
+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_PreviousChunkIsIncompleteAndEofIsNotReached : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private byte[] _handle;
+        private int _fileSize;
+        private byte[] _chunk1;
+        private byte[] _chunk2;
+        private byte[] _chunk2CatchUp1;
+        private byte[] _chunk2CatchUp2;
+        private byte[] _chunk3;
+        private byte[] _chunk4;
+        private byte[] _chunk5;
+        private SftpFileReader _reader;
+        private byte[] _actualChunk1;
+        private byte[] _actualChunk2;
+        private byte[] _actualChunk3;
+        private ManualResetEvent _chunk1BeginRead;
+        private ManualResetEvent _chunk2BeginRead;
+        private ManualResetEvent _chunk3BeginRead;
+        private ManualResetEvent _chunk4BeginRead;
+        private ManualResetEvent _chunk5BeginRead;
+        private ManualResetEvent _chunk6BeginRead;
+        private byte[] _actualChunk4;
+        private byte[] _actualChunk2CatchUp1;
+        private byte[] _actualChunk2CatchUp2;
+        private byte[] _chunk6;
+        private byte[] _actualChunk5;
+        private byte[] _actualChunk6;
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 3);
+            _chunk1 = CreateByteArray(random, ChunkLength);
+            _chunk2 = CreateByteArray(random, ChunkLength - 17);
+            _chunk2CatchUp1 = CreateByteArray(random, 10);
+            _chunk2CatchUp2 = CreateByteArray(random, 7);
+            _chunk3 = CreateByteArray(random, ChunkLength);
+            _chunk4 = CreateByteArray(random, ChunkLength);
+            _chunk5 = CreateByteArray(random, ChunkLength);
+            _chunk6 = new byte[0];
+            _chunk1BeginRead = new ManualResetEvent(false);
+            _chunk2BeginRead = new ManualResetEvent(false);
+            _chunk3BeginRead = new ManualResetEvent(false);
+            _chunk4BeginRead = new ManualResetEvent(false);
+            _chunk5BeginRead = new ManualResetEvent(false);
+            _chunk6BeginRead = new ManualResetEvent(false);
+            _fileSize = _chunk1.Length + _chunk2.Length + _chunk2CatchUp1.Length + _chunk2CatchUp2.Length + _chunk3.Length + _chunk4.Length + _chunk5.Length;
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SftpSessionMock.InSequence(seq).Setup(p => p.RequestFStat(_handle)).Returns(CreateSftpFileAttributes(_fileSize));
+            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) =>
+                            {
+                                _chunk1BeginRead.Set();
+                                var asyncResult = new SftpReadAsyncResult(callback, state);
+                                asyncResult.SetAsCompleted(_chunk1, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+            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) =>
+                            {
+                                _chunk2BeginRead.Set();
+                                var asyncResult = new SftpReadAsyncResult(callback, state);
+                                asyncResult.SetAsCompleted(_chunk2, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+            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) =>
+                            {
+                                _chunk3BeginRead.Set();
+                                var asyncResult = new SftpReadAsyncResult(callback, state);
+                                asyncResult.SetAsCompleted(_chunk3, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+            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) =>
+                            {
+                                _chunk4BeginRead.Set();
+                                var asyncResult = new SftpReadAsyncResult(callback, state);
+                                asyncResult.SetAsCompleted(_chunk4, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+            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) =>
+                            {
+                                _chunk5BeginRead.Set();
+                                var asyncResult = new SftpReadAsyncResult(callback, state);
+                                asyncResult.SetAsCompleted(_chunk5, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(seq)
+                            .Setup(p => p.RequestRead(_handle, 2 * ChunkLength - 17, 17))
+                            .Returns(_chunk2CatchUp1);
+            SftpSessionMock.InSequence(seq)
+                            .Setup(p => p.RequestRead(_handle, 2 * ChunkLength - 7, 7))
+                            .Returns(_chunk2CatchUp2);
+            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) =>
+                            {
+                                _chunk6BeginRead.Set();
+                                var asyncResult = new SftpReadAsyncResult(callback, state);
+                                asyncResult.SetAsCompleted(_chunk6, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, 3);
+        }
+
+        protected override void Act()
+        {
+            // reader is configured to read-ahead max. 3 chunks, so chunk4 should not have been read
+            Assert.IsFalse(_chunk4BeginRead.WaitOne(0));
+            // consume chunk 1
+            _actualChunk1 = _reader.Read();
+            // consuming chunk1 allows chunk4 to be read-ahead
+            Assert.IsTrue(_chunk4BeginRead.WaitOne(200));
+            // verify that chunk5 has not yet been read-ahead
+            Assert.IsFalse(_chunk5BeginRead.WaitOne(0));
+            // consume chunk 2
+            _actualChunk2 = _reader.Read();
+            // consuming chunk2 allows chunk5 to be read-ahead
+            Assert.IsTrue(_chunk5BeginRead.WaitOne(200));
+            // consume remaining parts of chunk 2
+            _actualChunk2CatchUp1 = _reader.Read();
+            _actualChunk2CatchUp2 = _reader.Read();
+            // verify that chunk6 has not yet been read-ahead
+            Assert.IsFalse(_chunk6BeginRead.WaitOne(0));
+            // consume chunk 3
+            _actualChunk3 = _reader.Read();
+            // consuming chunk3 allows chunk6 to be read-ahead
+            Assert.IsTrue(_chunk6BeginRead.WaitOne(200));
+            // consume chunk 4
+            _actualChunk4 = _reader.Read();
+            // consume chunk 5
+            _actualChunk5 = _reader.Read();
+            // consume chunk 6
+            _actualChunk6 = _reader.Read();
+        }
+
+        [TestMethod]
+        public void FirstReadShouldReturnChunk1()
+        {
+            Assert.IsNotNull(_actualChunk1);
+            Assert.AreSame(_chunk1, _actualChunk1);
+        }
+
+        [TestMethod]
+        public void SecondReadShouldReturnChunk2()
+        {
+            Assert.IsNotNull(_actualChunk2);
+            Assert.AreSame(_chunk2, _actualChunk2);
+        }
+
+        [TestMethod]
+        public void ThirdReadShouldReturnChunk2CatchUp1()
+        {
+            Assert.IsNotNull(_actualChunk2CatchUp1);
+            Assert.AreSame(_chunk2CatchUp1, _actualChunk2CatchUp1);
+        }
+
+        [TestMethod]
+        public void FourthReadShouldReturnChunk2CatchUp2()
+        {
+            Assert.IsNotNull(_actualChunk2CatchUp2);
+            Assert.AreSame(_chunk2CatchUp2, _actualChunk2CatchUp2);
+        }
+
+        [TestMethod]
+        public void FifthReadShouldReturnChunk3()
+        {
+            Assert.IsNotNull(_actualChunk3);
+            Assert.AreSame(_chunk3, _actualChunk3);
+        }
+
+        [TestMethod]
+        public void SixthReadShouldReturnChunk4()
+        {
+            Assert.IsNotNull(_actualChunk4);
+            Assert.AreSame(_chunk4, _actualChunk4);
+        }
+
+        [TestMethod]
+        public void SeventhReadShouldReturnChunk5()
+        {
+            Assert.IsNotNull(_actualChunk5);
+            Assert.AreSame(_chunk5, _actualChunk5);
+        }
+
+        [TestMethod]
+        public void EightReadShouldReturnChunk6()
+        {
+            Assert.IsNotNull(_actualChunk6);
+            Assert.AreSame(_chunk6, _actualChunk6);
+        }
+
+        [TestMethod]
+        public void ReadAfterEndOfFileShouldThrowSshException()
+        {
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (SshException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual("Attempting to read beyond the end of the file.", ex.Message);
+            }
+        }
+
+        [TestMethod]
+        public void DisposeShouldCompleteImmediately()
+        {
+            var stopwatch = Stopwatch.StartNew();
+            _reader.Dispose();
+            stopwatch.Stop();
+
+            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
+        }
+    }
+}

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

@@ -0,0 +1,158 @@
+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_PreviousChunkIsIncompleteAndEofIsReached : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private byte[] _handle;
+        private int _fileSize;
+        private byte[] _chunk1;
+        private byte[] _chunk2;
+        private byte[] _chunk2CatchUp;
+        private byte[] _chunk3;
+        private SftpFileReader _reader;
+        private byte[] _actualChunk1;
+        private byte[] _actualChunk2;
+        private byte[] _actualChunk2CatchUp;
+        private byte[] _actualChunk3;
+        private ManualResetEvent _chunk1BeginRead;
+        private ManualResetEvent _chunk2BeginRead;
+        private ManualResetEvent _chunk3BeginRead;
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 3);
+            _chunk1 = CreateByteArray(random, ChunkLength);
+            _chunk2 = CreateByteArray(random, ChunkLength - 10);
+            _chunk2CatchUp = CreateByteArray(random, 10);
+            _chunk3 = new byte[0];
+            _chunk1BeginRead = new ManualResetEvent(false);
+            _chunk2BeginRead = new ManualResetEvent(false);
+            _chunk3BeginRead = new ManualResetEvent(false);
+            _fileSize = _chunk1.Length + _chunk2.Length + _chunk2CatchUp.Length + _chunk3.Length;
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SftpSessionMock.InSequence(seq).Setup(p => p.RequestFStat(_handle)).Returns(CreateSftpFileAttributes(_fileSize));
+            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) =>
+                            {
+                                _chunk1BeginRead.Set();
+                                var asyncResult = new SftpReadAsyncResult(callback, state);
+                                asyncResult.SetAsCompleted(_chunk1, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+            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) =>
+                            {
+                                _chunk2BeginRead.Set();
+                                var asyncResult = new SftpReadAsyncResult(callback, state);
+                                asyncResult.SetAsCompleted(_chunk2, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+            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) =>
+                            {
+                                _chunk3BeginRead.Set();
+                                var asyncResult = new SftpReadAsyncResult(callback, state);
+                                asyncResult.SetAsCompleted(_chunk3, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+            SftpSessionMock.InSequence(seq)
+                            .Setup(p => p.RequestRead(_handle, 2 * ChunkLength - 10, 10))
+                            .Returns(_chunk2CatchUp);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, 3);
+        }
+
+        protected override void Act()
+        {
+            // consume chunk 1
+            _actualChunk1 = _reader.Read();
+            // consume chunk 2
+            _actualChunk2 = _reader.Read();
+            // wait until chunk3 has been read-ahead
+            Assert.IsTrue(_chunk3BeginRead.WaitOne(200));
+            // consume remaining parts of chunk 2
+            _actualChunk2CatchUp = _reader.Read();
+            // consume chunk 3
+            _actualChunk3 = _reader.Read();
+        }
+
+        [TestMethod]
+        public void FirstReadShouldReturnChunk1()
+        {
+            Assert.IsNotNull(_actualChunk1);
+            Assert.AreSame(_chunk1, _actualChunk1);
+        }
+
+        [TestMethod]
+        public void SecondReadShouldReturnChunk2()
+        {
+            Assert.IsNotNull(_actualChunk2);
+            Assert.AreSame(_chunk2, _actualChunk2);
+        }
+
+        [TestMethod]
+        public void ThirdReadShouldReturnChunk2CatchUp()
+        {
+            Assert.IsNotNull(_actualChunk2CatchUp);
+            Assert.AreSame(_chunk2CatchUp, _actualChunk2CatchUp);
+        }
+
+        [TestMethod]
+        public void FourthReadShouldReturnChunk3()
+        {
+            Assert.IsNotNull(_actualChunk3);
+            Assert.AreSame(_chunk3, _actualChunk3);
+        }
+
+        [TestMethod]
+        public void ReadAfterEndOfFileShouldThrowSshException()
+        {
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (SshException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual("Attempting to read beyond the end of the file.", ex.Message);
+            }
+        }
+
+        [TestMethod]
+        public void DisposeShouldCompleteImmediately()
+        {
+            var stopwatch = Stopwatch.StartNew();
+            _reader.Dispose();
+            stopwatch.Stop();
+
+            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
+        }
+    }
+}

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

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

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

@@ -0,0 +1,142 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Abstractions;
+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_ReadAheadEndInvokeException_DiscardsFurtherReadAheads : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private byte[] _handle;
+        private int _fileSize;
+        private byte[] _chunk1;
+        private byte[] _chunk3;
+        private ManualResetEvent _readAheadChunk3Completed;
+        private SftpFileReader _reader;
+        private SshException _exception;
+        private SshException _actualException;
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 5);
+            _chunk1 = CreateByteArray(random, ChunkLength);
+            _chunk3 = CreateByteArray(random, ChunkLength);
+            _fileSize = 3 * ChunkLength;
+
+            _readAheadChunk3Completed = new ManualResetEvent(false);
+
+            _exception = new SshException();
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SftpSessionMock.InSequence(seq).Setup(p => p.RequestFStat(_handle)).Returns(CreateSftpFileAttributes(_fileSize));
+            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.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
+                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
+                            {
+                                ThreadAbstraction.ExecuteThread(() =>
+                                {
+                                    // wait until the read-ahead for chunk3 has completed
+                                    _readAheadChunk3Completed.WaitOne(TimeSpan.FromSeconds(5));
+
+                                    // complete async read of chunk2 with exception
+                                    var asyncResult = new SftpReadAsyncResult(callback, state);
+                                    asyncResult.SetAsCompleted(_exception, false);
+                                });
+                            })
+                           .Returns((SftpReadAsyncResult)null);
+            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);
+
+                                // signal that we've com^meted the read-ahead for chunk3
+                                _readAheadChunk3Completed.Set();
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, 2);
+        }
+
+        protected override void Act()
+        {
+            _reader.Read();
+
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (SshException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void ReadOfSecondChunkShouldThrowExceptionThatOccurredInReadAhead()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.AreSame(_exception, _actualException);
+        }
+
+        [TestMethod]
+        public void ReahAheadOfChunk3ShouldHaveBeenDone()
+        {
+            SftpSessionMock.Verify(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()), Times.Once);
+        }
+
+        [TestMethod]
+        public void ReadAfterReadAheadExceptionShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual(typeof(SftpFileReader).FullName, ex.ObjectName);
+            }
+        }
+
+        [TestMethod]
+        public void DisposeShouldCompleteImmediately()
+        {
+            var stopwatch = Stopwatch.StartNew();
+            _reader.Dispose();
+            stopwatch.Stop();
+
+            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
+        }
+    }
+}

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

@@ -0,0 +1,155 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Abstractions;
+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_ReadAheadEndInvokeException_PreventsFurtherReadAheads : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private byte[] _handle;
+        private int _fileSize;
+        private byte[] _chunk1;
+        private byte[] _chunk3;
+        private SftpFileReader _reader;
+        private ManualResetEvent _readAheadChunk2;
+        private ManualResetEvent _readChunk2;
+        private SshException _exception;
+        private SshException _actualException;
+
+        protected override void SetupData()
+        {
+            var random = new Random();
+
+            _handle = CreateByteArray(random, 5);
+            _chunk1 = CreateByteArray(random, ChunkLength);
+            _chunk3 = CreateByteArray(random, ChunkLength);
+            _fileSize = 3 * _chunk1.Length;
+
+            _readAheadChunk2 = new ManualResetEvent(false);
+            _readChunk2 = new ManualResetEvent(false);
+
+            _exception = new SshException();
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SftpSessionMock.InSequence(seq).Setup(p => p.RequestFStat(_handle)).Returns(CreateSftpFileAttributes(_fileSize));
+            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.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
+                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
+                            {
+                                ThreadAbstraction.ExecuteThread(() =>
+                                {
+                                    // signal that we're in the read-ahead for chunk2
+                                    _readAheadChunk2.Set();
+                                    // wait for client to start reading this chunk
+                                    _readChunk2.WaitOne(TimeSpan.FromSeconds(5));
+                                    // sleep a short time to make sure the client is in the blocking wait
+                                    Thread.Sleep(500);
+                                    // complete async read of chunk2 with exception
+                                    var asyncResult = new SftpReadAsyncResult(callback, state);
+                                    asyncResult.SetAsCompleted(_exception, false);
+                                });
+                            })
+                           .Returns((SftpReadAsyncResult)null);
+            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);
+
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            // use a max. read-ahead of 1 to allow us to verify that the next read-ahead is not done
+            // when a read-ahead has failed
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, 1);
+        }
+
+        protected override void Act()
+        {
+            _reader.Read();
+
+            // wait until SftpFileReader has starting reading ahead chunk 2
+            Assert.IsTrue(_readAheadChunk2.WaitOne(TimeSpan.FromSeconds(5)));
+            // signal that we are about to read chunk 2
+            _readChunk2.Set();
+
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (SshException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void ReadOfSecondChunkShouldThrowExceptionThatOccurredInReadAhead()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.AreSame(_exception, _actualException);
+        }
+
+        [TestMethod]
+        public void ReadAfterReadAheadExceptionShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual(typeof(SftpFileReader).FullName, ex.ObjectName);
+            }
+        }
+
+        [TestMethod]
+        public void DisposeShouldCompleteImmediately()
+        {
+            var stopwatch = Stopwatch.StartNew();
+            _reader.Dispose();
+            stopwatch.Stop();
+
+            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
+        }
+
+        [TestMethod]
+        public void ExceptionInReadAheadShouldPreventFurtherReadAheads()
+        {
+            SftpSessionMock.Verify(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()), Times.Never);
+        }
+    }
+}

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

@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    class SftpFileReaderTest_ReadBackBeginReadException
+    {
+    }
+}

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

@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    class SftpFileReaderTest_ReadBackEndInvokeException
+    {
+    }
+}

+ 135 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ExceptionInReadAhead.cs

@@ -0,0 +1,135 @@
+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_ExceptionInReadAhead : SftpFileReaderTestBase
+    {
+        private const int ChunkLength = 32 * 1024;
+
+        private byte[] _handle;
+        private int _fileSize;
+        private byte[] _chunk1;
+        private byte[] _chunk2;
+        private SftpFileReader _reader;
+        private ManualResetEvent _readAhead;
+        private ManualResetEvent _readChunk3;
+        private SshException _exception;
+        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;
+
+            _readAhead = new ManualResetEvent(false);
+            _readChunk3 = new ManualResetEvent(false);
+
+            _exception = new SshException();
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SftpSessionMock.InSequence(seq).Setup(p => p.RequestFStat(_handle)).Returns(CreateSftpFileAttributes(_fileSize));
+            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.BeginRead(_handle, 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(_chunk2, false);
+                            })
+                            .Returns((SftpReadAsyncResult)null);
+            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) =>
+                            {
+                                _readAhead.Set();
+                                _readChunk3.WaitOne(TimeSpan.FromSeconds(5));
+                                // sleep a short time to make sure the client is in the blocking wait
+                                Thread.Sleep(500);
+                            })
+                            .Throws(_exception);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, 3);
+        }
+
+        protected override void Act()
+        {
+            _reader.Read();
+            _reader.Read();
+
+            // wait until we've the SftpFileReader has starting reading ahead chunk 3
+            Assert.IsTrue(_readAhead.WaitOne(TimeSpan.FromSeconds(5)));
+            // signal that we are about to read chunk 3
+            _readChunk3.Set();
+
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (SshException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void ReadOfThirdChunkShouldThrowExceptionThatOccurredInReadAhead()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.AreSame(_exception, _actualException);
+        }
+
+        [TestMethod]
+        public void ReadAfterReadAheadExceptionShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                _reader.Read();
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual(typeof(SftpFileReader).FullName, ex.ObjectName);
+            }
+        }
+
+        [TestMethod]
+        public void DisposeShouldCompleteImmediately()
+        {
+            var stopwatch = Stopwatch.StartNew();
+            _reader.Dispose();
+            stopwatch.Stop();
+
+            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
+        }
+    }
+}

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

@@ -340,6 +340,17 @@
     <Compile Include="Classes\SftpClientTest_Dispose_Disconnected.cs" />
     <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_LastChunkBeforeEofIsComplete.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_LastChunkBeforeEofIsPartial.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsNotReached.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsReached.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_ReadAheadEndInvokeException_DiscardsFurtherReadAheads.cs" />
+    <Compile Include="Classes\Sftp\SftpFileReaderTest_ReadAheadEndInvokeException_PreventsFurtherReadAheads.cs" />
+    <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\SftpFileStreamTest_CanRead_Closed_FileAccessRead.cs" />
     <Compile Include="Classes\Sftp\SftpFileStreamTest_CanRead_Closed_FileAccessReadWrite.cs" />
     <Compile Include="Classes\Sftp\SftpFileStreamTest_CanRead_Closed_FileAccessWrite.cs" />

+ 139 - 65
src/Renci.SshNet/Sftp/SftpFileReader.cs

@@ -6,71 +6,54 @@ using System.Threading;
 
 namespace Renci.SshNet.Sftp
 {
-    internal class SftpFileReader
+    internal class SftpFileReader : IDisposable
     {
-        private const int MaxReadAhead = 15;
-
         private readonly byte[] _handle;
         private readonly ISftpSession _sftpSession;
-        private SemaphoreLight _semaphore;
-        private bool _isCompleted;
         private uint _chunkLength;
-        private int _readAheadChunkIndex;
-        private int _nextChunkIndex;
-        private ulong _readAheadOffset;
         private ulong _offset;
         private ulong _fileSize;
-        private Exception _exception;
         private readonly IDictionary<int, BufferedRead> _queue;
+
+        private int _readAheadChunkIndex;
+        private ulong _readAheadOffset;
+        private ManualResetEvent _readAheadCompleted;
+        private int _nextChunkIndex;
+
+        private bool _isEndOfFile;
+        private SemaphoreLight _semaphore;
         private readonly object _readLock;
 
-        public SftpFileReader(byte[] handle, ISftpSession sftpSession)
+        private Exception _exception;
+        private bool _disposed;
+
+        /// <summary>
+        /// Initializes a new <see cref="SftpFileReader"/> instance with the specified handle,
+        /// <see cref="ISftpSession"/> and the maximum number of pending reads.
+        /// </summary>
+        /// <param name="handle"></param>
+        /// <param name="sftpSession"></param>
+        /// <param name="maxReadHead">The maximum number of pending reads.</param>
+        public SftpFileReader(byte[] handle, ISftpSession sftpSession, int maxReadHead)
         {
             _handle = handle;
             _sftpSession = sftpSession;
-            _chunkLength = 32 * 1024; // TODO !
-            _semaphore = new SemaphoreLight(MaxReadAhead);
-            _queue = new Dictionary<int, BufferedRead>(MaxReadAhead);
+            _chunkLength = 32 * 1024 - 13; // TODO !
+            _semaphore = new SemaphoreLight(maxReadHead);
+            _queue = new Dictionary<int, BufferedRead>(maxReadHead);
             _readLock = new object();
+            _readAheadCompleted = new ManualResetEvent(false);
 
             _fileSize = (ulong)_sftpSession.RequestFStat(_handle).Size;
 
-            ThreadAbstraction.ExecuteThread(() =>
-            {
-                // read one chunk beyond the chunk in which we read "file size" bytes
-                // to get an EOF
-
-                while (_readAheadOffset <= (_fileSize + _chunkLength) && _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();
-
-                    // don't bother reading any more chunks if we reached EOF, or an exception has occurred
-                    // while processing a chunk
-                    if (_isCompleted || _exception != null)
-                        break;
-
-                    // TODO: catch exception, signal error to Read() and break loop
-
-                    // start reading next chunk
-                    _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkLength, ReadCompleted,
-                                           new BufferedRead(_readAheadChunkIndex, _readAheadOffset));
-
-                    // advance read-ahead offset
-                    _readAheadOffset += _chunkLength;
-
-                    _readAheadChunkIndex++;
-                }
-
-                Console.WriteLine("Finished read-ahead");
-            });
+            StartReadAhead();
         }
 
         public byte[] Read()
         {
-            if (_isCompleted)
+            if (_exception != null || _disposed)
+                throw new ObjectDisposedException(GetType().FullName);
+            if (_isEndOfFile)
                 throw new SshException("Attempting to read beyond the end of the file.");
 
             lock (_readLock)
@@ -81,7 +64,9 @@ namespace Renci.SshNet.Sftp
 
                 // wait until either the next chunk is avalable or an exception has occurred
                 while (!_queue.TryGetValue(_nextChunkIndex, out nextChunk) && _exception == null)
+                {
                     Monitor.Wait(_readLock);
+                }
 
                 if (_exception != null)
                     throw _exception;
@@ -98,7 +83,7 @@ namespace Renci.SshNet.Sftp
                     // have we reached EOF?
                     if (data.Length == 0)
                     {
-                        _isCompleted = true;
+                        _isEndOfFile = true;
                     }
                     // unblock wait in read-ahead
                     _semaphore.Release();
@@ -109,7 +94,7 @@ namespace Renci.SshNet.Sftp
                 // chunk if we haven't already read up to the file size
                 if (nextChunk.Data.Length == 0 && _offset == _fileSize)
                 {
-                    _isCompleted = true;
+                    _isEndOfFile = true;
 
                     // unblock wait in read-ahead
                     _semaphore.Release();
@@ -120,29 +105,94 @@ namespace Renci.SshNet.Sftp
                 // when the server returned less bytes than requested (for the previous chunk)
                 // we'll synchronously request the remaining data
 
-                var catchUp = new byte[nextChunk.Offset - _offset];
-                var bytesCaughtUp = 0L;
+                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)
+                {
+                    // move reader to error state
+                    _exception = new SshException("Unexpectedly reached end of file.");
+                    // unblock wait in read-ahead
+                    _semaphore.Release();
+                    // notify caller of error
+                    throw _exception;
+                }
+
+                _offset += (uint) read.Length;
+
+                return read;
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        protected void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                var readAheadCompleted = _readAheadCompleted;
+                if (readAheadCompleted != null)
+                {
+                    _readAheadCompleted = null;
+                    if (!readAheadCompleted.WaitOne(TimeSpan.FromSeconds(1)))
+                    {
+                        DiagnosticAbstraction.Log("Read-ahead thread did not complete within time-out.");
+                    }
+                    readAheadCompleted.Dispose();
+                }
 
-                while (bytesCaughtUp < catchUp.Length)
+                _disposed = true;
+            }
+        }
+
+        private void StartReadAhead()
+        {
+            ThreadAbstraction.ExecuteThread(() =>
+            {
+                while (_exception == null)
                 {
-                    // TODO: break loop and interrupt blocking wait in case of exception
-                    var read = _sftpSession.RequestRead(_handle, _offset, (uint) catchUp.Length);
-                    if (read.Length == 0)
+                    // 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();
+
+                    // don't bother reading any more chunks if we reached EOF, or an exception has occurred
+                    // while processing a chunk
+                    if (_isEndOfFile || _exception != null)
+                        break;
+
+                    // start reading next chunk
+                    try
                     {
-                        // move reader to error state
-                        _exception = new SshException("Unexpectedly reached end of file.");
-                        // unblock wait in read-ahead
-                        _semaphore.Release();
-                        // notify caller of error
-                        throw _exception;
+                        _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkLength, ReadCompleted,
+                                               new BufferedRead(_readAheadChunkIndex, _readAheadOffset));
+                    }
+                    catch (Exception ex)
+                    {
+                        HandleFailure(ex);
+                        break;
                     }
 
-                    bytesCaughtUp += read.Length;
-                    _offset += (ulong) bytesCaughtUp;
+                    if (_readAheadOffset >= _fileSize)
+                    {
+                        // read one chunk beyond the chunk in which we read "file size" bytes
+                        // to get an EOF
+                        break;
+                    }
+
+                    // advance read-ahead offset
+                    _readAheadOffset += _chunkLength;
+
+                    _readAheadChunkIndex++;
                 }
 
-                return catchUp;
-            }
+                _readAheadCompleted.Set();
+            });
         }
 
         private void ReadCompleted(IAsyncResult result)
@@ -151,7 +201,17 @@ namespace Renci.SshNet.Sftp
             if (readAsyncResult == null)
                 return;
 
-            var data = readAsyncResult.EndInvoke();
+            byte[] data = null;
+
+            try
+            {
+                data = readAsyncResult.EndInvoke();
+            }
+            catch (Exception ex)
+            {
+                HandleFailure(ex);
+                return;
+            }
 
             // a read that completes with a zero-byte result signals EOF
             // but there may be pending reads before that read
@@ -167,7 +227,21 @@ namespace Renci.SshNet.Sftp
             }
         }
 
-        private class BufferedRead
+        private void HandleFailure(Exception cause)
+        {
+            _exception = cause;
+
+            // unblock read-ahead
+            _semaphore.Release();
+
+            // unblock Read()
+            lock (_readLock)
+            {
+                Monitor.PulseAll(_readLock);
+            }
+        }
+
+        internal class BufferedRead
         {
             public int ChunkIndex { get; private set; }
 

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

@@ -2000,7 +2000,7 @@ namespace Renci.SshNet
             // TODO close handle in case of exception
             // TODO decide whether to move opening (and closing) of handle to SftpFileReader
 
-            var fileReader = new SftpFileReader(handle, _sftpSession);
+            var fileReader = new SftpFileReader(handle, _sftpSession, 15);
             var totalBytesRead = 0UL;
 
             while (true)