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);
}
}
}