internal static async Task AsyncModeCopyToAsync(SafeFileHandle handle, bool canSeek, long filePosition, Stream destination, int bufferSize, CancellationToken cancellationToken) { // For efficiency, we avoid creating a new task and associated state for each asynchronous read. // Instead, we create a single reusable awaitable object that will be triggered when an await completes // and reset before going again. var readAwaitable = new AsyncCopyToAwaitable(handle); // Make sure we are reading from the position that we think we are. // Only set the position in the awaitable if we can seek (e.g. not for pipes). if (canSeek) { readAwaitable._position = filePosition; } // Get the buffer to use for the copy operation, as the base CopyToAsync does. We don't try to use // _buffer here, even if it's not null, as concurrent operations are allowed, and another operation may // actually be using the buffer already. Plus, it'll be rare for _buffer to be non-null, as typically // CopyToAsync is used as the only operation performed on the stream, and the buffer is lazily initialized. // Further, typically the CopyToAsync buffer size will be larger than that used by the FileStream, such that // we'd likely be unable to use it anyway. Instead, we rent the buffer from a pool. byte[] copyBuffer = ArrayPool <byte> .Shared.Rent(bufferSize); // Allocate an Overlapped we can use repeatedly for all operations var awaitableOverlapped = new PreAllocatedOverlapped(AsyncCopyToAwaitable.s_callback, readAwaitable, copyBuffer); var cancellationReg = default(CancellationTokenRegistration); try { // Register for cancellation. We do this once for the whole copy operation, and just try to cancel // whatever read operation may currently be in progress, if there is one. It's possible the cancellation // request could come in between operations, in which case we flag that with explicit calls to ThrowIfCancellationRequested // in the read/write copy loop. if (cancellationToken.CanBeCanceled) { cancellationReg = cancellationToken.UnsafeRegister(static s =>
private async Task AsyncModeCopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { Debug.Assert(_useAsyncIO, "This implementation is for async mode only"); Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); Debug.Assert(CanRead, "_parent.CanRead"); // Make sure any pending writes have been flushed before we do a read. if (_writePos > 0) { await FlushWriteAsync(cancellationToken).ConfigureAwait(false); } // Typically CopyToAsync would be invoked as the only "read" on the stream, but it's possible some reading is // done and then the CopyToAsync is issued. For that case, see if we have any data available in the buffer. if (GetBuffer() != null) { int bufferedBytes = _readLength - _readPos; if (bufferedBytes > 0) { await destination.WriteAsync(GetBuffer(), _readPos, bufferedBytes, cancellationToken).ConfigureAwait(false); _readPos = _readLength = 0; } } // For efficiency, we avoid creating a new task and associated state for each asynchronous read. // Instead, we create a single reusable awaitable object that will be triggered when an await completes // and reset before going again. var readAwaitable = new AsyncCopyToAwaitable(this); // Make sure we are reading from the position that we think we are. // Only set the position in the awaitable if we can seek (e.g. not for pipes). bool canSeek = CanSeek; if (canSeek) { VerifyOSHandlePosition(); readAwaitable._position = _filePosition; } // Get the buffer to use for the copy operation, as the base CopyToAsync does. We don't try to use // _buffer here, even if it's not null, as concurrent operations are allowed, and another operation may // actually be using the buffer already. Plus, it'll be rare for _buffer to be non-null, as typically // CopyToAsync is used as the only operation performed on the stream, and the buffer is lazily initialized. // Further, typically the CopyToAsync buffer size will be larger than that used by the FileStream, such that // we'd likely be unable to use it anyway. Instead, we rent the buffer from a pool. byte[] copyBuffer = ArrayPool<byte>.Shared.Rent(bufferSize); bufferSize = 0; // repurpose bufferSize to be the high water mark for the buffer, to avoid an extra field in the state machine // Allocate an Overlapped we can use repeatedly for all operations var awaitableOverlapped = new PreAllocatedOverlapped(AsyncCopyToAwaitable.s_callback, readAwaitable, copyBuffer); var cancellationReg = default(CancellationTokenRegistration); try { // Register for cancellation. We do this once for the whole copy operation, and just try to cancel // whatever read operation may currently be in progress, if there is one. It's possible the cancellation // request could come in between operations, in which case we flag that with explicit calls to ThrowIfCancellationRequested // in the read/write copy loop. if (cancellationToken.CanBeCanceled) { cancellationReg = cancellationToken.Register(s => { var innerAwaitable = (AsyncCopyToAwaitable)s; unsafe { lock (innerAwaitable.CancellationLock) // synchronize with cleanup of the overlapped { if (innerAwaitable._nativeOverlapped != null) { // Try to cancel the I/O. We ignore the return value, as cancellation is opportunistic and we // don't want to fail the operation because we couldn't cancel it. Interop.Kernel32.CancelIoEx(innerAwaitable._fileStream._fileHandle, innerAwaitable._nativeOverlapped); } } } }, readAwaitable); } // Repeatedly read from this FileStream and write the results to the destination stream. while (true) { cancellationToken.ThrowIfCancellationRequested(); readAwaitable.ResetForNextOperation(); try { bool synchronousSuccess; int errorCode; unsafe { // Allocate a native overlapped for our reusable overlapped, and set position to read based on the next // desired address stored in the awaitable. (This position may be 0, if either we're at the beginning or // if the stream isn't seekable.) readAwaitable._nativeOverlapped = _fileHandle.ThreadPoolBinding.AllocateNativeOverlapped(awaitableOverlapped); if (canSeek) { readAwaitable._nativeOverlapped->OffsetLow = unchecked((int)readAwaitable._position); readAwaitable._nativeOverlapped->OffsetHigh = (int)(readAwaitable._position >> 32); } // Kick off the read. synchronousSuccess = ReadFileNative(_fileHandle, copyBuffer, 0, copyBuffer.Length, readAwaitable._nativeOverlapped, out errorCode) >= 0; } // If the operation did not synchronously succeed, it either failed or initiated the asynchronous operation. if (!synchronousSuccess) { switch (errorCode) { case ERROR_IO_PENDING: // Async operation in progress. break; case ERROR_BROKEN_PIPE: case ERROR_HANDLE_EOF: // We're at or past the end of the file, and the overlapped callback // won't be raised in these cases. Mark it as completed so that the await // below will see it as such. readAwaitable.MarkCompleted(); break; default: // Everything else is an error (and there won't be a callback). throw Win32Marshal.GetExceptionForWin32Error(errorCode); } } // Wait for the async operation (which may or may not have already completed), then throw if it failed. await readAwaitable; switch (readAwaitable._errorCode) { case 0: // success Debug.Assert(readAwaitable._numBytes >= 0, $"Expected non-negative numBytes, got {readAwaitable._numBytes}"); break; case ERROR_BROKEN_PIPE: // logically success with 0 bytes read (write end of pipe closed) case ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file) Debug.Assert(readAwaitable._numBytes == 0, $"Expected 0 bytes read, got {readAwaitable._numBytes}"); break; case Interop.Errors.ERROR_OPERATION_ABORTED: // canceled throw new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(true)); default: // error throw Win32Marshal.GetExceptionForWin32Error((int)readAwaitable._errorCode); } // Successful operation. If we got zero bytes, we're done: exit the read/write loop. int numBytesRead = (int)readAwaitable._numBytes; if (numBytesRead == 0) { break; } // Otherwise, update the read position for next time accordingly. if (canSeek) { readAwaitable._position += numBytesRead; } // (and keep track of the maximum number of bytes in the buffer we used, to avoid excessive and unnecessary // clearing of the buffer before we return it to the pool) if (numBytesRead > bufferSize) { bufferSize = numBytesRead; } } finally { // Free the resources for this read operation unsafe { NativeOverlapped* overlapped; lock (readAwaitable.CancellationLock) // just an Exchange, but we need this to be synchronized with cancellation, so using the same lock { overlapped = readAwaitable._nativeOverlapped; readAwaitable._nativeOverlapped = null; } if (overlapped != null) { _fileHandle.ThreadPoolBinding.FreeNativeOverlapped(overlapped); } } } // Write out the read data. await destination.WriteAsync(copyBuffer, 0, (int)readAwaitable._numBytes, cancellationToken).ConfigureAwait(false); } } finally { // Cleanup from the whole copy operation cancellationReg.Dispose(); awaitableOverlapped.Dispose(); Array.Clear(copyBuffer, 0, bufferSize); ArrayPool<byte>.Shared.Return(copyBuffer, clearArray: false); // Make sure the stream's current position reflects where we ended up if (!_fileHandle.IsClosed && CanSeek) { SeekCore(0, SeekOrigin.End); } } }