|
|
@@ -6,13 +6,17 @@ using System.Threading;
|
|
|
|
|
|
namespace Renci.SshNet.Sftp
|
|
|
{
|
|
|
- internal class SftpFileReader : IDisposable
|
|
|
+ internal class SftpFileReader : ISftpFileReader
|
|
|
{
|
|
|
private readonly byte[] _handle;
|
|
|
private readonly ISftpSession _sftpSession;
|
|
|
- private uint _chunkLength;
|
|
|
+ private readonly uint _chunkSize;
|
|
|
private ulong _offset;
|
|
|
- private ulong _fileSize;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Holds the size of the file, when available.
|
|
|
+ /// </summary>
|
|
|
+ private long? _fileSize;
|
|
|
private readonly IDictionary<int, BufferedRead> _queue;
|
|
|
|
|
|
private int _readAheadChunkIndex;
|
|
|
@@ -20,7 +24,14 @@ namespace Renci.SshNet.Sftp
|
|
|
private ManualResetEvent _readAheadCompleted;
|
|
|
private int _nextChunkIndex;
|
|
|
|
|
|
- private bool _isEndOfFile;
|
|
|
+ /// <summary>
|
|
|
+ /// Holds a value indicating whether EOF has already been signaled by the SSH server.
|
|
|
+ /// </summary>
|
|
|
+ private bool _endOfFileReceived;
|
|
|
+ /// <summary>
|
|
|
+ /// Holds a value indicating whether the client has read up to the end of the file.
|
|
|
+ /// </summary>
|
|
|
+ private bool _isEndOfFileRead;
|
|
|
private SemaphoreLight _semaphore;
|
|
|
private readonly object _readLock;
|
|
|
|
|
|
@@ -33,19 +44,20 @@ namespace Renci.SshNet.Sftp
|
|
|
/// </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)
|
|
|
+ /// <param name="chunkSize">The size of a individual read-ahead chunk.</param>
|
|
|
+ /// <param name="maxPendingReads">The maximum number of pending reads.</param>
|
|
|
+ /// <param name="fileSize">The size of the file, if known; otherwise, <c>null</c>.</param>
|
|
|
+ public SftpFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize)
|
|
|
{
|
|
|
_handle = handle;
|
|
|
_sftpSession = sftpSession;
|
|
|
- _chunkLength = 32 * 1024 - 13; // TODO !
|
|
|
- _semaphore = new SemaphoreLight(maxReadHead);
|
|
|
- _queue = new Dictionary<int, BufferedRead>(maxReadHead);
|
|
|
+ _fileSize = fileSize;
|
|
|
+ _chunkSize = chunkSize;
|
|
|
+ _semaphore = new SemaphoreLight(maxPendingReads);
|
|
|
+ _queue = new Dictionary<int, BufferedRead>(maxPendingReads);
|
|
|
_readLock = new object();
|
|
|
_readAheadCompleted = new ManualResetEvent(false);
|
|
|
|
|
|
- _fileSize = (ulong)_sftpSession.RequestFStat(_handle).Size;
|
|
|
-
|
|
|
StartReadAhead();
|
|
|
}
|
|
|
|
|
|
@@ -53,7 +65,7 @@ namespace Renci.SshNet.Sftp
|
|
|
{
|
|
|
if (_exception != null || _disposed)
|
|
|
throw new ObjectDisposedException(GetType().FullName);
|
|
|
- if (_isEndOfFile)
|
|
|
+ if (_isEndOfFileRead)
|
|
|
throw new SshException("Attempting to read beyond the end of the file.");
|
|
|
|
|
|
lock (_readLock)
|
|
|
@@ -74,27 +86,32 @@ namespace Renci.SshNet.Sftp
|
|
|
if (nextChunk.Offset == _offset)
|
|
|
{
|
|
|
var data = nextChunk.Data;
|
|
|
- _offset += (ulong) data.Length;
|
|
|
|
|
|
- // remove processed chunk
|
|
|
- _queue.Remove(_nextChunkIndex);
|
|
|
- // move to next chunk
|
|
|
- _nextChunkIndex++;
|
|
|
// have we reached EOF?
|
|
|
if (data.Length == 0)
|
|
|
{
|
|
|
- _isEndOfFile = true;
|
|
|
+ _isEndOfFileRead = true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // remove processed chunk
|
|
|
+ _queue.Remove(_nextChunkIndex);
|
|
|
+ // update offset
|
|
|
+ _offset += (ulong)data.Length;
|
|
|
+ // move to next chunk
|
|
|
+ _nextChunkIndex++;
|
|
|
}
|
|
|
// unblock wait in read-ahead
|
|
|
_semaphore.Release();
|
|
|
return data;
|
|
|
}
|
|
|
|
|
|
- // when we received an EOF for the next chunk, then we only complete the current
|
|
|
- // chunk if we haven't already read up to the file size
|
|
|
- if (nextChunk.Data.Length == 0 && _offset == _fileSize)
|
|
|
+ // when we received an EOF for the next chunk and the size of the file is known, then
|
|
|
+ // we only complete the current chunk if we haven't already read up to the file size;
|
|
|
+ // this way we save an extra round-trip to the server
|
|
|
+ if (nextChunk.Data.Length == 0 && _fileSize.HasValue && _offset == (ulong)_fileSize.Value)
|
|
|
{
|
|
|
- _isEndOfFile = true;
|
|
|
+ _isEndOfFileRead = true;
|
|
|
|
|
|
// unblock wait in read-ahead
|
|
|
_semaphore.Release();
|
|
|
@@ -104,6 +121,13 @@ namespace Renci.SshNet.Sftp
|
|
|
|
|
|
// when the server returned less bytes than requested (for the previous chunk)
|
|
|
// we'll synchronously request the remaining data
|
|
|
+ //
|
|
|
+ // due to the optimization above, we'll only get here in one of the following cases:
|
|
|
+ // - an EOF situation for files for which we were unable to obtain the file size
|
|
|
+ // - fewer bytes that requested were returned
|
|
|
+ //
|
|
|
+ // according to the SSH specification, this last case should never happen for normal
|
|
|
+ // disk files (but can happen for device files).
|
|
|
|
|
|
var bytesToCatchUp = nextChunk.Offset - _offset;
|
|
|
|
|
|
@@ -111,6 +135,18 @@ namespace Renci.SshNet.Sftp
|
|
|
var read = _sftpSession.RequestRead(_handle, _offset, (uint) bytesToCatchUp);
|
|
|
if (read.Length == 0)
|
|
|
{
|
|
|
+ // a zero-length (EOF) response is only valid for the read-back when EOF has
|
|
|
+ // been signaled for the next read-ahead chunk
|
|
|
+ if (nextChunk.Data.Length == 0)
|
|
|
+ {
|
|
|
+ _isEndOfFileRead = true;
|
|
|
+
|
|
|
+ // unblock wait in read-ahead
|
|
|
+ _semaphore.Release();
|
|
|
+ // signal EOF to caller
|
|
|
+ return read;
|
|
|
+ }
|
|
|
+
|
|
|
// move reader to error state
|
|
|
_exception = new SshException("Unexpectedly reached end of file.");
|
|
|
// unblock wait in read-ahead
|
|
|
@@ -128,7 +164,6 @@ namespace Renci.SshNet.Sftp
|
|
|
public void Dispose()
|
|
|
{
|
|
|
Dispose(true);
|
|
|
- GC.SuppressFinalize(this);
|
|
|
}
|
|
|
|
|
|
protected void Dispose(bool disposing)
|
|
|
@@ -138,14 +173,16 @@ namespace Renci.SshNet.Sftp
|
|
|
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();
|
|
|
+ _readAheadCompleted = null;
|
|
|
}
|
|
|
|
|
|
+ _sftpSession.RequestClose(_handle);
|
|
|
+
|
|
|
_disposed = true;
|
|
|
}
|
|
|
}
|
|
|
@@ -154,6 +191,7 @@ namespace Renci.SshNet.Sftp
|
|
|
{
|
|
|
ThreadAbstraction.ExecuteThread(() =>
|
|
|
{
|
|
|
+ // TODO: take dispose into account
|
|
|
while (_exception == null)
|
|
|
{
|
|
|
// TODO implement cancellation!?
|
|
|
@@ -161,15 +199,15 @@ namespace Renci.SshNet.Sftp
|
|
|
// 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
|
|
|
+ // don't bother reading any more chunks if we received EOF, or an exception has occurred
|
|
|
// while processing a chunk
|
|
|
- if (_isEndOfFile || _exception != null)
|
|
|
+ if (_endOfFileReceived || _exception != null)
|
|
|
break;
|
|
|
|
|
|
// start reading next chunk
|
|
|
try
|
|
|
{
|
|
|
- _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkLength, ReadCompleted,
|
|
|
+ _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkSize, ReadCompleted,
|
|
|
new BufferedRead(_readAheadChunkIndex, _readAheadOffset));
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
@@ -178,15 +216,8 @@ namespace Renci.SshNet.Sftp
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
- 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;
|
|
|
+ _readAheadOffset += _chunkSize;
|
|
|
|
|
|
_readAheadChunkIndex++;
|
|
|
}
|
|
|
@@ -201,7 +232,7 @@ namespace Renci.SshNet.Sftp
|
|
|
if (readAsyncResult == null)
|
|
|
return;
|
|
|
|
|
|
- byte[] data = null;
|
|
|
+ byte[] data;
|
|
|
|
|
|
try
|
|
|
{
|
|
|
@@ -219,8 +250,15 @@ namespace Renci.SshNet.Sftp
|
|
|
bufferedRead.Complete(data);
|
|
|
_queue.Add(bufferedRead.ChunkIndex, bufferedRead);
|
|
|
|
|
|
+ // check if server signaled EOF
|
|
|
+ if (data.Length == 0)
|
|
|
+ {
|
|
|
+ // set a flag to stop read-aheads
|
|
|
+ _endOfFileReceived = true;
|
|
|
+ }
|
|
|
+
|
|
|
// signal that a chunk has been read or EOF has been reached;
|
|
|
- // in both cases, we want to unblock the "read-ahead" thread
|
|
|
+ // in both cases, Read() will eventually also unblock the "read-ahead" thread
|
|
|
lock (_readLock)
|
|
|
{
|
|
|
Monitor.PulseAll(_readLock);
|