| | | 1 | | using System; |
| | | 2 | | using System.Collections.Generic; |
| | | 3 | | using System.Globalization; |
| | | 4 | | using System.Runtime.ExceptionServices; |
| | | 5 | | using System.Threading; |
| | | 6 | | |
| | | 7 | | using Renci.SshNet.Abstractions; |
| | | 8 | | using Renci.SshNet.Common; |
| | | 9 | | |
| | | 10 | | namespace Renci.SshNet.Sftp |
| | | 11 | | { |
| | | 12 | | internal sealed class SftpFileReader : ISftpFileReader |
| | | 13 | | { |
| | | 14 | | private const int ReadAheadWaitTimeoutInMilliseconds = 1000; |
| | | 15 | | |
| | | 16 | | private readonly byte[] _handle; |
| | | 17 | | private readonly ISftpSession _sftpSession; |
| | | 18 | | private readonly uint _chunkSize; |
| | | 19 | | private readonly SemaphoreLight _semaphore; |
| | | 20 | | private readonly object _readLock; |
| | | 21 | | private readonly ManualResetEvent _disposingWaitHandle; |
| | | 22 | | private readonly ManualResetEvent _readAheadCompleted; |
| | | 23 | | private readonly Dictionary<int, BufferedRead> _queue; |
| | | 24 | | private readonly WaitHandle[] _waitHandles; |
| | | 25 | | |
| | | 26 | | /// <summary> |
| | | 27 | | /// Holds the size of the file, when available. |
| | | 28 | | /// </summary> |
| | | 29 | | private readonly long? _fileSize; |
| | | 30 | | |
| | | 31 | | private ulong _offset; |
| | | 32 | | private int _readAheadChunkIndex; |
| | | 33 | | private ulong _readAheadOffset; |
| | | 34 | | private int _nextChunkIndex; |
| | | 35 | | |
| | | 36 | | /// <summary> |
| | | 37 | | /// Holds a value indicating whether EOF has already been signaled by the SSH server. |
| | | 38 | | /// </summary> |
| | | 39 | | private bool _endOfFileReceived; |
| | | 40 | | |
| | | 41 | | /// <summary> |
| | | 42 | | /// Holds a value indicating whether the client has read up to the end of the file. |
| | | 43 | | /// </summary> |
| | | 44 | | private bool _isEndOfFileRead; |
| | | 45 | | |
| | | 46 | | private bool _disposingOrDisposed; |
| | | 47 | | |
| | | 48 | | private Exception _exception; |
| | | 49 | | |
| | | 50 | | /// <summary> |
| | | 51 | | /// Initializes a new instance of the <see cref="SftpFileReader"/> class with the specified handle, |
| | | 52 | | /// <see cref="ISftpSession"/> and the maximum number of pending reads. |
| | | 53 | | /// </summary> |
| | | 54 | | /// <param name="handle">The file handle.</param> |
| | | 55 | | /// <param name="sftpSession">The SFT session.</param> |
| | | 56 | | /// <param name="chunkSize">The size of a individual read-ahead chunk.</param> |
| | | 57 | | /// <param name="maxPendingReads">The maximum number of pending reads.</param> |
| | | 58 | | /// <param name="fileSize">The size of the file, if known; otherwise, <see langword="null"/>.</param> |
| | 220 | 59 | | public SftpFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSi |
| | 220 | 60 | | { |
| | 220 | 61 | | _handle = handle; |
| | 220 | 62 | | _sftpSession = sftpSession; |
| | 220 | 63 | | _chunkSize = chunkSize; |
| | 220 | 64 | | _fileSize = fileSize; |
| | 220 | 65 | | _semaphore = new SemaphoreLight(maxPendingReads); |
| | 220 | 66 | | _queue = new Dictionary<int, BufferedRead>(maxPendingReads); |
| | 220 | 67 | | _readLock = new object(); |
| | 220 | 68 | | _readAheadCompleted = new ManualResetEvent(initialState: false); |
| | 220 | 69 | | _disposingWaitHandle = new ManualResetEvent(initialState: false); |
| | 220 | 70 | | _waitHandles = _sftpSession.CreateWaitHandleArray(_disposingWaitHandle, _semaphore.AvailableWaitHandle); |
| | | 71 | | |
| | 220 | 72 | | StartReadAhead(); |
| | 220 | 73 | | } |
| | | 74 | | |
| | | 75 | | public byte[] Read() |
| | 4248 | 76 | | { |
| | | 77 | | #if NET7_0_OR_GREATER |
| | 4063 | 78 | | ObjectDisposedException.ThrowIf(_disposingOrDisposed, this); |
| | | 79 | | #else |
| | 185 | 80 | | if (_disposingOrDisposed) |
| | 4 | 81 | | { |
| | 4 | 82 | | throw new ObjectDisposedException(GetType().FullName); |
| | | 83 | | } |
| | | 84 | | #endif // NET7_0_OR_GREATER |
| | | 85 | | |
| | 4236 | 86 | | if (_exception is not null) |
| | 24 | 87 | | { |
| | 24 | 88 | | ExceptionDispatchInfo.Capture(_exception).Throw(); |
| | 0 | 89 | | } |
| | | 90 | | |
| | 4212 | 91 | | if (_isEndOfFileRead) |
| | 12 | 92 | | { |
| | 12 | 93 | | throw new SshException("Attempting to read beyond the end of the file."); |
| | | 94 | | } |
| | | 95 | | |
| | | 96 | | BufferedRead nextChunk; |
| | | 97 | | |
| | 4200 | 98 | | lock (_readLock) |
| | 4200 | 99 | | { |
| | | 100 | | // wait until either the next chunk is available, an exception has occurred or the current |
| | | 101 | | // instance is already disposed |
| | 7996 | 102 | | while (!_queue.TryGetValue(_nextChunkIndex, out nextChunk) && _exception is null) |
| | 3796 | 103 | | { |
| | 3796 | 104 | | _ =Monitor.Wait(_readLock); |
| | 3796 | 105 | | } |
| | | 106 | | |
| | | 107 | | // throw when exception occured in read-ahead, or the current instance is already disposed |
| | 4200 | 108 | | if (_exception != null) |
| | 60 | 109 | | { |
| | 60 | 110 | | ExceptionDispatchInfo.Capture(_exception).Throw(); |
| | 0 | 111 | | } |
| | | 112 | | |
| | 4140 | 113 | | var data = nextChunk.Data; |
| | | 114 | | |
| | 4140 | 115 | | if (nextChunk.Offset == _offset) |
| | 4003 | 116 | | { |
| | | 117 | | // have we reached EOF? |
| | 4003 | 118 | | if (data.Length == 0) |
| | 65 | 119 | | { |
| | | 120 | | // PERF: we do not bother updating all of the internal state when we've reached EOF |
| | 65 | 121 | | _isEndOfFileRead = true; |
| | 65 | 122 | | } |
| | | 123 | | else |
| | 3938 | 124 | | { |
| | | 125 | | // remove processed chunk |
| | 3938 | 126 | | _ = _queue.Remove(_nextChunkIndex); |
| | | 127 | | |
| | | 128 | | // update offset |
| | 3938 | 129 | | _offset += (ulong) data.Length; |
| | | 130 | | |
| | | 131 | | // move to next chunk |
| | 3938 | 132 | | _nextChunkIndex++; |
| | 3938 | 133 | | } |
| | | 134 | | |
| | | 135 | | // unblock wait in read-ahead |
| | 4003 | 136 | | _ = _semaphore.Release(); |
| | | 137 | | |
| | 4003 | 138 | | return data; |
| | | 139 | | } |
| | | 140 | | |
| | | 141 | | // When we received an EOF for the next chunk and the size of the file is known, then |
| | | 142 | | // we only complete the current chunk if we haven't already read up to the file size. |
| | | 143 | | // This way we save an extra round-trip to the server. |
| | 137 | 144 | | if (data.Length == 0 && _fileSize.HasValue && _offset == (ulong) _fileSize.Value) |
| | 59 | 145 | | { |
| | | 146 | | // avoid future reads |
| | 59 | 147 | | _isEndOfFileRead = true; |
| | | 148 | | |
| | | 149 | | // unblock wait in read-ahead |
| | 59 | 150 | | _ = _semaphore.Release(); |
| | | 151 | | |
| | | 152 | | // signal EOF to caller |
| | 59 | 153 | | return nextChunk.Data; |
| | | 154 | | } |
| | 78 | 155 | | } |
| | | 156 | | |
| | | 157 | | /* |
| | | 158 | | * When the server returned less bytes than requested (for the previous chunk) |
| | | 159 | | * we'll synchronously request the remaining data. |
| | | 160 | | * |
| | | 161 | | * Due to the optimization above, we'll only get here in one of the following cases: |
| | | 162 | | * - an EOF situation for files for which we were unable to obtain the file size |
| | | 163 | | * - fewer bytes that requested were returned |
| | | 164 | | * |
| | | 165 | | * According to the SSH specification, this last case should never happen for normal |
| | | 166 | | * disk files (but can happen for device files). In practice, OpenSSH - for example - |
| | | 167 | | * returns less bytes than requested when requesting more than 64 KB. |
| | | 168 | | * |
| | | 169 | | * Important: |
| | | 170 | | * To avoid a deadlock, this read must be done outside of the read lock. |
| | | 171 | | */ |
| | | 172 | | |
| | 78 | 173 | | var bytesToCatchUp = nextChunk.Offset - _offset; |
| | | 174 | | |
| | | 175 | | /* |
| | | 176 | | * TODO: break loop and interrupt blocking wait in case of exception |
| | | 177 | | */ |
| | | 178 | | |
| | 78 | 179 | | var read = _sftpSession.RequestRead(_handle, _offset, (uint) bytesToCatchUp); |
| | 78 | 180 | | if (read.Length == 0) |
| | 0 | 181 | | { |
| | | 182 | | // process data in read lock to avoid ObjectDisposedException while releasing semaphore |
| | 0 | 183 | | lock (_readLock) |
| | 0 | 184 | | { |
| | | 185 | | // a zero-length (EOF) response is only valid for the read-back when EOF has |
| | | 186 | | // been signaled for the next read-ahead chunk |
| | 0 | 187 | | if (nextChunk.Data.Length == 0) |
| | 0 | 188 | | { |
| | 0 | 189 | | _isEndOfFileRead = true; |
| | | 190 | | |
| | | 191 | | // ensure we've not yet disposed the current instance |
| | 0 | 192 | | if (!_disposingOrDisposed) |
| | 0 | 193 | | { |
| | | 194 | | // unblock wait in read-ahead |
| | 0 | 195 | | _ = _semaphore.Release(); |
| | 0 | 196 | | } |
| | | 197 | | |
| | | 198 | | // signal EOF to caller |
| | 0 | 199 | | return read; |
| | | 200 | | } |
| | | 201 | | |
| | | 202 | | // move reader to error state |
| | 0 | 203 | | _exception = new SshException("Unexpectedly reached end of file."); |
| | | 204 | | |
| | | 205 | | // ensure we've not yet disposed the current instance |
| | 0 | 206 | | if (!_disposingOrDisposed) |
| | 0 | 207 | | { |
| | | 208 | | // unblock wait in read-ahead |
| | 0 | 209 | | _ = _semaphore.Release(); |
| | 0 | 210 | | } |
| | | 211 | | |
| | | 212 | | // notify caller of error |
| | 0 | 213 | | throw _exception; |
| | | 214 | | } |
| | | 215 | | } |
| | | 216 | | |
| | 78 | 217 | | _offset += (uint) read.Length; |
| | | 218 | | |
| | 78 | 219 | | return read; |
| | 4140 | 220 | | } |
| | | 221 | | |
| | | 222 | | ~SftpFileReader() |
| | 210 | 223 | | { |
| | 105 | 224 | | Dispose(disposing: false); |
| | 210 | 225 | | } |
| | | 226 | | |
| | | 227 | | public void Dispose() |
| | 118 | 228 | | { |
| | 118 | 229 | | Dispose(disposing: true); |
| | 118 | 230 | | GC.SuppressFinalize(this); |
| | 118 | 231 | | } |
| | | 232 | | |
| | | 233 | | /// <summary> |
| | | 234 | | /// Releases unmanaged and - optionally - managed resources. |
| | | 235 | | /// </summary> |
| | | 236 | | /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langwor |
| | | 237 | | private void Dispose(bool disposing) |
| | 223 | 238 | | { |
| | 223 | 239 | | if (_disposingOrDisposed) |
| | 3 | 240 | | { |
| | 3 | 241 | | return; |
| | | 242 | | } |
| | | 243 | | |
| | | 244 | | // transition to disposing state |
| | 220 | 245 | | _disposingOrDisposed = true; |
| | | 246 | | |
| | 220 | 247 | | if (disposing) |
| | 115 | 248 | | { |
| | | 249 | | // record exception to break prevent future Read() |
| | 115 | 250 | | _exception = new ObjectDisposedException(GetType().FullName); |
| | | 251 | | |
| | | 252 | | // signal that we're disposing to interrupt wait in read-ahead |
| | 115 | 253 | | _ = _disposingWaitHandle.Set(); |
| | | 254 | | |
| | | 255 | | // wait until the read-ahead thread has completed |
| | 115 | 256 | | _ = _readAheadCompleted.WaitOne(); |
| | | 257 | | |
| | | 258 | | // unblock the Read() |
| | 115 | 259 | | lock (_readLock) |
| | 115 | 260 | | { |
| | | 261 | | // dispose semaphore in read lock to ensure we don't run into an ObjectDisposedException |
| | | 262 | | // in Read() |
| | 115 | 263 | | _semaphore.Dispose(); |
| | | 264 | | |
| | | 265 | | // awake Read |
| | 115 | 266 | | Monitor.PulseAll(_readLock); |
| | 115 | 267 | | } |
| | | 268 | | |
| | 115 | 269 | | _readAheadCompleted.Dispose(); |
| | 115 | 270 | | _disposingWaitHandle.Dispose(); |
| | | 271 | | |
| | 115 | 272 | | if (_sftpSession.IsOpen) |
| | 106 | 273 | | { |
| | | 274 | | try |
| | 106 | 275 | | { |
| | 106 | 276 | | var closeAsyncResult = _sftpSession.BeginClose(_handle, callback: null, state: null); |
| | 97 | 277 | | _sftpSession.EndClose(closeAsyncResult); |
| | 88 | 278 | | } |
| | 18 | 279 | | catch (Exception ex) |
| | 18 | 280 | | { |
| | 18 | 281 | | DiagnosticAbstraction.Log("Failure closing handle: " + ex); |
| | 18 | 282 | | } |
| | 106 | 283 | | } |
| | 115 | 284 | | } |
| | 223 | 285 | | } |
| | | 286 | | |
| | | 287 | | private void StartReadAhead() |
| | 220 | 288 | | { |
| | 220 | 289 | | ThreadAbstraction.ExecuteThread(() => |
| | 220 | 290 | | { |
| | 4384 | 291 | | while (!_endOfFileReceived && _exception is null) |
| | 4235 | 292 | | { |
| | 220 | 293 | | // check if we should continue with the read-ahead loop |
| | 220 | 294 | | // note that the EOF and exception check are not included |
| | 220 | 295 | | // in this check as they do not require Read() to be |
| | 220 | 296 | | // unblocked (or have already done this) |
| | 4235 | 297 | | if (!ContinueReadAhead()) |
| | 33 | 298 | | { |
| | 220 | 299 | | // unblock the Read() |
| | 33 | 300 | | lock (_readLock) |
| | 33 | 301 | | { |
| | 33 | 302 | | Monitor.PulseAll(_readLock); |
| | 33 | 303 | | } |
| | 220 | 304 | | |
| | 220 | 305 | | // break the read-ahead loop |
| | 33 | 306 | | break; |
| | 220 | 307 | | } |
| | 220 | 308 | | |
| | 220 | 309 | | // attempt to obtain the semaphore; this may time out when all semaphores are |
| | 220 | 310 | | // in use due to pending read-aheads (which in turn can happen when the server |
| | 220 | 311 | | // is slow to respond or when the session is broken) |
| | 4202 | 312 | | if (!_semaphore.Wait(ReadAheadWaitTimeoutInMilliseconds)) |
| | 0 | 313 | | { |
| | 220 | 314 | | // re-evaluate whether an exception occurred, and - if not - wait again |
| | 0 | 315 | | continue; |
| | 220 | 316 | | } |
| | 220 | 317 | | |
| | 220 | 318 | | // don't bother reading any more chunks if we received EOF, an exception has occurred |
| | 220 | 319 | | // or the current instance is disposed |
| | 4202 | 320 | | if (_endOfFileReceived || _exception != null) |
| | 29 | 321 | | { |
| | 29 | 322 | | break; |
| | 220 | 323 | | } |
| | 220 | 324 | | |
| | 220 | 325 | | // start reading next chunk |
| | 4173 | 326 | | var bufferedRead = new BufferedRead(_readAheadChunkIndex, _readAheadOffset); |
| | 220 | 327 | | |
| | 220 | 328 | | try |
| | 4173 | 329 | | { |
| | 220 | 330 | | // even if we know the size of the file and have read up to EOF, we still want |
| | 220 | 331 | | // to keep reading (ahead) until we receive zero bytes from the remote host as |
| | 220 | 332 | | // we do not want to rely purely on the reported file size |
| | 220 | 333 | | // |
| | 220 | 334 | | // if the offset of the read-ahead chunk is greater than that file size, then |
| | 220 | 335 | | // we can expect to be reading the last (zero-byte) chunk and switch to synchronous |
| | 220 | 336 | | // mode to avoid having multiple read-aheads that read beyond EOF |
| | 4173 | 337 | | if (_fileSize != null && (long) _readAheadOffset > _fileSize.Value) |
| | 59 | 338 | | { |
| | 59 | 339 | | var asyncResult = _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkSize, callback: nu |
| | 59 | 340 | | var data = _sftpSession.EndRead(asyncResult); |
| | 59 | 341 | | ReadCompletedCore(bufferedRead, data); |
| | 59 | 342 | | } |
| | 220 | 343 | | else |
| | 4114 | 344 | | { |
| | 4114 | 345 | | _ = _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkSize, ReadCompleted, bufferedRea |
| | 4105 | 346 | | } |
| | 4164 | 347 | | } |
| | 9 | 348 | | catch (Exception ex) |
| | 9 | 349 | | { |
| | 9 | 350 | | HandleFailure(ex); |
| | 9 | 351 | | break; |
| | 220 | 352 | | } |
| | 220 | 353 | | |
| | 220 | 354 | | // advance read-ahead offset |
| | 4164 | 355 | | _readAheadOffset += _chunkSize; |
| | 220 | 356 | | |
| | 220 | 357 | | // increment index of read-ahead chunk |
| | 4164 | 358 | | _readAheadChunkIndex++; |
| | 4164 | 359 | | } |
| | 220 | 360 | | |
| | 220 | 361 | | _ = _readAheadCompleted.Set(); |
| | 440 | 362 | | }); |
| | 220 | 363 | | } |
| | | 364 | | |
| | | 365 | | /// <summary> |
| | | 366 | | /// Returns a value indicating whether the read-ahead loop should be continued. |
| | | 367 | | /// </summary> |
| | | 368 | | /// <returns> |
| | | 369 | | /// <see langword="true"/> if the read-ahead loop should be continued; otherwise, <see langword="false"/>. |
| | | 370 | | /// </returns> |
| | | 371 | | private bool ContinueReadAhead() |
| | 4235 | 372 | | { |
| | | 373 | | try |
| | 4235 | 374 | | { |
| | 4235 | 375 | | var waitResult = _sftpSession.WaitAny(_waitHandles, _sftpSession.OperationTimeout); |
| | 4217 | 376 | | switch (waitResult) |
| | | 377 | | { |
| | | 378 | | case 0: // disposing |
| | 15 | 379 | | return false; |
| | | 380 | | case 1: // semaphore available |
| | 4202 | 381 | | return true; |
| | | 382 | | default: |
| | 0 | 383 | | throw new NotImplementedException(string.Format(CultureInfo.InvariantCulture, "WaitAny return va |
| | | 384 | | } |
| | | 385 | | } |
| | 18 | 386 | | catch (Exception ex) |
| | 18 | 387 | | { |
| | 18 | 388 | | _ = Interlocked.CompareExchange(ref _exception, ex, comparand: null); |
| | 18 | 389 | | return false; |
| | | 390 | | } |
| | 4235 | 391 | | } |
| | | 392 | | |
| | | 393 | | private void ReadCompleted(IAsyncResult result) |
| | 4066 | 394 | | { |
| | 4066 | 395 | | if (_disposingOrDisposed) |
| | 12 | 396 | | { |
| | | 397 | | // skip further processing if we're disposing the current instance |
| | | 398 | | // to avoid accessing disposed members |
| | 12 | 399 | | return; |
| | | 400 | | } |
| | | 401 | | |
| | 4054 | 402 | | var readAsyncResult = (SftpReadAsyncResult) result; |
| | | 403 | | |
| | | 404 | | byte[] data; |
| | | 405 | | |
| | | 406 | | try |
| | 4054 | 407 | | { |
| | 4054 | 408 | | data = readAsyncResult.EndInvoke(); |
| | 4027 | 409 | | } |
| | 27 | 410 | | catch (Exception ex) |
| | 27 | 411 | | { |
| | 27 | 412 | | HandleFailure(ex); |
| | 27 | 413 | | return; |
| | | 414 | | } |
| | | 415 | | |
| | | 416 | | // a read that completes with a zero-byte result signals EOF |
| | | 417 | | // but there may be pending reads before that read |
| | 4027 | 418 | | var bufferedRead = (BufferedRead) readAsyncResult.AsyncState; |
| | 4027 | 419 | | ReadCompletedCore(bufferedRead, data); |
| | 4066 | 420 | | } |
| | | 421 | | |
| | | 422 | | private void ReadCompletedCore(BufferedRead bufferedRead, byte[] data) |
| | 4086 | 423 | | { |
| | 4086 | 424 | | bufferedRead.Complete(data); |
| | | 425 | | |
| | 4086 | 426 | | lock (_readLock) |
| | 4086 | 427 | | { |
| | | 428 | | // add item to queue |
| | 4086 | 429 | | _queue.Add(bufferedRead.ChunkIndex, bufferedRead); |
| | | 430 | | |
| | | 431 | | // Signal that a chunk has been read or EOF has been reached. |
| | | 432 | | // In both cases, Read() will eventually also unblock the "read-ahead" thread. |
| | 4086 | 433 | | Monitor.PulseAll(_readLock); |
| | 4086 | 434 | | } |
| | | 435 | | |
| | | 436 | | // check if server signaled EOF |
| | 4086 | 437 | | if (data.Length == 0) |
| | 124 | 438 | | { |
| | | 439 | | // set a flag to stop read-aheads |
| | 124 | 440 | | _endOfFileReceived = true; |
| | 124 | 441 | | } |
| | 4086 | 442 | | } |
| | | 443 | | |
| | | 444 | | private void HandleFailure(Exception cause) |
| | 36 | 445 | | { |
| | 36 | 446 | | _ = Interlocked.CompareExchange(ref _exception, cause, comparand: null); |
| | | 447 | | |
| | | 448 | | // unblock read-ahead |
| | 36 | 449 | | _ = _semaphore.Release(); |
| | | 450 | | |
| | | 451 | | // unblock Read() |
| | 36 | 452 | | lock (_readLock) |
| | 36 | 453 | | { |
| | 36 | 454 | | Monitor.PulseAll(_readLock); |
| | 36 | 455 | | } |
| | 36 | 456 | | } |
| | | 457 | | |
| | | 458 | | internal sealed class BufferedRead |
| | | 459 | | { |
| | 4086 | 460 | | public int ChunkIndex { get; } |
| | | 461 | | |
| | 8285 | 462 | | public byte[] Data { get; private set; } |
| | | 463 | | |
| | 4218 | 464 | | public ulong Offset { get; } |
| | | 465 | | |
| | 4173 | 466 | | public BufferedRead(int chunkIndex, ulong offset) |
| | 4173 | 467 | | { |
| | 4173 | 468 | | ChunkIndex = chunkIndex; |
| | 4173 | 469 | | Offset = offset; |
| | 4173 | 470 | | } |
| | | 471 | | |
| | | 472 | | public void Complete(byte[] data) |
| | 4086 | 473 | | { |
| | 4086 | 474 | | Data = data; |
| | 4086 | 475 | | } |
| | | 476 | | } |
| | | 477 | | } |
| | | 478 | | } |