From cd1698de91c452da166aad810f605fea6d93f197 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 25 Feb 2021 15:47:15 +0100 Subject: [PATCH 01/28] introduce WindowsFileStreamStrategy --- .../System.Private.CoreLib.Shared.projitems | 3 +- .../System/IO/FileStreamHelpers.Windows.cs | 206 ++++++++++++ .../IO/LegacyFileStreamStrategy.Windows.cs | 176 +---------- .../System/IO/WindowsFileStreamStrategy.cs | 298 ++++++++++++++++++ 4 files changed, 516 insertions(+), 167 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index bc84b8c6174d7..0cf2ec6493669 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1638,10 +1638,11 @@ + - + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs index 66dd3cb259e4f..02e8aa2932683 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading; using Microsoft.Win32.SafeHandles; namespace System.IO @@ -144,5 +145,210 @@ private static SafeFileHandle ValidateFileHandle(SafeFileHandle fileHandle, stri fileHandle.IsAsync = useAsyncIO; return fileHandle; } + + internal static unsafe long GetFileLength(SafeFileHandle handle, string? path) + { + Interop.Kernel32.FILE_STANDARD_INFO info; + + if (!Interop.Kernel32.GetFileInformationByHandleEx(handle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))) + { + throw Win32Marshal.GetExceptionForLastWin32Error(path); + } + + return info.EndOfFile; + } + + internal static void FlushToDisk(SafeFileHandle handle, string? path) + { + if (!Interop.Kernel32.FlushFileBuffers(handle)) + { + throw Win32Marshal.GetExceptionForLastWin32Error(path); + } + } + + internal static long Seek(SafeFileHandle handle, string? path, long offset, SeekOrigin origin, bool closeInvalidHandle = false) + { + Debug.Assert(origin >= SeekOrigin.Begin && origin <= SeekOrigin.End, "origin >= SeekOrigin.Begin && origin <= SeekOrigin.End"); + + if (!Interop.Kernel32.SetFilePointerEx(handle, offset, out long ret, (uint)origin)) + { + if (closeInvalidHandle) + { + throw Win32Marshal.GetExceptionForWin32Error(GetLastWin32ErrorAndDisposeHandleIfInvalid(handle), path); + } + else + { + throw Win32Marshal.GetExceptionForLastWin32Error(path); + } + } + + return ret; + } + + private static int GetLastWin32ErrorAndDisposeHandleIfInvalid(SafeFileHandle handle) + { + int errorCode = Marshal.GetLastWin32Error(); + + // If ERROR_INVALID_HANDLE is returned, it doesn't suffice to set + // the handle as invalid; the handle must also be closed. + // + // Marking the handle as invalid but not closing the handle + // resulted in exceptions during finalization and locked column + // values (due to invalid but unclosed handle) in SQL Win32FileStream + // scenarios. + // + // A more mainstream scenario involves accessing a file on a + // network share. ERROR_INVALID_HANDLE may occur because the network + // connection was dropped and the server closed the handle. However, + // the client side handle is still open and even valid for certain + // operations. + // + // Note that _parent.Dispose doesn't throw so we don't need to special case. + // SetHandleAsInvalid only sets _closed field to true (without + // actually closing handle) so we don't need to call that as well. + if (errorCode == Interop.Errors.ERROR_INVALID_HANDLE) + { + handle.Dispose(); + } + + return errorCode; + } + + internal static void Lock(SafeFileHandle handle, string? path, long position, long length) + { + int positionLow = unchecked((int)(position)); + int positionHigh = unchecked((int)(position >> 32)); + int lengthLow = unchecked((int)(length)); + int lengthHigh = unchecked((int)(length >> 32)); + + if (!Interop.Kernel32.LockFile(handle, positionLow, positionHigh, lengthLow, lengthHigh)) + { + throw Win32Marshal.GetExceptionForLastWin32Error(path); + } + } + + internal static void Unlock(SafeFileHandle handle, string? path, long position, long length) + { + int positionLow = unchecked((int)(position)); + int positionHigh = unchecked((int)(position >> 32)); + int lengthLow = unchecked((int)(length)); + int lengthHigh = unchecked((int)(length >> 32)); + + if (!Interop.Kernel32.UnlockFile(handle, positionLow, positionHigh, lengthLow, lengthHigh)) + { + throw Win32Marshal.GetExceptionForLastWin32Error(path); + } + } + + internal static void ValidateFileTypeForNonExtendedPaths(SafeFileHandle handle, string originalPath) + { + if (!PathInternal.IsExtended(originalPath)) + { + // To help avoid stumbling into opening COM/LPT ports by accident, we will block on non file handles unless + // we were explicitly passed a path that has \\?\. GetFullPath() will turn paths like C:\foo\con.txt into + // \\.\CON, so we'll only allow the \\?\ syntax. + + int fileType = Interop.Kernel32.GetFileType(handle); + if (fileType != Interop.Kernel32.FileTypes.FILE_TYPE_DISK) + { + int errorCode = fileType == Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN + ? Marshal.GetLastWin32Error() + : Interop.Errors.ERROR_SUCCESS; + + handle.Dispose(); + + if (errorCode != Interop.Errors.ERROR_SUCCESS) + { + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + throw new NotSupportedException(SR.NotSupported_FileStreamOnNonFiles); + } + } + } + + internal static void GetFileTypeSpecificInformation(SafeFileHandle handle, out bool canSeek, out bool isPipe) + { + int handleType = Interop.Kernel32.GetFileType(handle); + Debug.Assert(handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK + || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE + || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_CHAR, + "FileStream was passed an unknown file type!"); + + canSeek = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; + isPipe = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE; + } + + internal static unsafe void SetLength(SafeFileHandle handle, string? path, long length) + { + var eofInfo = new Interop.Kernel32.FILE_END_OF_FILE_INFO + { + EndOfFile = length + }; + + if (!Interop.Kernel32.SetFileInformationByHandle( + handle, + Interop.Kernel32.FileEndOfFileInfo, + &eofInfo, + (uint)sizeof(Interop.Kernel32.FILE_END_OF_FILE_INFO))) + { + int errorCode = Marshal.GetLastWin32Error(); + if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) + throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_FileLengthTooBig); + throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); + } + } + + // __ConsoleStream also uses this code. + internal static unsafe int ReadFileNative(SafeFileHandle handle, Span bytes, NativeOverlapped* overlapped, out int errorCode) + { + Debug.Assert(handle != null, "handle != null"); + + int r; + int numBytesRead = 0; + + fixed (byte* p = &MemoryMarshal.GetReference(bytes)) + { + r = overlapped != null ? + Interop.Kernel32.ReadFile(handle, p, bytes.Length, IntPtr.Zero, overlapped) : + Interop.Kernel32.ReadFile(handle, p, bytes.Length, out numBytesRead, IntPtr.Zero); + } + + if (r == 0) + { + errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); + return -1; + } + else + { + errorCode = 0; + return numBytesRead; + } + } + + internal static unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan buffer, NativeOverlapped* overlapped, out int errorCode) + { + Debug.Assert(handle != null, "handle != null"); + + int numBytesWritten = 0; + int r; + + fixed (byte* p = &MemoryMarshal.GetReference(buffer)) + { + r = overlapped != null ? + Interop.Kernel32.WriteFile(handle, p, buffer.Length, IntPtr.Zero, overlapped) : + Interop.Kernel32.WriteFile(handle, p, buffer.Length, out numBytesWritten, IntPtr.Zero); + } + + if (r == 0) + { + errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); + return -1; + } + else + { + errorCode = 0; + return numBytesWritten; + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs index 4eaf3d69d0e2f..b0c3bfb5353ca 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Diagnostics; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; @@ -53,28 +52,7 @@ internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy private void Init(FileMode mode, FileShare share, string originalPath, FileOptions options) { - if (!PathInternal.IsExtended(originalPath)) - { - // To help avoid stumbling into opening COM/LPT ports by accident, we will block on non file handles unless - // we were explicitly passed a path that has \\?\. GetFullPath() will turn paths like C:\foo\con.txt into - // \\.\CON, so we'll only allow the \\?\ syntax. - - int fileType = Interop.Kernel32.GetFileType(_fileHandle); - if (fileType != Interop.Kernel32.FileTypes.FILE_TYPE_DISK) - { - int errorCode = fileType == Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN - ? Marshal.GetLastWin32Error() - : Interop.Errors.ERROR_SUCCESS; - - _fileHandle.Dispose(); - - if (errorCode != Interop.Errors.ERROR_SUCCESS) - { - throw Win32Marshal.GetExceptionForWin32Error(errorCode); - } - throw new NotSupportedException(SR.NotSupported_FileStreamOnNonFiles); - } - } + FileStreamHelpers.ValidateFileTypeForNonExtendedPaths(_fileHandle, originalPath); // This is necessary for async IO using IO Completion ports via our // managed Threadpool API's. This (theoretically) calls the OS's @@ -139,11 +117,7 @@ private void InitFromHandle(SafeFileHandle handle, FileAccess access, bool useAs private void InitFromHandleImpl(SafeFileHandle handle, bool useAsyncIO) { - int handleType = Interop.Kernel32.GetFileType(handle); - Debug.Assert(handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_CHAR, "FileStream was passed an unknown file type!"); - - _canSeek = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; - _isPipe = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE; + FileStreamHelpers.GetFileTypeSpecificInformation(handle, out _canSeek, out _isPipe); // This is necessary for async IO using IO Completion ports via our // managed Threadpool API's. This calls the OS's @@ -189,11 +163,7 @@ public unsafe override long Length { get { - Interop.Kernel32.FILE_STANDARD_INFO info; - - if (!Interop.Kernel32.GetFileInformationByHandleEx(_fileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))) - throw Win32Marshal.GetExceptionForLastWin32Error(_path); - long len = info.EndOfFile; + long len = FileStreamHelpers.GetFileLength(_fileHandle, _path); // If we're writing near the end of the file, we must include our // internal buffer in our Length calculation. Don't flush because @@ -275,13 +245,7 @@ public override async ValueTask DisposeAsync() } } - private void FlushOSBuffer() - { - if (!Interop.Kernel32.FlushFileBuffers(_fileHandle)) - { - throw Win32Marshal.GetExceptionForLastWin32Error(_path); - } - } + private void FlushOSBuffer() => FileStreamHelpers.FlushToDisk(_fileHandle, _path); // Returns a task that flushes the internal write buffer private Task FlushWriteAsync(CancellationToken cancellationToken) @@ -368,22 +332,7 @@ private unsafe void SetLengthCore(long value) Debug.Assert(value >= 0, "value >= 0"); VerifyOSHandlePosition(); - var eofInfo = new Interop.Kernel32.FILE_END_OF_FILE_INFO - { - EndOfFile = value - }; - - if (!Interop.Kernel32.SetFileInformationByHandle( - _fileHandle, - Interop.Kernel32.FileEndOfFileInfo, - &eofInfo, - (uint)sizeof(Interop.Kernel32.FILE_END_OF_FILE_INFO))) - { - int errorCode = Marshal.GetLastWin32Error(); - if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) - throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_FileLengthTooBig); - throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); - } + FileStreamHelpers.SetLength(_fileHandle, _path, value); if (_filePosition > value) { @@ -594,22 +543,8 @@ public override long Seek(long offset, SeekOrigin origin) private long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin, bool closeInvalidHandle = false) { Debug.Assert(!fileHandle.IsClosed && _canSeek, "!fileHandle.IsClosed && _canSeek"); - Debug.Assert(origin >= SeekOrigin.Begin && origin <= SeekOrigin.End, "origin >= SeekOrigin.Begin && origin <= SeekOrigin.End"); - - if (!Interop.Kernel32.SetFilePointerEx(fileHandle, offset, out long ret, (uint)origin)) - { - if (closeInvalidHandle) - { - throw Win32Marshal.GetExceptionForWin32Error(GetLastWin32ErrorAndDisposeHandleIfInvalid(), _path); - } - else - { - throw Win32Marshal.GetExceptionForLastWin32Error(_path); - } - } - _filePosition = ret; - return ret; + return _filePosition = FileStreamHelpers.Seek(fileHandle, _path, offset, origin, closeInvalidHandle); } partial void OnBufferAllocated() @@ -1144,85 +1079,16 @@ private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, Cancella // __ConsoleStream also uses this code. private unsafe int ReadFileNative(SafeFileHandle handle, Span bytes, NativeOverlapped* overlapped, out int errorCode) { - Debug.Assert(handle != null, "handle != null"); Debug.Assert((_useAsyncIO && overlapped != null) || (!_useAsyncIO && overlapped == null), "Async IO and overlapped parameters inconsistent in call to ReadFileNative."); - int r; - int numBytesRead = 0; - - fixed (byte* p = &MemoryMarshal.GetReference(bytes)) - { - r = _useAsyncIO ? - Interop.Kernel32.ReadFile(handle, p, bytes.Length, IntPtr.Zero, overlapped) : - Interop.Kernel32.ReadFile(handle, p, bytes.Length, out numBytesRead, IntPtr.Zero); - } - - if (r == 0) - { - errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(); - return -1; - } - else - { - errorCode = 0; - return numBytesRead; - } + return FileStreamHelpers.ReadFileNative(handle, bytes, overlapped, out errorCode); } private unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan buffer, NativeOverlapped* overlapped, out int errorCode) { - Debug.Assert(handle != null, "handle != null"); Debug.Assert((_useAsyncIO && overlapped != null) || (!_useAsyncIO && overlapped == null), "Async IO and overlapped parameters inconsistent in call to WriteFileNative."); - int numBytesWritten = 0; - int r; - - fixed (byte* p = &MemoryMarshal.GetReference(buffer)) - { - r = _useAsyncIO ? - Interop.Kernel32.WriteFile(handle, p, buffer.Length, IntPtr.Zero, overlapped) : - Interop.Kernel32.WriteFile(handle, p, buffer.Length, out numBytesWritten, IntPtr.Zero); - } - - if (r == 0) - { - errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(); - return -1; - } - else - { - errorCode = 0; - return numBytesWritten; - } - } - - private int GetLastWin32ErrorAndDisposeHandleIfInvalid() - { - int errorCode = Marshal.GetLastWin32Error(); - - // If ERROR_INVALID_HANDLE is returned, it doesn't suffice to set - // the handle as invalid; the handle must also be closed. - // - // Marking the handle as invalid but not closing the handle - // resulted in exceptions during finalization and locked column - // values (due to invalid but unclosed handle) in SQL Win32FileStream - // scenarios. - // - // A more mainstream scenario involves accessing a file on a - // network share. ERROR_INVALID_HANDLE may occur because the network - // connection was dropped and the server closed the handle. However, - // the client side handle is still open and even valid for certain - // operations. - // - // Note that _parent.Dispose doesn't throw so we don't need to special case. - // SetHandleAsInvalid only sets _closed field to true (without - // actually closing handle) so we don't need to call that as well. - if (errorCode == Interop.Errors.ERROR_INVALID_HANDLE) - { - _fileHandle.Dispose(); - } - - return errorCode; + return FileStreamHelpers.WriteFileNative(handle, buffer, overlapped, out errorCode); } public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) @@ -1529,30 +1395,8 @@ public void UnsafeOnCompleted(Action continuation) } } - internal override void Lock(long position, long length) - { - int positionLow = unchecked((int)(position)); - int positionHigh = unchecked((int)(position >> 32)); - int lengthLow = unchecked((int)(length)); - int lengthHigh = unchecked((int)(length >> 32)); - - if (!Interop.Kernel32.LockFile(_fileHandle, positionLow, positionHigh, lengthLow, lengthHigh)) - { - throw Win32Marshal.GetExceptionForLastWin32Error(_path); - } - } - - internal override void Unlock(long position, long length) - { - int positionLow = unchecked((int)(position)); - int positionHigh = unchecked((int)(position >> 32)); - int lengthLow = unchecked((int)(length)); - int lengthHigh = unchecked((int)(length >> 32)); + internal override void Lock(long position, long length) => FileStreamHelpers.Lock(_fileHandle, _path, position, length); - if (!Interop.Kernel32.UnlockFile(_fileHandle, positionLow, positionHigh, lengthLow, lengthHigh)) - { - throw Win32Marshal.GetExceptionForLastWin32Error(_path); - } - } + internal override void Unlock(long position, long length) => FileStreamHelpers.Unlock(_fileHandle, _path, position, length); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs new file mode 100644 index 0000000000000..a5655cc3ac1e2 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs @@ -0,0 +1,298 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using System.Runtime.CompilerServices; + +namespace System.IO +{ + // this type serves some basic functionality that is common for Async and Sync Windows File Stream Strategies + internal abstract class WindowsFileStreamStrategy : FileStreamStrategy + { + // Error codes (not HRESULTS), from winerror.h + internal const int ERROR_BROKEN_PIPE = 109; + internal const int ERROR_NO_DATA = 232; + protected const int ERROR_HANDLE_EOF = 38; + protected const int ERROR_INVALID_PARAMETER = 87; + protected const int ERROR_IO_PENDING = 997; + + protected readonly SafeFileHandle _fileHandle; // only ever null if ctor throws + + /// Whether the file is opened for reading, writing, or both. + private readonly FileAccess _access; + + /// The path to the opened file. + protected readonly string? _path; + + protected long _filePosition; + + private readonly bool _canSeek; + private readonly bool _isPipe; // Whether to disable async buffering code. + + /// Whether the file stream's handle has been exposed. + protected bool _exposedHandle; + + private long _appendStart; // When appending, prevent overwriting file. + + internal WindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access) + { + _exposedHandle = true; + + InitFromHandle(handle, access, out _canSeek, out _isPipe); + + // Note: Cleaner to set the following fields in ValidateAndInitFromHandle, + // but we can't as they're readonly. + _access = access; + + // As the handle was passed in, we must set the handle field at the very end to + // avoid the finalizer closing the handle when we throw errors. + _fileHandle = handle; + } + + internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + { + string fullPath = Path.GetFullPath(path); + + _path = fullPath; + _access = access; + + _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options); + + try + { + _canSeek = true; + + Init(mode, path); + } + catch + { + // If anything goes wrong while setting up the stream, make sure we deterministically dispose + // of the opened handle. + _fileHandle.Dispose(); + _fileHandle = null!; + throw; + } + } + + public sealed override bool CanSeek => _canSeek; + + public sealed override bool CanRead => !_fileHandle.IsClosed && (_access & FileAccess.Read) != 0; + + public sealed override bool CanWrite => !_fileHandle.IsClosed && (_access & FileAccess.Write) != 0; + + public unsafe sealed override long Length => FileStreamHelpers.GetFileLength(_fileHandle, _path); + + /// Gets or sets the position within the current stream + public override long Position + { + get + { + VerifyOSHandlePosition(); + + return _filePosition; + } + set + { + Seek(value, SeekOrigin.Begin); + } + } + + internal sealed override string Name => _path ?? SR.IO_UnknownFileName; + + internal sealed override bool IsClosed => _fileHandle.IsClosed; + + internal sealed override SafeFileHandle SafeFileHandle + { + get + { + // Flushing is the responsibility of BufferedFileStreamStrategy + _exposedHandle = true; + return _fileHandle; + } + } + + // this method just disposes everything as there is no buffer here + // and we don't really need to Flush anything here + public override ValueTask DisposeAsync() + { + if (_fileHandle != null && !_fileHandle.IsClosed) + { + _fileHandle.ThreadPoolBinding?.Dispose(); + _fileHandle.Dispose(); + } + + GC.SuppressFinalize(this); // the handle is closed; nothing further for the finalizer to do + + return ValueTask.CompletedTask; + } + + // this method in the future will be called in no-buffering scenarios + internal sealed override void DisposeInternal(bool disposing) => Dispose(disposing); + + // this method is called from BufferedStream.Dispose so the content is already flushed + protected override void Dispose(bool disposing) + { + if (_fileHandle != null && !_fileHandle.IsClosed) + { + _fileHandle.ThreadPoolBinding?.Dispose(); + _fileHandle.Dispose(); + } + + // Don't set the buffer to null, to avoid a NullReferenceException + // when users have a race condition in their code (i.e. they call + // Close when calling another method on Stream like Read). + } + + public sealed override void Flush() => Flush(flushToDisk: false); // we have nothing to flush as there is no buffer here + + internal sealed override void Flush(bool flushToDisk) + { + if (flushToDisk && CanWrite) + { + FileStreamHelpers.FlushToDisk(_fileHandle, _path); + } + } + + public sealed override long Seek(long offset, SeekOrigin origin) + { + if (origin < SeekOrigin.Begin || origin > SeekOrigin.End) + throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)); + if (_fileHandle.IsClosed) throw Error.GetFileNotOpen(); + if (!CanSeek) throw Error.GetSeekNotSupported(); + + // Verify that internal position is in sync with the handle + VerifyOSHandlePosition(); + + long oldPos = _filePosition; + long pos = SeekCore(_fileHandle, offset, origin); + + // Prevent users from overwriting data in a file that was opened in + // append mode. + if (_appendStart != -1 && pos < _appendStart) + { + SeekCore(_fileHandle, oldPos, SeekOrigin.Begin); + throw new IOException(SR.IO_SeekAppendOverwrite); + } + + return pos; + } + + // This doesn't do argument checking. Necessary for SetLength, which must + // set the file pointer beyond the end of the file. This will update the + // internal position + protected long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin, bool closeInvalidHandle = false) + { + Debug.Assert(!fileHandle.IsClosed && _canSeek, "!fileHandle.IsClosed && _canSeek"); + + return _filePosition = FileStreamHelpers.Seek(fileHandle, _path, offset, origin, closeInvalidHandle); + } + + internal sealed override void Lock(long position, long length) => FileStreamHelpers.Lock(_fileHandle, _path, position, length); + + internal sealed override void Unlock(long position, long length) => FileStreamHelpers.Unlock(_fileHandle, _path, position, length); + + protected abstract void OnInitFromHandle(SafeFileHandle handle); + + protected virtual void OnInit() { } + + private void Init(FileMode mode, string originalPath) + { + FileStreamHelpers.ValidateFileTypeForNonExtendedPaths(_fileHandle, originalPath); + + OnInit(); + + // For Append mode... + if (mode == FileMode.Append) + { + _appendStart = SeekCore(_fileHandle, 0, SeekOrigin.End); + } + else + { + _appendStart = -1; + } + } + + private void InitFromHandle(SafeFileHandle handle, FileAccess access, out bool canSeek, out bool isPipe) + { +#if DEBUG + bool hadBinding = handle.ThreadPoolBinding != null; + + try + { +#endif + InitFromHandleImpl(handle, out canSeek, out isPipe); +#if DEBUG + } + catch + { + Debug.Assert(hadBinding || handle.ThreadPoolBinding == null, "We should never error out with a ThreadPoolBinding we've added"); + throw; + } +#endif + } + + private void InitFromHandleImpl(SafeFileHandle handle, out bool canSeek, out bool isPipe) + { + FileStreamHelpers.GetFileTypeSpecificInformation(handle, out canSeek, out isPipe); + + OnInitFromHandle(handle); + + if (_canSeek) + SeekCore(handle, 0, SeekOrigin.Current); + else + _filePosition = 0; + } + + public sealed override void SetLength(long value) + { + if (_appendStart != -1 && value < _appendStart) + throw new IOException(SR.IO_SetLengthAppendTruncate); + + SetLengthCore(value); + } + + // We absolutely need this method broken out so that WriteInternalCoreAsync can call + // a method without having to go through buffering code that might call FlushWrite. + protected unsafe void SetLengthCore(long value) + { + Debug.Assert(value >= 0, "value >= 0"); + VerifyOSHandlePosition(); + + FileStreamHelpers.SetLength(_fileHandle, _path, value); + + if (_filePosition > value) + { + SeekCore(_fileHandle, 0, SeekOrigin.End); + } + } + + /// + /// Verify that the actual position of the OS's handle equals what we expect it to. + /// This will fail if someone else moved the UnixFileStream's handle or if + /// our position updating code is incorrect. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void VerifyOSHandlePosition() + { + bool verifyPosition = _exposedHandle; // in release, only verify if we've given out the handle such that someone else could be manipulating it +#if DEBUG + verifyPosition = true; // in debug, always make sure our position matches what the OS says it should be +#endif + if (verifyPosition && CanSeek) + { + long oldPos = _filePosition; // SeekCore will override the current _position, so save it now + long curPos = SeekCore(_fileHandle, 0, SeekOrigin.Current); + if (oldPos != curPos) + { + // For reads, this is non-fatal but we still could have returned corrupted + // data in some cases, so discard the internal buffer. For writes, + // this is a problem; discard the buffer and error out. + + throw new IOException(SR.IO_FileStreamHandlePosition); + } + } + } + } +} From ad5b455a2cf8f6b1440f0eb5a9d59e498ec7d672 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 25 Feb 2021 16:55:39 +0100 Subject: [PATCH 02/28] introduce SyncWindowsFileStreamStrategy --- .../System.Private.CoreLib.Shared.projitems | 1 + .../System/IO/FileStreamHelpers.Windows.cs | 2 +- .../IO/SyncWindowsFileStreamStrategy.cs | 159 ++++++++++++++++++ 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 0cf2ec6493669..05fcf47e8ae21 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1642,6 +1642,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs index 02e8aa2932683..cb1380f69bd77 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs @@ -59,7 +59,7 @@ internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsyn return handle.IsAsync ?? !IsHandleSynchronous(handle, ignoreInvalid: true) ?? defaultIsAsync; } - private static unsafe bool? IsHandleSynchronous(SafeFileHandle fileHandle, bool ignoreInvalid) + internal static unsafe bool? IsHandleSynchronous(SafeFileHandle fileHandle, bool ignoreInvalid) { if (fileHandle.IsInvalid) return null; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs new file mode 100644 index 0000000000000..99a64e961cc12 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + internal sealed class SyncWindowsFileStreamStrategy : WindowsFileStreamStrategy + { + internal SyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access) : base(handle, access) + { + } + + internal SyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + : base(path, mode, access, share, options) + { + } + + internal override bool IsAsync => false; + + protected override void OnInitFromHandle(SafeFileHandle handle) + { + // As we can accurately check the handle type when we have access to NtQueryInformationFile we don't need to skip for + // any particular file handle type. + + // If the handle was passed in without an explicit async setting, we already looked it up in GetDefaultIsAsync + if (!handle.IsAsync.HasValue) + return; + + // If we can't check the handle, just assume it is ok. + if (!(FileStreamHelpers.IsHandleSynchronous(handle, ignoreInvalid: false) ?? true)) + throw new ArgumentException(SR.Arg_HandleNotSync, nameof(handle)); + } + + public override int Read(byte[] buffer, int offset, int count) => ReadSpan(new Span(buffer, offset, count)); + + public override int Read(Span buffer) => ReadSpan(buffer); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Read is invoked asynchronously. But we can do so using the base Stream's internal helper + // that bypasses delegating to BeginRead, since we already know this is FileStream rather + // than something derived from it and what our BeginRead implementation is going to do. + return (Task)BeginReadInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Read is invoked asynchronously. But if we have a byte[], we can do so using the base Stream's + // internal helper that bypasses delegating to BeginRead, since we already know this is FileStream + // rather than something derived from it and what our BeginRead implementation is going to do. + return MemoryMarshal.TryGetArray(buffer, out ArraySegment segment) ? + new ValueTask((Task)BeginReadInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : + base.ReadAsync(buffer, cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + => WriteSpan(new ReadOnlySpan(buffer, offset, count)); + + public override void Write(ReadOnlySpan buffer) + { + if (_fileHandle.IsClosed) + { + throw Error.GetFileNotOpen(); + } + + WriteSpan(buffer); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Write is invoked asynchronously. But we can do so using the base Stream's internal helper + // that bypasses delegating to BeginWrite, since we already know this is FileStream rather + // than something derived from it and what our BeginWrite implementation is going to do. + return (Task)BeginWriteInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Write is invoked asynchronously. But if we have a byte[], we can do so using the base Stream's + // internal helper that bypasses delegating to BeginWrite, since we already know this is FileStream + // rather than something derived from it and what our BeginWrite implementation is going to do. + return MemoryMarshal.TryGetArray(buffer, out ArraySegment segment) ? + new ValueTask((Task)BeginWriteInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : + base.WriteAsync(buffer, cancellationToken); + } + + private unsafe int ReadSpan(Span destination) + { + Debug.Assert(CanRead, "BufferedStream has already verified that"); + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + + // Make sure we are reading from the right spot + VerifyOSHandlePosition(); + + int r = FileStreamHelpers.ReadFileNative(_fileHandle, destination, null, out int errorCode); + + if (r == -1) + { + // For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe. + if (errorCode == ERROR_BROKEN_PIPE) + { + r = 0; + } + else + { + if (errorCode == ERROR_INVALID_PARAMETER) + throw new ArgumentException(SR.Arg_HandleNotSync, "_fileHandle"); + + throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); + } + } + Debug.Assert(r >= 0, "FileStream's ReadNative is likely broken."); + _filePosition += r; + + return r; + } + + private unsafe void WriteSpan(ReadOnlySpan source) + { + Debug.Assert(CanWrite, "BufferedStream has already verified that"); + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + + // Make sure we are writing to the position that we think we are + VerifyOSHandlePosition(); + + int r = FileStreamHelpers.WriteFileNative(_fileHandle, source, null, out int errorCode); + + if (r == -1) + { + // For pipes, ERROR_NO_DATA is not an error, but the pipe is closing. + if (errorCode == ERROR_NO_DATA) + { + r = 0; + } + else + { + // ERROR_INVALID_PARAMETER may be returned for writes + // where the position is too large or for synchronous writes + // to a handle opened asynchronously. + if (errorCode == ERROR_INVALID_PARAMETER) + throw new IOException(SR.IO_FileTooLongOrHandleNotSync); + throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); + } + } + Debug.Assert(r >= 0, "FileStream's WriteCore is likely broken."); + _filePosition += r; + return; + } + } +} From a5ff09c8e3a4d2aa4351c90207afc94094757b8f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 25 Feb 2021 17:00:23 +0100 Subject: [PATCH 03/28] introduce AsyncWindowsFileStreamStrategy --- .../src/System/Threading/Overlapped.cs | 4 + .../System.Private.CoreLib.Shared.projitems | 3 +- .../IO/AsyncWindowsFileStreamStrategy.cs | 396 ++++++++++++++++++ .../IO/FileStreamCompletionSource.Win32.cs | 257 ------------ .../IO/FileStreamCompletionSource.Windows.cs | 267 ++++++++++++ .../System/IO/FileStreamHelpers.Windows.cs | 246 +++++++++++ .../IO/LegacyFileStreamStrategy.Windows.cs | 252 +---------- 7 files changed, 928 insertions(+), 497 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs delete mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Win32.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs diff --git a/src/coreclr/System.Private.CoreLib/src/System/Threading/Overlapped.cs b/src/coreclr/System.Private.CoreLib/src/System/Threading/Overlapped.cs index 728954377dff8..53abea5f0d16d 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Threading/Overlapped.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Threading/Overlapped.cs @@ -132,6 +132,8 @@ internal sealed unsafe class OverlappedData return AllocateNativeOverlapped(); } + internal bool IsUserObject(byte[]? buffer) => ReferenceEquals(_userObject, buffer); + [MethodImpl(MethodImplOptions.InternalCall)] private extern NativeOverlapped* AllocateNativeOverlapped(); @@ -258,6 +260,8 @@ public static unsafe void Free(NativeOverlapped* nativeOverlappedPtr) OverlappedData.GetOverlappedFromNative(nativeOverlappedPtr)._overlapped._overlappedData = null; OverlappedData.FreeNativeOverlapped(nativeOverlappedPtr); } + + internal bool IsUserObject(byte[]? buffer) => _overlappedData!.IsUserObject(buffer); } #endregion class Overlapped diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 05fcf47e8ae21..95cf4d04ba169 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1634,10 +1634,11 @@ + - + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs new file mode 100644 index 0000000000000..9d21309fd2107 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs @@ -0,0 +1,396 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + internal sealed partial class AsyncWindowsFileStreamStrategy : WindowsFileStreamStrategy, IFileStreamCompletionSourceStrategy + { + private PreAllocatedOverlapped? _preallocatedOverlapped; // optimization for async ops to avoid per-op allocations + private FileStreamCompletionSource? _currentOverlappedOwner; // async op currently using the preallocated overlapped + + internal AsyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access) + : base(handle, access) + { + } + + internal AsyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + : base(path, mode, access, share, options) + { + } + + internal override bool IsAsync => true; + + public override ValueTask DisposeAsync() + { + // the order matters, let the base class Dispose handle first + ValueTask result = base.DisposeAsync(); + Debug.Assert(result.IsCompleted, "the method must be sync, as it performs no flushing"); + + _preallocatedOverlapped?.Dispose(); + + return result; + } + + protected override void Dispose(bool disposing) + { + // the order matters, let the base class Dispose handle first + base.Dispose(disposing); + + _preallocatedOverlapped?.Dispose(); + } + + protected override void OnInitFromHandle(SafeFileHandle handle) + { + // This is necessary for async IO using IO Completion ports via our + // managed Threadpool API's. This calls the OS's + // BindIoCompletionCallback method, and passes in a stub for the + // LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped + // struct for this request and gets a delegate to a managed callback + // from there, which it then calls on a threadpool thread. (We allocate + // our native OVERLAPPED structs 2 pointers too large and store EE + // state & a handle to a delegate there.) + // + // If, however, we've already bound this file handle to our completion port, + // don't try to bind it again because it will fail. A handle can only be + // bound to a single completion port at a time. + if (!(handle.IsAsync ?? false)) + { + try + { + handle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(handle); + } + catch (Exception ex) + { + // If you passed in a synchronous handle and told us to use + // it asynchronously, throw here. + throw new ArgumentException(SR.Arg_HandleNotAsync, nameof(handle), ex); + } + } + } + + protected override void OnInit() + { + // This is necessary for async IO using IO Completion ports via our + // managed Threadpool API's. This (theoretically) calls the OS's + // BindIoCompletionCallback method, and passes in a stub for the + // LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped + // struct for this request and gets a delegate to a managed callback + // from there, which it then calls on a threadpool thread. (We allocate + // our native OVERLAPPED structs 2 pointers too large and store EE state + // & GC handles there, one to an IAsyncResult, the other to a delegate.) + try + { + _fileHandle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(_fileHandle); + } + catch (ArgumentException ex) + { + throw new IOException(SR.IO_BindHandleFailed, ex); + } + finally + { + if (_fileHandle.ThreadPoolBinding == null) + { + // We should close the handle so that the handle is not open until SafeFileHandle GC + Debug.Assert(!_exposedHandle, "Are we closing handle that we exposed/not own, how?"); + _fileHandle.Dispose(); + } + } + } + + // called by BufferedStream. TODO: find a cleaner solution + internal void OnBufferAllocated(byte[] buffer) + { + Debug.Assert(buffer != null); + Debug.Assert(_preallocatedOverlapped == null); + + _preallocatedOverlapped = new PreAllocatedOverlapped(FileStreamCompletionSource.s_ioCallback, this, buffer); + } + + SafeFileHandle IFileStreamCompletionSourceStrategy.FileHandle => _fileHandle; + + FileStreamCompletionSource? IFileStreamCompletionSourceStrategy.CurrentOverlappedOwner => _currentOverlappedOwner; + + FileStreamCompletionSource? IFileStreamCompletionSourceStrategy.CompareExchangeCurrentOverlappedOwner(FileStreamCompletionSource? newSource, FileStreamCompletionSource? existingSource) + => Interlocked.CompareExchange(ref _currentOverlappedOwner, newSource, existingSource); + + public override int Read(byte[] buffer, int offset, int count) + => ReadAsyncInternal(new Memory(buffer, offset, count)).GetAwaiter().GetResult(); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => ReadAsyncInternal(new Memory(buffer, offset, count), cancellationToken); + + public override ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) + => new ValueTask(ReadAsyncInternal(destination, cancellationToken)); + + private unsafe Task ReadAsyncInternal(Memory destination, CancellationToken cancellationToken = default) + { + Debug.Assert(CanRead, "BufferedStream has already verified that"); + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + + // Create and store async stream class library specific data in the async result + FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, _preallocatedOverlapped, 0, destination); + NativeOverlapped* intOverlapped = completionSource.Overlapped; + + // Calculate position in the file we should be at after the read is done + if (CanSeek) + { + long len = Length; + + // Make sure we are reading from the position that we think we are + VerifyOSHandlePosition(); + + if (_filePosition + destination.Length > len) + { + if (_filePosition <= len) + { + destination = destination.Slice(0, (int)(len - _filePosition)); + } + else + { + destination = default; + } + } + + // Now set the position to read from in the NativeOverlapped struct + // For pipes, we should leave the offset fields set to 0. + intOverlapped->OffsetLow = unchecked((int)_filePosition); + intOverlapped->OffsetHigh = (int)(_filePosition >> 32); + + // When using overlapped IO, the OS is not supposed to + // touch the file pointer location at all. We will adjust it + // ourselves. This isn't threadsafe. + + // WriteFile should not update the file pointer when writing + // in overlapped mode, according to MSDN. But it does update + // the file pointer when writing to a UNC path! + // So changed the code below to seek to an absolute + // location, not a relative one. ReadFile seems consistent though. + SeekCore(_fileHandle, destination.Length, SeekOrigin.Current); + } + + // queue an async ReadFile operation and pass in a packed overlapped + int r = FileStreamHelpers.ReadFileNative(_fileHandle, destination.Span, intOverlapped, out int errorCode); + + // ReadFile, the OS version, will return 0 on failure. But + // my ReadFileNative wrapper returns -1. My wrapper will return + // the following: + // On error, r==-1. + // On async requests that are still pending, r==-1 w/ errorCode==ERROR_IO_PENDING + // on async requests that completed sequentially, r==0 + // You will NEVER RELIABLY be able to get the number of bytes + // read back from this call when using overlapped structures! You must + // not pass in a non-null lpNumBytesRead to ReadFile when using + // overlapped structures! This is by design NT behavior. + if (r == -1) + { + // For pipes, when they hit EOF, they will come here. + if (errorCode == ERROR_BROKEN_PIPE) + { + // Not an error, but EOF. AsyncFSCallback will NOT be + // called. Call the user callback here. + + // We clear the overlapped status bit for this special case. + // Failure to do so looks like we are freeing a pending overlapped later. + intOverlapped->InternalLow = IntPtr.Zero; + completionSource.SetCompletedSynchronously(0); + } + else if (errorCode != ERROR_IO_PENDING) + { + if (!_fileHandle.IsClosed && CanSeek) // Update Position - It could be anywhere. + { + SeekCore(_fileHandle, 0, SeekOrigin.Current); + } + + completionSource.ReleaseNativeResource(); + + if (errorCode == ERROR_HANDLE_EOF) + { + throw Error.GetEndOfFile(); + } + else + { + throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); + } + } + else if (cancellationToken.CanBeCanceled) // ERROR_IO_PENDING + { + // Only once the IO is pending do we register for cancellation + completionSource.RegisterForCancellation(cancellationToken); + } + } + else + { + // Due to a workaround for a race condition in NT's ReadFile & + // WriteFile routines, we will always be returning 0 from ReadFileNative + // when we do async IO instead of the number of bytes read, + // irregardless of whether the operation completed + // synchronously or asynchronously. We absolutely must not + // set asyncResult._numBytes here, since will never have correct + // results. + } + + return completionSource.Task; + } + + public override void Write(byte[] buffer, int offset, int count) + => WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), CancellationToken.None).AsTask().GetAwaiter().GetResult(); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + => WriteAsyncInternal(buffer, cancellationToken); + + private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationToken cancellationToken) + => new ValueTask(WriteAsyncInternalCore(source, cancellationToken)); + + private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, CancellationToken cancellationToken) + { + Debug.Assert(CanWrite, "BufferedStream has already verified that"); + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + + // Create and store async stream class library specific data in the async result + FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, _preallocatedOverlapped, 0, source); + NativeOverlapped* intOverlapped = completionSource.Overlapped; + + if (CanSeek) + { + // Make sure we set the length of the file appropriately. + long len = Length; + + // Make sure we are writing to the position that we think we are + VerifyOSHandlePosition(); + + if (_filePosition + source.Length > len) + { + SetLengthCore(_filePosition + source.Length); + } + + // Now set the position to read from in the NativeOverlapped struct + // For pipes, we should leave the offset fields set to 0. + intOverlapped->OffsetLow = (int)_filePosition; + intOverlapped->OffsetHigh = (int)(_filePosition >> 32); + + // When using overlapped IO, the OS is not supposed to + // touch the file pointer location at all. We will adjust it + // ourselves. This isn't threadsafe. + SeekCore(_fileHandle, source.Length, SeekOrigin.Current); + } + + // queue an async WriteFile operation and pass in a packed overlapped + int r = FileStreamHelpers.WriteFileNative(_fileHandle, source.Span, intOverlapped, out int errorCode); + + // WriteFile, the OS version, will return 0 on failure. But + // my WriteFileNative wrapper returns -1. My wrapper will return + // the following: + // On error, r==-1. + // On async requests that are still pending, r==-1 w/ errorCode==ERROR_IO_PENDING + // On async requests that completed sequentially, r==0 + // You will NEVER RELIABLY be able to get the number of bytes + // written back from this call when using overlapped IO! You must + // not pass in a non-null lpNumBytesWritten to WriteFile when using + // overlapped structures! This is ByDesign NT behavior. + if (r == -1) + { + // For pipes, when they are closed on the other side, they will come here. + if (errorCode == ERROR_NO_DATA) + { + // Not an error, but EOF. AsyncFSCallback will NOT be called. + // Completing TCS and return cached task allowing the GC to collect TCS. + completionSource.SetCompletedSynchronously(0); + return Task.CompletedTask; + } + else if (errorCode != ERROR_IO_PENDING) + { + if (!_fileHandle.IsClosed && CanSeek) // Update Position - It could be anywhere. + { + SeekCore(_fileHandle, 0, SeekOrigin.Current); + } + + completionSource.ReleaseNativeResource(); + + if (errorCode == ERROR_HANDLE_EOF) + { + throw Error.GetEndOfFile(); + } + else + { + throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); + } + } + else if (cancellationToken.CanBeCanceled) // ERROR_IO_PENDING + { + // Only once the IO is pending do we register for cancellation + completionSource.RegisterForCancellation(cancellationToken); + } + } + else + { + // Due to a workaround for a race condition in NT's ReadFile & + // WriteFile routines, we will always be returning 0 from WriteFileNative + // when we do async IO instead of the number of bytes written, + // irregardless of whether the operation completed + // synchronously or asynchronously. We absolutely must not + // set asyncResult._numBytes here, since will never have correct + // results. + } + + return completionSource.Task; + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + ValidateCopyToArguments(destination, bufferSize); + + // Fail if the file was closed + if (_fileHandle.IsClosed) + { + throw Error.GetFileNotOpen(); + } + if (!CanRead) + { + throw Error.GetReadNotSupported(); + } + + // Bail early for cancellation if cancellation has been requested + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return AsyncModeCopyToAsync(destination, bufferSize, cancellationToken); + } + + private async Task AsyncModeCopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + Debug.Assert(CanRead, "_parent.CanRead"); + + bool canSeek = CanSeek; + if (canSeek) + { + VerifyOSHandlePosition(); + } + + try + { +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await FileStreamHelpers.AsyncModeCopyToAsync(_fileHandle, _path, canSeek, _filePosition, destination, bufferSize, cancellationToken); +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + } + finally + { + // Make sure the stream's current position reflects where we ended up + if (!_fileHandle.IsClosed && CanSeek) + { + SeekCore(_fileHandle, 0, SeekOrigin.End); + } + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Win32.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Win32.cs deleted file mode 100644 index c7b56290ca7ee..0000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Win32.cs +++ /dev/null @@ -1,257 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Buffers; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace System.IO -{ - internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy - { - // This is an internal object extending TaskCompletionSource with fields - // for all of the relevant data necessary to complete the IO operation. - // This is used by IOCallback and all of the async methods. - private unsafe class FileStreamCompletionSource : TaskCompletionSource - { - private const long NoResult = 0; - private const long ResultSuccess = (long)1 << 32; - private const long ResultError = (long)2 << 32; - private const long RegisteringCancellation = (long)4 << 32; - private const long CompletedCallback = (long)8 << 32; - private const ulong ResultMask = ((ulong)uint.MaxValue) << 32; - - private static Action? s_cancelCallback; - - private readonly LegacyFileStreamStrategy _stream; - private readonly int _numBufferedBytes; - private CancellationTokenRegistration _cancellationRegistration; -#if DEBUG - private bool _cancellationHasBeenRegistered; -#endif - private NativeOverlapped* _overlapped; // Overlapped class responsible for operations in progress when an appdomain unload occurs - private long _result; // Using long since this needs to be used in Interlocked APIs - - // Using RunContinuationsAsynchronously for compat reasons (old API used Task.Factory.StartNew for continuations) - protected FileStreamCompletionSource(LegacyFileStreamStrategy stream, int numBufferedBytes, byte[]? bytes) - : base(TaskCreationOptions.RunContinuationsAsynchronously) - { - _numBufferedBytes = numBufferedBytes; - _stream = stream; - _result = NoResult; - - // Create the native overlapped. We try to use the preallocated overlapped if possible: it's possible if the byte - // buffer is null (there's nothing to pin) or the same one that's associated with the preallocated overlapped (and - // thus is already pinned) and if no one else is currently using the preallocated overlapped. This is the fast-path - // for cases where the user-provided buffer is smaller than the FileStream's buffer (such that the FileStream's - // buffer is used) and where operations on the FileStream are not being performed concurrently. - Debug.Assert(bytes == null || ReferenceEquals(bytes, _stream._buffer)); - - // The _preallocatedOverlapped is null if the internal buffer was never created, so we check for - // a non-null bytes before using the stream's _preallocatedOverlapped - _overlapped = bytes != null && _stream.CompareExchangeCurrentOverlappedOwner(this, null) == null ? - _stream._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(_stream._preallocatedOverlapped!) : // allocated when buffer was created, and buffer is non-null - _stream._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(s_ioCallback, this, bytes); - Debug.Assert(_overlapped != null, "AllocateNativeOverlapped returned null"); - } - - internal NativeOverlapped* Overlapped => _overlapped; - - public void SetCompletedSynchronously(int numBytes) - { - ReleaseNativeResource(); - TrySetResult(numBytes + _numBufferedBytes); - } - - public void RegisterForCancellation(CancellationToken cancellationToken) - { -#if DEBUG - Debug.Assert(cancellationToken.CanBeCanceled); - Debug.Assert(!_cancellationHasBeenRegistered, "Cannot register for cancellation twice"); - _cancellationHasBeenRegistered = true; -#endif - - // Quick check to make sure the IO hasn't completed - if (_overlapped != null) - { - Action? cancelCallback = s_cancelCallback ??= Cancel; - - // Register the cancellation only if the IO hasn't completed - long packedResult = Interlocked.CompareExchange(ref _result, RegisteringCancellation, NoResult); - if (packedResult == NoResult) - { - _cancellationRegistration = cancellationToken.UnsafeRegister(cancelCallback, this); - - // Switch the result, just in case IO completed while we were setting the registration - packedResult = Interlocked.Exchange(ref _result, NoResult); - } - else if (packedResult != CompletedCallback) - { - // Failed to set the result, IO is in the process of completing - // Attempt to take the packed result - packedResult = Interlocked.Exchange(ref _result, NoResult); - } - - // If we have a callback that needs to be completed - if ((packedResult != NoResult) && (packedResult != CompletedCallback) && (packedResult != RegisteringCancellation)) - { - CompleteCallback((ulong)packedResult); - } - } - } - - internal virtual void ReleaseNativeResource() - { - // Ensure that cancellation has been completed and cleaned up. - _cancellationRegistration.Dispose(); - - // Free the overlapped. - // NOTE: The cancellation must *NOT* be running at this point, or it may observe freed memory - // (this is why we disposed the registration above). - if (_overlapped != null) - { - _stream._fileHandle.ThreadPoolBinding!.FreeNativeOverlapped(_overlapped); - _overlapped = null; - } - - // Ensure we're no longer set as the current completion source (we may not have been to begin with). - // Only one operation at a time is eligible to use the preallocated overlapped, - _stream.CompareExchangeCurrentOverlappedOwner(null, this); - } - - // When doing IO asynchronously (i.e. _isAsync==true), this callback is - // called by a free thread in the threadpool when the IO operation - // completes. - internal static void IOCallback(uint errorCode, uint numBytes, NativeOverlapped* pOverlapped) - { - // Extract the completion source from the overlapped. The state in the overlapped - // will either be a FileStream (in the case where the preallocated overlapped was used), - // in which case the operation being completed is its _currentOverlappedOwner, or it'll - // be directly the FileStreamCompletionSource that's completing (in the case where the preallocated - // overlapped was already in use by another operation). - object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped); - Debug.Assert(state is LegacyFileStreamStrategy || state is FileStreamCompletionSource); - FileStreamCompletionSource completionSource = state is LegacyFileStreamStrategy fs ? - fs._currentOverlappedOwner! : // must be owned - (FileStreamCompletionSource)state!; - Debug.Assert(completionSource != null); - Debug.Assert(completionSource._overlapped == pOverlapped, "Overlaps don't match"); - - // Handle reading from & writing to closed pipes. While I'm not sure - // this is entirely necessary anymore, maybe it's possible for - // an async read on a pipe to be issued and then the pipe is closed, - // returning this error. This may very well be necessary. - ulong packedResult; - if (errorCode != 0 && errorCode != ERROR_BROKEN_PIPE && errorCode != ERROR_NO_DATA) - { - packedResult = ((ulong)ResultError | errorCode); - } - else - { - packedResult = ((ulong)ResultSuccess | numBytes); - } - - // Stow the result so that other threads can observe it - // And, if no other thread is registering cancellation, continue - if (NoResult == Interlocked.Exchange(ref completionSource._result, (long)packedResult)) - { - // Successfully set the state, attempt to take back the callback - if (Interlocked.Exchange(ref completionSource._result, CompletedCallback) != NoResult) - { - // Successfully got the callback, finish the callback - completionSource.CompleteCallback(packedResult); - } - // else: Some other thread stole the result, so now it is responsible to finish the callback - } - // else: Some other thread is registering a cancellation, so it *must* finish the callback - } - - private void CompleteCallback(ulong packedResult) - { - // Free up the native resource and cancellation registration - CancellationToken cancellationToken = _cancellationRegistration.Token; // access before disposing registration - ReleaseNativeResource(); - - // Unpack the result and send it to the user - long result = (long)(packedResult & ResultMask); - if (result == ResultError) - { - int errorCode = unchecked((int)(packedResult & uint.MaxValue)); - if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED) - { - TrySetCanceled(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(true)); - } - else - { - Exception e = Win32Marshal.GetExceptionForWin32Error(errorCode); - e.SetCurrentStackTrace(); - TrySetException(e); - } - } - else - { - Debug.Assert(result == ResultSuccess, "Unknown result"); - TrySetResult((int)(packedResult & uint.MaxValue) + _numBufferedBytes); - } - } - - private static void Cancel(object? state) - { - // WARNING: This may potentially be called under a lock (during cancellation registration) - - Debug.Assert(state is FileStreamCompletionSource, "Unknown state passed to cancellation"); - FileStreamCompletionSource completionSource = (FileStreamCompletionSource)state; - Debug.Assert(completionSource._overlapped != null && !completionSource.Task.IsCompleted, "IO should not have completed yet"); - - // If the handle is still valid, attempt to cancel the IO - if (!completionSource._stream._fileHandle.IsInvalid && - !Interop.Kernel32.CancelIoEx(completionSource._stream._fileHandle, completionSource._overlapped)) - { - int errorCode = Marshal.GetLastWin32Error(); - - // ERROR_NOT_FOUND is returned if CancelIoEx cannot find the request to cancel. - // This probably means that the IO operation has completed. - if (errorCode != Interop.Errors.ERROR_NOT_FOUND) - { - throw Win32Marshal.GetExceptionForWin32Error(errorCode); - } - } - } - - public static FileStreamCompletionSource Create(LegacyFileStreamStrategy stream, int numBufferedBytesRead, ReadOnlyMemory memory) - { - // If the memory passed in is the stream's internal buffer, we can use the base FileStreamCompletionSource, - // which has a PreAllocatedOverlapped with the memory already pinned. Otherwise, we use the derived - // MemoryFileStreamCompletionSource, which Retains the memory, which will result in less pinning in the case - // where the underlying memory is backed by pre-pinned buffers. - return MemoryMarshal.TryGetArray(memory, out ArraySegment buffer) && ReferenceEquals(buffer.Array, stream._buffer) ? - new FileStreamCompletionSource(stream, numBufferedBytesRead, buffer.Array) : - new MemoryFileStreamCompletionSource(stream, numBufferedBytesRead, memory); - } - } - - /// - /// Extends with to support disposing of a - /// when the operation has completed. This should only be used - /// when memory doesn't wrap a byte[]. - /// - private sealed class MemoryFileStreamCompletionSource : FileStreamCompletionSource - { - private MemoryHandle _handle; // mutable struct; do not make this readonly - - internal MemoryFileStreamCompletionSource(LegacyFileStreamStrategy stream, int numBufferedBytes, ReadOnlyMemory memory) : - base(stream, numBufferedBytes, bytes: null) // this type handles the pinning, so null is passed for bytes - { - _handle = memory.Pin(); - } - - internal override void ReleaseNativeResource() - { - _handle.Dispose(); - base.ReleaseNativeResource(); - } - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs new file mode 100644 index 0000000000000..eeea50a56f5a6 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs @@ -0,0 +1,267 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + // to avoid code duplicaiton of FileStreamCompletionSource for LegacyFileStreamStrategy and AsyncWindowsFileStreamStrategy + // we have created the following interface that is a common contract for both of them + internal interface IFileStreamCompletionSourceStrategy + { + SafeFileHandle FileHandle { get; } + + FileStreamCompletionSource? CurrentOverlappedOwner { get; } + + FileStreamCompletionSource? CompareExchangeCurrentOverlappedOwner(FileStreamCompletionSource? newSource, FileStreamCompletionSource? existingSource); + } + + // This is an internal object extending TaskCompletionSource with fields + // for all of the relevant data necessary to complete the IO operation. + // This is used by IOCallback and all of the async methods. + internal unsafe class FileStreamCompletionSource : TaskCompletionSource + { + private const long NoResult = 0; + private const long ResultSuccess = (long)1 << 32; + private const long ResultError = (long)2 << 32; + private const long RegisteringCancellation = (long)4 << 32; + private const long CompletedCallback = (long)8 << 32; + private const ulong ResultMask = ((ulong)uint.MaxValue) << 32; + private const int ERROR_BROKEN_PIPE = 109; + private const int ERROR_NO_DATA = 232; + + internal static readonly unsafe IOCompletionCallback s_ioCallback = IOCallback; + + private static Action? s_cancelCallback; + + private readonly IFileStreamCompletionSourceStrategy _strategy; + private readonly int _numBufferedBytes; + private CancellationTokenRegistration _cancellationRegistration; +#if DEBUG + private bool _cancellationHasBeenRegistered; +#endif + internal NativeOverlapped* _overlapped; // Overlapped class responsible for operations in progress when an appdomain unload occurs + private long _result; // Using long since this needs to be used in Interlocked APIs + + // Using RunContinuationsAsynchronously for compat reasons (old API used Task.Factory.StartNew for continuations) + internal FileStreamCompletionSource(IFileStreamCompletionSourceStrategy strategy, PreAllocatedOverlapped? preallocatedOverlapped, + int numBufferedBytes, byte[]? bytes) : base(TaskCreationOptions.RunContinuationsAsynchronously) + { + _numBufferedBytes = numBufferedBytes; + _strategy = strategy; + _result = NoResult; + + // The _preallocatedOverlapped is null if the internal buffer was never created, so we check for + // a non-null bytes before using the stream's _preallocatedOverlapped + _overlapped = bytes != null && strategy.CompareExchangeCurrentOverlappedOwner(this, null) == null ? + strategy.FileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(preallocatedOverlapped!) : // allocated when buffer was created, and buffer is non-null + strategy.FileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(s_ioCallback, this, bytes); + Debug.Assert(_overlapped != null, "AllocateNativeOverlapped returned null"); + } + + internal NativeOverlapped* Overlapped => _overlapped; + + public void SetCompletedSynchronously(int numBytes) + { + ReleaseNativeResource(); + TrySetResult(numBytes + _numBufferedBytes); + } + + public void RegisterForCancellation(CancellationToken cancellationToken) + { +#if DEBUG + Debug.Assert(cancellationToken.CanBeCanceled); + Debug.Assert(!_cancellationHasBeenRegistered, "Cannot register for cancellation twice"); + _cancellationHasBeenRegistered = true; +#endif + + // Quick check to make sure the IO hasn't completed + if (_overlapped != null) + { + Action? cancelCallback = s_cancelCallback ??= Cancel; + + // Register the cancellation only if the IO hasn't completed + long packedResult = Interlocked.CompareExchange(ref _result, RegisteringCancellation, NoResult); + if (packedResult == NoResult) + { + _cancellationRegistration = cancellationToken.UnsafeRegister(cancelCallback, this); + + // Switch the result, just in case IO completed while we were setting the registration + packedResult = Interlocked.Exchange(ref _result, NoResult); + } + else if (packedResult != CompletedCallback) + { + // Failed to set the result, IO is in the process of completing + // Attempt to take the packed result + packedResult = Interlocked.Exchange(ref _result, NoResult); + } + + // If we have a callback that needs to be completed + if ((packedResult != NoResult) && (packedResult != CompletedCallback) && (packedResult != RegisteringCancellation)) + { + CompleteCallback((ulong)packedResult); + } + } + } + + internal virtual void ReleaseNativeResource() + { + // Ensure that cancellation has been completed and cleaned up. + _cancellationRegistration.Dispose(); + + // Free the overlapped. + // NOTE: The cancellation must *NOT* be running at this point, or it may observe freed memory + // (this is why we disposed the registration above). + if (_overlapped != null) + { + _strategy.FileHandle.ThreadPoolBinding!.FreeNativeOverlapped(_overlapped); + _overlapped = null; + } + + // Ensure we're no longer set as the current completion source (we may not have been to begin with). + // Only one operation at a time is eligible to use the preallocated overlapped, + _strategy.CompareExchangeCurrentOverlappedOwner(null, this); + } + + // When doing IO asynchronously (i.e. _isAsync==true), this callback is + // called by a free thread in the threadpool when the IO operation + // completes. + internal static void IOCallback(uint errorCode, uint numBytes, NativeOverlapped* pOverlapped) + { + // Extract the completion source from the overlapped. The state in the overlapped + // will either be a FileStreamStrategy (in the case where the preallocated overlapped was used), + // in which case the operation being completed is its _currentOverlappedOwner, or it'll + // be directly the FileStreamCompletionSource that's completing (in the case where the preallocated + // overlapped was already in use by another operation). + object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped); + Debug.Assert(state is IFileStreamCompletionSourceStrategy || state is FileStreamCompletionSource); + FileStreamCompletionSource completionSource = state switch + { + IFileStreamCompletionSourceStrategy strategy => strategy.CurrentOverlappedOwner!, // must be owned + _ => (FileStreamCompletionSource)state + }; + Debug.Assert(completionSource != null); + Debug.Assert(completionSource._overlapped == pOverlapped, "Overlaps don't match"); + + // Handle reading from & writing to closed pipes. While I'm not sure + // this is entirely necessary anymore, maybe it's possible for + // an async read on a pipe to be issued and then the pipe is closed, + // returning this error. This may very well be necessary. + ulong packedResult; + if (errorCode != 0 && errorCode != ERROR_BROKEN_PIPE && errorCode != ERROR_NO_DATA) + { + packedResult = ((ulong)ResultError | errorCode); + } + else + { + packedResult = ((ulong)ResultSuccess | numBytes); + } + + // Stow the result so that other threads can observe it + // And, if no other thread is registering cancellation, continue + if (NoResult == Interlocked.Exchange(ref completionSource._result, (long)packedResult)) + { + // Successfully set the state, attempt to take back the callback + if (Interlocked.Exchange(ref completionSource._result, CompletedCallback) != NoResult) + { + // Successfully got the callback, finish the callback + completionSource.CompleteCallback(packedResult); + } + // else: Some other thread stole the result, so now it is responsible to finish the callback + } + // else: Some other thread is registering a cancellation, so it *must* finish the callback + } + + private void CompleteCallback(ulong packedResult) + { + // Free up the native resource and cancellation registration + CancellationToken cancellationToken = _cancellationRegistration.Token; // access before disposing registration + ReleaseNativeResource(); + + // Unpack the result and send it to the user + long result = (long)(packedResult & ResultMask); + if (result == ResultError) + { + int errorCode = unchecked((int)(packedResult & uint.MaxValue)); + if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED) + { + TrySetCanceled(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(true)); + } + else + { + Exception e = Win32Marshal.GetExceptionForWin32Error(errorCode); + e.SetCurrentStackTrace(); + TrySetException(e); + } + } + else + { + Debug.Assert(result == ResultSuccess, "Unknown result"); + TrySetResult((int)(packedResult & uint.MaxValue) + _numBufferedBytes); + } + } + + private static void Cancel(object? state) + { + // WARNING: This may potentially be called under a lock (during cancellation registration) + + Debug.Assert(state is FileStreamCompletionSource, "Unknown state passed to cancellation"); + FileStreamCompletionSource completionSource = (FileStreamCompletionSource)state; + Debug.Assert(completionSource._overlapped != null && !completionSource.Task.IsCompleted, "IO should not have completed yet"); + + // If the handle is still valid, attempt to cancel the IO + if (!completionSource._strategy.FileHandle.IsInvalid && + !Interop.Kernel32.CancelIoEx(completionSource._strategy.FileHandle, completionSource._overlapped)) + { + int errorCode = Marshal.GetLastWin32Error(); + + // ERROR_NOT_FOUND is returned if CancelIoEx cannot find the request to cancel. + // This probably means that the IO operation has completed. + if (errorCode != Interop.Errors.ERROR_NOT_FOUND) + { + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + } + } + + public static FileStreamCompletionSource Create(IFileStreamCompletionSourceStrategy strategy, PreAllocatedOverlapped? preallocatedOverlapped, + int numBufferedBytesRead, ReadOnlyMemory memory) + { + // If the memory passed in is the strategy's internal buffer, we can use the base FileStreamCompletionSource, + // which has a PreAllocatedOverlapped with the memory already pinned. Otherwise, we use the derived + // MemoryFileStreamCompletionSource, which Retains the memory, which will result in less pinning in the case + // where the underlying memory is backed by pre-pinned buffers. + return preallocatedOverlapped != null && MemoryMarshal.TryGetArray(memory, out ArraySegment buffer) + && preallocatedOverlapped._overlapped.IsUserObject(buffer.Array) // preallocatedOverlapped is allocated when BufferedStream|LegacyFileStreamStrategy allocates the buffer + ? new FileStreamCompletionSource(strategy, preallocatedOverlapped, numBufferedBytesRead, buffer.Array) + : new MemoryFileStreamCompletionSource(strategy, numBufferedBytesRead, memory); + } + } + + /// + /// Extends with to support disposing of a + /// when the operation has completed. This should only be used + /// when memory doesn't wrap a byte[]. + /// + internal sealed class MemoryFileStreamCompletionSource : FileStreamCompletionSource + { + private MemoryHandle _handle; // mutable struct; do not make this readonly + + internal MemoryFileStreamCompletionSource(IFileStreamCompletionSourceStrategy strategy, int numBufferedBytes, ReadOnlyMemory memory) + : base(strategy, null, numBufferedBytes, null) // this type handles the pinning, so null is passed for bytes + { + _handle = memory.Pin(); + } + + internal override void ReleaseNativeResource() + { + _handle.Dispose(); + base.ReleaseNativeResource(); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs index cb1380f69bd77..dea977581e78e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs @@ -1,9 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; namespace System.IO @@ -11,6 +14,11 @@ namespace System.IO // this type defines a set of stateless FileStream/FileStreamStrategy helper methods internal static class FileStreamHelpers { + internal const int ERROR_BROKEN_PIPE = 109; + internal const int ERROR_NO_DATA = 232; + private const int ERROR_HANDLE_EOF = 38; + private const int ERROR_IO_PENDING = 997; + internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) => new LegacyFileStreamStrategy(fileStream, handle, access, bufferSize, isAsync); @@ -350,5 +358,243 @@ internal static unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan.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 => + { + Debug.Assert(s is AsyncCopyToAwaitable); + 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._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 = handle.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(handle, copyBuffer, 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, path); + } + } + + // 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 + 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, path); + } + + // 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; + } + } + 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) + { + handle.ThreadPoolBinding!.FreeNativeOverlapped(overlapped); + } + } + } + + // Write out the read data. + await destination.WriteAsync(new ReadOnlyMemory(copyBuffer, 0, (int)readAwaitable._numBytes), cancellationToken).ConfigureAwait(false); + } + } + finally + { + // Cleanup from the whole copy operation + cancellationReg.Dispose(); + awaitableOverlapped.Dispose(); + + ArrayPool.Shared.Return(copyBuffer); + } + } + + /// Used by AsyncWindowsFileStreamStrategy and LegacyFileStreamStrategy CopyToAsync to enable awaiting the result of an overlapped I/O operation with minimal overhead. + private sealed unsafe class AsyncCopyToAwaitable : ICriticalNotifyCompletion + { + /// Sentinel object used to indicate that the I/O operation has completed before being awaited. + private static readonly Action s_sentinel = () => { }; + /// Cached delegate to IOCallback. + internal static readonly IOCompletionCallback s_callback = IOCallback; + + internal readonly SafeFileHandle _fileHandle; + + /// Tracked position representing the next location from which to read. + internal long _position; + /// The current native overlapped pointer. This changes for each operation. + internal NativeOverlapped* _nativeOverlapped; + /// + /// null if the operation is still in progress, + /// s_sentinel if the I/O operation completed before the await, + /// s_callback if it completed after the await yielded. + /// + internal Action? _continuation; + /// Last error code from completed operation. + internal uint _errorCode; + /// Last number of read bytes from completed operation. + internal uint _numBytes; + + /// Lock object used to protect cancellation-related access to _nativeOverlapped. + internal object CancellationLock => this; + + /// Initialize the awaitable. + internal AsyncCopyToAwaitable(SafeFileHandle fileHandle) => _fileHandle = fileHandle; + + /// Reset state to prepare for the next read operation. + internal void ResetForNextOperation() + { + Debug.Assert(_position >= 0, $"Expected non-negative position, got {_position}"); + _continuation = null; + _errorCode = 0; + _numBytes = 0; + } + + /// Overlapped callback: store the results, then invoke the continuation delegate. + internal static void IOCallback(uint errorCode, uint numBytes, NativeOverlapped* pOVERLAP) + { + var awaitable = (AsyncCopyToAwaitable?)ThreadPoolBoundHandle.GetNativeOverlappedState(pOVERLAP); + Debug.Assert(awaitable != null); + + Debug.Assert(!ReferenceEquals(awaitable._continuation, s_sentinel), "Sentinel must not have already been set as the continuation"); + awaitable._errorCode = errorCode; + awaitable._numBytes = numBytes; + + (awaitable._continuation ?? Interlocked.CompareExchange(ref awaitable._continuation, s_sentinel, null))?.Invoke(); + } + + /// + /// Called when it's known that the I/O callback for an operation will not be invoked but we'll + /// still be awaiting the awaitable. + /// + internal void MarkCompleted() + { + Debug.Assert(_continuation == null, "Expected null continuation"); + _continuation = s_sentinel; + } + + public AsyncCopyToAwaitable GetAwaiter() => this; + public bool IsCompleted => ReferenceEquals(_continuation, s_sentinel); + public void GetResult() { } + public void OnCompleted(Action continuation) => UnsafeOnCompleted(continuation); + public void UnsafeOnCompleted(Action continuation) + { + if (ReferenceEquals(_continuation, s_sentinel) || + Interlocked.CompareExchange(ref _continuation, continuation, null) != null) + { + Debug.Assert(ReferenceEquals(_continuation, s_sentinel), $"Expected continuation set to s_sentinel, got ${_continuation}"); + Task.Run(continuation); + } + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs index b0c3bfb5353ca..dd34b3b3d6648 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; -using System.Runtime.CompilerServices; /* * Win32FileStream supports different modes of accessing the disk - async mode @@ -38,7 +36,7 @@ namespace System.IO { - internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy + internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy, IFileStreamCompletionSourceStrategy { private bool _canSeek; private bool _isPipe; // Whether to disable async buffering code. @@ -340,11 +338,6 @@ private unsafe void SetLengthCore(long value) } } - // Instance method to help code external to this MarshalByRefObject avoid - // accessing its fields by ref. This avoids a compiler warning. - private FileStreamCompletionSource? CompareExchangeCurrentOverlappedOwner(FileStreamCompletionSource? newSource, FileStreamCompletionSource? existingSource) => - Interlocked.CompareExchange(ref _currentOverlappedOwner, newSource, existingSource); - private int ReadSpan(Span destination) { Debug.Assert(!_useAsyncIO, "Must only be used when in synchronous mode"); @@ -556,6 +549,13 @@ partial void OnBufferAllocated() _preallocatedOverlapped = new PreAllocatedOverlapped(s_ioCallback, this, _buffer); } + SafeFileHandle IFileStreamCompletionSourceStrategy.FileHandle => _fileHandle; + + FileStreamCompletionSource? IFileStreamCompletionSourceStrategy.CurrentOverlappedOwner => _currentOverlappedOwner; + + FileStreamCompletionSource? IFileStreamCompletionSourceStrategy.CompareExchangeCurrentOverlappedOwner(FileStreamCompletionSource? newSource, FileStreamCompletionSource? existingSource) + => Interlocked.CompareExchange(ref _currentOverlappedOwner, newSource, existingSource); + private void WriteSpan(ReadOnlySpan source) { Debug.Assert(!_useAsyncIO, "Must only be used when in synchronous mode"); @@ -763,7 +763,7 @@ private unsafe Task ReadNativeAsync(Memory destination, int numBuffer Debug.Assert(_useAsyncIO, "ReadNativeAsync doesn't work on synchronous file streams!"); // Create and store async stream class library specific data in the async result - FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, numBufferedBytesRead, destination); + FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, _preallocatedOverlapped, numBufferedBytesRead, destination); NativeOverlapped* intOverlapped = completionSource.Overlapped; // Calculate position in the file we should be at after the read is done @@ -981,7 +981,7 @@ private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, Cancella Debug.Assert(_useAsyncIO, "WriteInternalCoreAsync doesn't work on synchronous file streams!"); // Create and store async stream class library specific data in the async result - FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, 0, source); + FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, _preallocatedOverlapped, 0, source); NativeOverlapped* intOverlapped = completionSource.Overlapped; if (CanSeek) @@ -1147,164 +1147,20 @@ private async Task AsyncModeCopyToAsync(Stream destination, int bufferSize, Canc } } - // 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.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 => - { - Debug.Assert(s is AsyncCopyToAwaitable); - 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, 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, _path); - } - } - - // 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 - 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, _path); - } - - // 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; - } - } - 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(new ReadOnlyMemory(copyBuffer, 0, (int)readAwaitable._numBytes), cancellationToken).ConfigureAwait(false); - } +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await FileStreamHelpers.AsyncModeCopyToAsync(_fileHandle, _path, canSeek, _filePosition, destination, bufferSize, cancellationToken); +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task } finally { - // Cleanup from the whole copy operation - cancellationReg.Dispose(); - awaitableOverlapped.Dispose(); - - ArrayPool.Shared.Return(copyBuffer); - // Make sure the stream's current position reflects where we ended up if (!_fileHandle.IsClosed && CanSeek) { @@ -1313,88 +1169,6 @@ private async Task AsyncModeCopyToAsync(Stream destination, int bufferSize, Canc } } - /// Used by CopyToAsync to enable awaiting the result of an overlapped I/O operation with minimal overhead. - private sealed unsafe class AsyncCopyToAwaitable : ICriticalNotifyCompletion - { - /// Sentinel object used to indicate that the I/O operation has completed before being awaited. - private static readonly Action s_sentinel = () => { }; - /// Cached delegate to IOCallback. - internal static readonly IOCompletionCallback s_callback = IOCallback; - - /// The FileStream that owns this instance. - internal readonly LegacyFileStreamStrategy _fileStream; - - /// Tracked position representing the next location from which to read. - internal long _position; - /// The current native overlapped pointer. This changes for each operation. - internal NativeOverlapped* _nativeOverlapped; - /// - /// null if the operation is still in progress, - /// s_sentinel if the I/O operation completed before the await, - /// s_callback if it completed after the await yielded. - /// - internal Action? _continuation; - /// Last error code from completed operation. - internal uint _errorCode; - /// Last number of read bytes from completed operation. - internal uint _numBytes; - - /// Lock object used to protect cancellation-related access to _nativeOverlapped. - internal object CancellationLock => this; - - /// Initialize the awaitable. - internal AsyncCopyToAwaitable(LegacyFileStreamStrategy fileStream) - { - _fileStream = fileStream; - } - - /// Reset state to prepare for the next read operation. - internal void ResetForNextOperation() - { - Debug.Assert(_position >= 0, $"Expected non-negative position, got {_position}"); - _continuation = null; - _errorCode = 0; - _numBytes = 0; - } - - /// Overlapped callback: store the results, then invoke the continuation delegate. - internal static void IOCallback(uint errorCode, uint numBytes, NativeOverlapped* pOVERLAP) - { - var awaitable = (AsyncCopyToAwaitable?)ThreadPoolBoundHandle.GetNativeOverlappedState(pOVERLAP); - Debug.Assert(awaitable != null); - - Debug.Assert(!ReferenceEquals(awaitable._continuation, s_sentinel), "Sentinel must not have already been set as the continuation"); - awaitable._errorCode = errorCode; - awaitable._numBytes = numBytes; - - (awaitable._continuation ?? Interlocked.CompareExchange(ref awaitable._continuation, s_sentinel, null))?.Invoke(); - } - - /// - /// Called when it's known that the I/O callback for an operation will not be invoked but we'll - /// still be awaiting the awaitable. - /// - internal void MarkCompleted() - { - Debug.Assert(_continuation == null, "Expected null continuation"); - _continuation = s_sentinel; - } - - public AsyncCopyToAwaitable GetAwaiter() => this; - public bool IsCompleted => ReferenceEquals(_continuation, s_sentinel); - public void GetResult() { } - public void OnCompleted(Action continuation) => UnsafeOnCompleted(continuation); - public void UnsafeOnCompleted(Action continuation) - { - if (ReferenceEquals(_continuation, s_sentinel) || - Interlocked.CompareExchange(ref _continuation, continuation, null) != null) - { - Debug.Assert(ReferenceEquals(_continuation, s_sentinel), $"Expected continuation set to s_sentinel, got ${_continuation}"); - Task.Run(continuation); - } - } - } - internal override void Lock(long position, long length) => FileStreamHelpers.Lock(_fileHandle, _path, position, length); internal override void Unlock(long position, long length) => FileStreamHelpers.Unlock(_fileHandle, _path, position, length); From 5a6421546a37cbdbeba9c894466ee9933fe42f11 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 26 Feb 2021 12:09:28 +0100 Subject: [PATCH 04/28] some minor improvements after reading the code again --- .../src/System/IO/FileStreamCompletionSource.Windows.cs | 2 +- .../src/System/IO/LegacyFileStreamStrategy.Windows.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs index eeea50a56f5a6..da3075211567d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs @@ -45,7 +45,7 @@ internal unsafe class FileStreamCompletionSource : TaskCompletionSource #if DEBUG private bool _cancellationHasBeenRegistered; #endif - internal NativeOverlapped* _overlapped; // Overlapped class responsible for operations in progress when an appdomain unload occurs + private NativeOverlapped* _overlapped; // Overlapped class responsible for operations in progress when an appdomain unload occurs private long _result; // Using long since this needs to be used in Interlocked APIs // Using RunContinuationsAsynchronously for compat reasons (old API used Task.Factory.StartNew for continuations) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs index dd34b3b3d6648..3849f620ebfc3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs @@ -42,8 +42,6 @@ internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy, IFi private bool _isPipe; // Whether to disable async buffering code. private long _appendStart; // When appending, prevent overwriting file. - private static readonly unsafe IOCompletionCallback s_ioCallback = FileStreamCompletionSource.IOCallback; - private Task _activeBufferOperation = Task.CompletedTask; // tracks in-progress async ops using the buffer private PreAllocatedOverlapped? _preallocatedOverlapped; // optimization for async ops to avoid per-op allocations private FileStreamCompletionSource? _currentOverlappedOwner; // async op currently using the preallocated overlapped @@ -546,7 +544,7 @@ partial void OnBufferAllocated() Debug.Assert(_preallocatedOverlapped == null); if (_useAsyncIO) - _preallocatedOverlapped = new PreAllocatedOverlapped(s_ioCallback, this, _buffer); + _preallocatedOverlapped = new PreAllocatedOverlapped(FileStreamCompletionSource.s_ioCallback, this, _buffer); } SafeFileHandle IFileStreamCompletionSourceStrategy.FileHandle => _fileHandle; From e27ce6c338ae60fa1095e7b2e920866edcc255d1 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 26 Feb 2021 12:20:55 +0100 Subject: [PATCH 05/28] only DerivedFileStreamStrategy needs to have a reference to FileStream remove finalizer from FileStream, keep it only in DerivedFileStreamStrategy and LegacyFileStreamStrategy --- .../System/IO/DerivedFileStreamStrategy.cs | 34 +++++++++++++++++-- .../src/System/IO/FileStream.cs | 19 +++-------- .../src/System/IO/FileStreamHelpers.Unix.cs | 8 ++--- .../System/IO/FileStreamHelpers.Windows.cs | 8 ++--- .../src/System/IO/FileStreamStrategy.cs | 4 --- .../src/System/IO/LegacyFileStreamStrategy.cs | 14 +++----- .../System/IO/WindowsFileStreamStrategy.cs | 2 -- 7 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/DerivedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/DerivedFileStreamStrategy.cs index 76d4441309f7b..e7dd824305f49 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/DerivedFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/DerivedFileStreamStrategy.cs @@ -15,8 +15,21 @@ namespace System.IO internal sealed class DerivedFileStreamStrategy : FileStreamStrategy { private readonly FileStreamStrategy _strategy; + private readonly FileStream _fileStream; - internal DerivedFileStreamStrategy(FileStream fileStream, FileStreamStrategy strategy) : base(fileStream) => _strategy = strategy; + internal DerivedFileStreamStrategy(FileStream fileStream, FileStreamStrategy strategy) + { + _fileStream = fileStream; + _strategy = strategy; + } + + ~DerivedFileStreamStrategy() + { + // Preserved for compatibility since FileStream has defined a + // finalizer in past releases and derived classes may depend + // on Dispose(false) call. + _fileStream.DisposeInternal(false); + } public override bool CanRead => _strategy.CanRead; @@ -36,7 +49,14 @@ public override long Position internal override string Name => _strategy.Name; - internal override SafeFileHandle SafeFileHandle => _strategy.SafeFileHandle; + internal override SafeFileHandle SafeFileHandle + { + get + { + _fileStream.Flush(false); + return _strategy.SafeFileHandle; + } + } internal override bool IsClosed => _strategy.IsClosed; @@ -136,6 +156,14 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio public override ValueTask DisposeAsync() => _fileStream.BaseDisposeAsync(); - internal override void DisposeInternal(bool disposing) => _strategy.DisposeInternal(disposing); + internal override void DisposeInternal(bool disposing) + { + _strategy.DisposeInternal(disposing); + + if (disposing) + { + GC.SuppressFinalize(this); + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs index 078b0bd0a4186..03620f33bf555 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -46,7 +46,7 @@ public FileStream(IntPtr handle, FileAccess access, bool ownsHandle, int bufferS { ValidateHandle(safeHandle, access, bufferSize, isAsync); - _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(this, safeHandle, access, bufferSize, isAsync)); + _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(safeHandle, access, bufferSize, isAsync)); } catch { @@ -95,7 +95,7 @@ public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize, bool { ValidateHandle(handle, access, bufferSize, isAsync); - _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(this, handle, access, bufferSize, isAsync)); + _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(handle, access, bufferSize, isAsync)); } public FileStream(string path, FileMode mode) : @@ -164,7 +164,7 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share SerializationInfo.ThrowIfDeserializationInProgress("AllowFileWrites", ref s_cachedSerializationSwitch); } - _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(this, path, mode, access, share, bufferSize, options)); + _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(path, mode, access, share, bufferSize, options)); } [Obsolete("This property has been deprecated. Please use FileStream's SafeFileHandle property instead. https://go.microsoft.com/fwlink/?linkid=14202")] @@ -397,18 +397,9 @@ public override long Position /// The byte to write to the stream. public override void WriteByte(byte value) => _strategy.WriteByte(value); - ~FileStream() - { - // Preserved for compatibility since FileStream has defined a - // finalizer in past releases and derived classes may depend - // on Dispose(false) call. - Dispose(false); - } + protected override void Dispose(bool disposing) => _strategy?.DisposeInternal(disposing); - protected override void Dispose(bool disposing) - { - _strategy?.DisposeInternal(disposing); // null _strategy possible in finalizer - } + internal void DisposeInternal(bool disposing) => Dispose(disposing); public override ValueTask DisposeAsync() => _strategy.DisposeAsync(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Unix.cs index 471005b833f74..25a09f1ab8940 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Unix.cs @@ -14,11 +14,11 @@ namespace System.IO internal static class FileStreamHelpers { // in the future we are most probably going to introduce more strategies (io_uring etc) - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) - => new LegacyFileStreamStrategy(fileStream, handle, access, bufferSize, isAsync); + internal static FileStreamStrategy ChooseStrategy(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) + => new LegacyFileStreamStrategy(handle, access, bufferSize, isAsync); - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - => new LegacyFileStreamStrategy(fileStream, path, mode, access, share, bufferSize, options); + internal static FileStreamStrategy ChooseStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + => new LegacyFileStreamStrategy(path, mode, access, share, bufferSize, options); internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs index dea977581e78e..c701b308e0071 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs @@ -19,11 +19,11 @@ internal static class FileStreamHelpers private const int ERROR_HANDLE_EOF = 38; private const int ERROR_IO_PENDING = 997; - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) - => new LegacyFileStreamStrategy(fileStream, handle, access, bufferSize, isAsync); + internal static FileStreamStrategy ChooseStrategy(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) + => new LegacyFileStreamStrategy(handle, access, bufferSize, isAsync); - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - => new LegacyFileStreamStrategy(fileStream, path, mode, access, share, bufferSize, options); + internal static FileStreamStrategy ChooseStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + => new LegacyFileStreamStrategy(path, mode, access, share, bufferSize, options); internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) => CreateFileOpenHandle(path, mode, access, share, options); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs index 97b5969355383..54166cd68d82f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs @@ -7,10 +7,6 @@ namespace System.IO { internal abstract class FileStreamStrategy : Stream { - protected readonly FileStream _fileStream; - - protected FileStreamStrategy(FileStream fileStream) => _fileStream = fileStream; - internal abstract bool IsAsync { get; } internal abstract string Name { get; } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs index 01e5db4f0827c..ea7b982da30b3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs @@ -61,7 +61,7 @@ internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy /// Whether the file stream's handle has been exposed. private bool _exposedHandle; - internal LegacyFileStreamStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) : base(fileStream) + internal LegacyFileStreamStrategy(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) { _exposedHandle = true; _bufferLength = bufferSize; @@ -78,7 +78,7 @@ internal LegacyFileStreamStrategy(FileStream fileStream, SafeFileHandle handle, _fileHandle = handle; } - internal LegacyFileStreamStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) : base(fileStream) + internal LegacyFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) { string fullPath = Path.GetFullPath(path); @@ -105,12 +105,7 @@ internal LegacyFileStreamStrategy(FileStream fileStream, string path, FileMode m } } - ~LegacyFileStreamStrategy() - { - // it looks like having this finalizer is mandatory, - // as we can not guarantee that the Strategy won't be null in FileStream finalizer - Dispose(false); - } + ~LegacyFileStreamStrategy() => Dispose(false); // mandatory to Flush the write buffer internal override void DisposeInternal(bool disposing) => Dispose(disposing); @@ -272,8 +267,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo return WriteAsyncInternal(buffer, cancellationToken); } - // this method might call Derived type implenentation of Flush(flushToDisk) - public override void Flush() => _fileStream.Flush(); + public override void Flush() => Flush(flushToDisk: false); internal override void Flush(bool flushToDisk) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs index a5655cc3ac1e2..b04658c955f0b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs @@ -123,8 +123,6 @@ public override ValueTask DisposeAsync() _fileHandle.Dispose(); } - GC.SuppressFinalize(this); // the handle is closed; nothing further for the finalizer to do - return ValueTask.CompletedTask; } From 3f84bd56b502cbd30267c54103c1db7561c0e25f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 26 Feb 2021 16:01:36 +0100 Subject: [PATCH 06/28] implement ReadByte and WriteByte to make new windows strategies fully functional they can now be used directly without any buffering on top of them! --- .../System/IO/AsyncWindowsFileStreamStrategy.cs | 12 ++++++++++-- .../System/IO/SyncWindowsFileStreamStrategy.cs | 12 ++++++++++-- .../src/System/IO/WindowsFileStreamStrategy.cs | 17 +++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs index 9d21309fd2107..4db3309cc075a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs @@ -129,7 +129,11 @@ public override ValueTask ReadAsync(Memory destination, CancellationT private unsafe Task ReadAsyncInternal(Memory destination, CancellationToken cancellationToken = default) { - Debug.Assert(CanRead, "BufferedStream has already verified that"); + if (!CanRead) + { + throw Error.GetReadNotSupported(); + } + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); // Create and store async stream class library specific data in the async result @@ -251,7 +255,11 @@ private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationTo private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, CancellationToken cancellationToken) { - Debug.Assert(CanWrite, "BufferedStream has already verified that"); + if (!CanWrite) + { + throw Error.GetWriteNotSupported(); + } + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); // Create and store async stream class library specific data in the async result diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs index 99a64e961cc12..4bf7e208e56d7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs @@ -95,7 +95,11 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo private unsafe int ReadSpan(Span destination) { - Debug.Assert(CanRead, "BufferedStream has already verified that"); + if (!CanRead) + { + throw Error.GetReadNotSupported(); + } + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); // Make sure we are reading from the right spot @@ -126,7 +130,11 @@ private unsafe int ReadSpan(Span destination) private unsafe void WriteSpan(ReadOnlySpan source) { - Debug.Assert(CanWrite, "BufferedStream has already verified that"); + if (!CanWrite) + { + throw Error.GetWriteNotSupported(); + } + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); // Make sure we are writing to the position that we think we are diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs index b04658c955f0b..a3100794ca3d8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs @@ -113,6 +113,23 @@ internal sealed override SafeFileHandle SafeFileHandle } } + // ReadByte and WriteByte methods are used only when the user has disabled buffering on purpose + // their performance is going to be horrible + // TODO: should we consider adding a new event provider and log an event so it can be detected? + public override int ReadByte() + { + Span buffer = stackalloc byte[1]; + int bytesRead = Read(buffer); + return bytesRead == 1 ? buffer[0] : -1; + } + + public override void WriteByte(byte value) + { + Span buffer = stackalloc byte[1]; + buffer[0] = value; + Write(buffer); + } + // this method just disposes everything as there is no buffer here // and we don't really need to Flush anything here public override ValueTask DisposeAsync() From 402cf653f21809a8c53c33d4d31376f8c36aed19 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 26 Feb 2021 16:49:33 +0100 Subject: [PATCH 07/28] implement FlushAsync for no buffering strategies as nop to get Flush_NothingToFlush_CompletesSynchronously passing --- .../src/System/IO/AsyncWindowsFileStreamStrategy.cs | 2 ++ .../src/System/IO/SyncWindowsFileStreamStrategy.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs index 4db3309cc075a..ef77a3c0a52d6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs @@ -250,6 +250,8 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => WriteAsyncInternal(buffer, cancellationToken); + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; // no buffering = nothing to flush + private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationToken cancellationToken) => new ValueTask(WriteAsyncInternalCore(source, cancellationToken)); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs index 4bf7e208e56d7..44563299c579f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs @@ -93,6 +93,8 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo base.WriteAsync(buffer, cancellationToken); } + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; // no buffering = nothing to flush + private unsafe int ReadSpan(Span destination) { if (!CanRead) From 9a303873e63674c22da751f2bc302051353eec08 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 26 Feb 2021 16:53:30 +0100 Subject: [PATCH 08/28] use the new strategies when buffering is not enabled --- .../System/IO/FileStreamHelpers.Windows.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs index c701b308e0071..6eff2056257ed 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs @@ -19,11 +19,31 @@ internal static class FileStreamHelpers private const int ERROR_HANDLE_EOF = 38; private const int ERROR_IO_PENDING = 997; + private static readonly bool UseLegacyStrategy = Environment.GetEnvironmentVariable("DOTNET_LEGACY_FILE_IO") == "1"; + internal static FileStreamStrategy ChooseStrategy(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) - => new LegacyFileStreamStrategy(handle, access, bufferSize, isAsync); + { + if (!UseLegacyStrategy && bufferSize == 1) + { + return isAsync + ? new AsyncWindowsFileStreamStrategy(handle, access) + : new SyncWindowsFileStreamStrategy(handle, access); + } + + return new LegacyFileStreamStrategy(handle, access, bufferSize, isAsync); + } internal static FileStreamStrategy ChooseStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - => new LegacyFileStreamStrategy(path, mode, access, share, bufferSize, options); + { + if (!UseLegacyStrategy && bufferSize == 1) + { + return (options & FileOptions.Asynchronous) != 0 + ? new AsyncWindowsFileStreamStrategy(path, mode, access, share, options) + : new SyncWindowsFileStreamStrategy(path, mode, access, share, options); + } + + return new LegacyFileStreamStrategy(path, mode, access, share, bufferSize, options); + } internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) => CreateFileOpenHandle(path, mode, access, share, options); From f34cb024994ac4f9e7b98c0ff04af0c56fa623f9 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 26 Feb 2021 17:05:18 +0100 Subject: [PATCH 09/28] introduce BufferedFileStreamStrategy --- .../tests/FileStream/SafeFileHandle.cs | 9 +- .../System.Private.CoreLib.Shared.projitems | 1 + .../IO/AsyncWindowsFileStreamStrategy.cs | 2 +- .../System/IO/BufferedFileStreamStrategy.cs | 132 ++++++++ .../src/System/IO/BufferedStream.cs | 308 +++++++++++++----- .../src/System/IO/FileStream.cs | 16 + .../System/IO/FileStreamHelpers.Windows.cs | 28 +- .../IO/LegacyFileStreamStrategy.Unix.cs | 2 +- .../IO/LegacyFileStreamStrategy.Windows.cs | 2 +- .../src/System/IO/LegacyFileStreamStrategy.cs | 16 - 10 files changed, 405 insertions(+), 111 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs index 04773b5a8df72..020a25e48871e 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs @@ -104,14 +104,7 @@ private async Task ThrowWhenHandlePositionIsChanged(bool useAsync) fs.WriteByte(0); fsr.Position++; - if (useAsync && OperatingSystem.IsWindows()) // Async I/O behaviors differ due to kernel-based implementation on Windows - { - Assert.Throws(() => FSAssert.CompletesSynchronously(fs.ReadAsync(new byte[1], 0, 1))); - } - else - { - await Assert.ThrowsAsync(() => fs.ReadAsync(new byte[1], 0, 1)); - } + await Assert.ThrowsAsync(() => fs.ReadAsync(new byte[1], 0, 1)); fs.WriteByte(0); fsr.Position++; diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 95cf4d04ba169..c9a7a9e8bdd0c 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -393,6 +393,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs index ef77a3c0a52d6..dadffb0164ffb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs @@ -102,7 +102,7 @@ protected override void OnInit() } } - // called by BufferedStream. TODO: find a cleaner solution + // called by BufferedStream internal void OnBufferAllocated(byte[] buffer) { Debug.Assert(buffer != null); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs new file mode 100644 index 0000000000000..bf5c50e2cfbfb --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + // this type exists so we can avoid duplicating the buffering logic in every FileStreamStrategy implementation + // for simple properties that would just call the wrapped stream properties, we call strategy directly + // for everything else, we are calling BufferedStream methods that take care of all the buffering work + internal sealed class BufferedFileStreamStrategy : FileStreamStrategy + { + private readonly FileStreamStrategy _strategy; + private readonly BufferedStream _bufferedStream; + + internal BufferedFileStreamStrategy(FileStreamStrategy strategy, int bufferSize) + { + _strategy = strategy; + _bufferedStream = new BufferedStream(strategy, bufferSize, actLikeFileStream: true); + } + + ~BufferedFileStreamStrategy() => DisposeInternal(false); + + public override bool CanRead => _strategy.CanRead; + + public override bool CanWrite => _strategy.CanWrite; + + public override bool CanSeek => _strategy.CanSeek; + + public override long Length => _bufferedStream.GetLengthWithoutFlushing(); + + public override long Position + { + get => _bufferedStream.GetPositionWithoutFlushing(); + set => _bufferedStream.Position = value; + } + + internal override bool IsAsync => _strategy.IsAsync; + + internal override string Name => _strategy.Name; + + internal override SafeFileHandle SafeFileHandle + { + get + { + _bufferedStream.Flush(); + return _strategy.SafeFileHandle; + } + } + + internal override bool IsClosed => _strategy.IsClosed; + + internal override void Lock(long position, long length) => _strategy.Lock(position, length); + + internal override void Unlock(long position, long length) => _strategy.Unlock(position, length); + + public override long Seek(long offset, SeekOrigin origin) => _bufferedStream.Seek(offset, origin); + + public override void SetLength(long value) => _bufferedStream.SetLength(value); + + public override int ReadByte() => _bufferedStream.ReadByte(); + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => _bufferedStream.BeginRead(buffer, offset, count, callback, state); + + public override int EndRead(IAsyncResult asyncResult) + => _bufferedStream.EndRead(asyncResult); + + public override int Read(byte[] buffer, int offset, int count) => _bufferedStream.Read(buffer, offset, count); + + public override int Read(Span buffer) => _bufferedStream.Read(buffer); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => _bufferedStream.ReadAsync(buffer, offset, count, cancellationToken); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + => _bufferedStream.ReadAsync(buffer, cancellationToken); + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => _bufferedStream.BeginWrite(buffer, offset, count, callback, state); + + public override void EndWrite(IAsyncResult asyncResult) + => _bufferedStream.EndWrite(asyncResult); + + public override void WriteByte(byte value) => _bufferedStream.WriteByte(value); + + public override void Write(byte[] buffer, int offset, int count) => _bufferedStream.Write(buffer, offset, count); + + public override void Write(ReadOnlySpan buffer) => _bufferedStream.Write(buffer); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => _bufferedStream.WriteAsync(buffer, offset, count, cancellationToken); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + => _bufferedStream.WriteAsync(buffer, cancellationToken); + + public override void Flush() => _bufferedStream.Flush(); + + internal override void Flush(bool flushToDisk) => _bufferedStream.Flush(flushToDisk); + + public override Task FlushAsync(CancellationToken cancellationToken) + => _bufferedStream.FlushAsync(cancellationToken); + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + => _bufferedStream.CopyToAsync(destination, bufferSize, cancellationToken); + + public override ValueTask DisposeAsync() + => _bufferedStream.DisposeAsync(); + + internal override void DisposeInternal(bool disposing) + { + try + { + // the finalizer must at least try to flush the write buffer + // so we enforce it by passing always true + _bufferedStream.DisposeInternal(true); + } + catch (Exception e) when (!disposing && FileStream.IsIoRelatedException(e)) + { + // On finalization, ignore failures from trying to flush the write buffer, + // e.g. if this stream is wrapping a pipe and the pipe is now broken. + } + + if (disposing) + { + GC.SuppressFinalize(this); + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs index bc1ea76470377..926a1a2f326eb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -51,6 +52,12 @@ public sealed class BufferedStream : Stream private Stream? _stream; // Underlying stream. Close sets _stream to null. private byte[]? _buffer; // Shared read/write buffer. Alloc on first use. private readonly int _bufferSize; // Length of internal buffer (not counting the shadow buffer). + /// + /// allows for: + /// 1. blocking zero byte reads + /// 2. skipping the serialization of async operations that don't use the buffer + /// + private readonly bool _actLikeFileStream; private int _readPos; // Read pointer within shared buffer. private int _readLen; // Number of bytes read in buffer from _stream. private int _writePos; // Write pointer within shared buffer. @@ -82,10 +89,17 @@ public BufferedStream(Stream stream, int bufferSize) throw new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); } + internal BufferedStream(Stream stream, int bufferSize, bool actLikeFileStream) : this(stream, bufferSize) + { + _actLikeFileStream = actLikeFileStream; + } + private void EnsureNotClosed() { if (_stream == null) - throw new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); + Throw(); + + static void Throw() => throw new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); } private void EnsureCanSeek() @@ -93,7 +107,9 @@ private void EnsureCanSeek() Debug.Assert(_stream != null); if (!_stream.CanSeek) - throw new NotSupportedException(SR.NotSupported_UnseekableStream); + Throw(); + + static void Throw() => throw new NotSupportedException(SR.NotSupported_UnseekableStream); } private void EnsureCanRead() @@ -101,7 +117,9 @@ private void EnsureCanRead() Debug.Assert(_stream != null); if (!_stream.CanRead) - throw new NotSupportedException(SR.NotSupported_UnreadableStream); + Throw(); + + static void Throw() => throw new NotSupportedException(SR.NotSupported_UnreadableStream); } private void EnsureCanWrite() @@ -109,7 +127,9 @@ private void EnsureCanWrite() Debug.Assert(_stream != null); if (!_stream.CanWrite) - throw new NotSupportedException(SR.NotSupported_UnwritableStream); + Throw(); + + static void Throw() => throw new NotSupportedException(SR.NotSupported_UnwritableStream); } private void EnsureShadowBufferAllocated() @@ -127,13 +147,23 @@ private void EnsureShadowBufferAllocated() _buffer = shadowBuffer; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EnsureBufferAllocated() { Debug.Assert(_bufferSize > 0); // BufferedStream is not intended for multi-threaded use, so no worries about the get/set race on _buffer. if (_buffer == null) + { _buffer = new byte[_bufferSize]; + +#if TARGET_WINDOWS + if (_actLikeFileStream && _stream is AsyncWindowsFileStreamStrategy asyncStrategy) + { + asyncStrategy.OnBufferAllocated(_buffer); + } +#endif + } } public Stream UnderlyingStream @@ -184,12 +214,27 @@ public override long Length EnsureNotClosed(); if (_writePos > 0) - FlushWrite(); + FlushWrite(true); return _stream!.Length; } } + // this method exists to keep old FileStream behaviour and don't perform a Flush when getting Length + internal long GetLengthWithoutFlushing() + { + Debug.Assert(_actLikeFileStream); + + EnsureNotClosed(); + + long len = _stream!.Length; + + if (_writePos > 0 && _stream!.Position + _writePos > len) + len = _writePos + _stream!.Position; + + return len; + } + public override long Position { get @@ -209,7 +254,7 @@ public override long Position EnsureCanSeek(); if (_writePos > 0) - FlushWrite(); + FlushWrite(true); _readPos = 0; _readLen = 0; @@ -217,6 +262,19 @@ public override long Position } } + // this method exists to keep old FileStream behaviour and don't perform a Flush when getting Position + internal long GetPositionWithoutFlushing() + { + Debug.Assert(_actLikeFileStream); + + EnsureNotClosed(); + EnsureCanSeek(); + + return (_stream!.Position - _readLen) + _readPos + _writePos; + } + + internal void DisposeInternal(bool disposing) => Dispose(disposing); + protected override void Dispose(bool disposing) { try @@ -266,16 +324,24 @@ public override async ValueTask DisposeAsync() } } - public override void Flush() + public override void Flush() => Flush(true); + + internal void Flush(bool performActualFlush) { EnsureNotClosed(); // Has write data in the buffer: if (_writePos > 0) { - FlushWrite(); - Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); - return; + // EnsureNotClosed does not guarantee that the Stream has not been closed + // an example could be a call to fileStream.SafeFileHandle.Dispose() + // so to avoid getting exception here, we just ensure that we can Write before doing it + if (_stream!.CanWrite) + { + FlushWrite(performActualFlush); + Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); + return; + } } // Has read data in the buffer: @@ -292,7 +358,7 @@ public override void Flush() // User streams may have opted to throw from Flush if CanWrite is false (although the abstract Stream does not do so). // However, if we do not forward the Flush to the underlying stream, we may have problems when chaining several streams. // Let us make a best effort attempt: - if (_stream.CanWrite) + if (performActualFlush && _stream.CanWrite) _stream.Flush(); // If the Stream was seekable, then we should have called FlushRead which resets _readPos & _readLen. @@ -301,7 +367,7 @@ public override void Flush() } // We had no data in the buffer, but we still need to tell the underlying stream to flush. - if (_stream!.CanWrite) + if (performActualFlush && _stream!.CanWrite) _stream.Flush(); _writePos = _readPos = _readLen = 0; @@ -314,14 +380,32 @@ public override Task FlushAsync(CancellationToken cancellationToken) EnsureNotClosed(); - return FlushAsyncInternal(cancellationToken); + // try to get the lock and exit in synchronous way if there is nothing to Flush + SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); + Task semaphoreLockTask = sem.WaitAsync(cancellationToken); + bool lockAcquired = semaphoreLockTask.IsCompletedSuccessfully; + if (lockAcquired) + { + if (_writePos == 0 && _readPos == _readLen) + { + sem.Release(); + + return CanWrite ? _stream!.FlushAsync(cancellationToken) : Task.CompletedTask; + } + } + + return FlushAsyncInternal(semaphoreLockTask, lockAcquired, cancellationToken); } - private async Task FlushAsyncInternal(CancellationToken cancellationToken) + private async Task FlushAsyncInternal(Task semaphoreLockTask, bool lockAcquired, CancellationToken cancellationToken) { Debug.Assert(_stream != null); - await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); + if (!lockAcquired) + { + await semaphoreLockTask.ConfigureAwait(false); + } + try { if (_writePos > 0) @@ -362,7 +446,7 @@ private async Task FlushAsyncInternal(CancellationToken cancellationToken) } finally { - _asyncActiveSemaphore.Release(); + _asyncActiveSemaphore!.Release(); } } @@ -384,6 +468,7 @@ private void FlushRead() /// /// Called by Write methods to clear the Read Buffer /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ClearReadBufferBeforeWrite() { Debug.Assert(_stream != null); @@ -403,12 +488,14 @@ private void ClearReadBufferBeforeWrite() // However, since the user did not call a method that is intuitively expected to seek, a better message is in order. // Ideally, we would throw an InvalidOperation here, but for backward compat we have to stick with NotSupported. if (!_stream.CanSeek) - throw new NotSupportedException(SR.NotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed); + Throw(); FlushRead(); + + static void Throw() => throw new NotSupportedException(SR.NotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed); } - private void FlushWrite() + private void FlushWrite(bool performActualFlush) { Debug.Assert(_stream != null); Debug.Assert(_readPos == 0 && _readLen == 0, @@ -418,7 +505,11 @@ private void FlushWrite() _stream.Write(_buffer, 0, _writePos); _writePos = 0; - _stream.Flush(); + + if (performActualFlush) + { + _stream.Flush(); + } } private async ValueTask FlushWriteAsync(CancellationToken cancellationToken) @@ -434,6 +525,7 @@ private async ValueTask FlushWriteAsync(CancellationToken cancellationToken) await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private int ReadFromBuffer(byte[] buffer, int offset, int count) { int readbytes = _readLen - _readPos; @@ -493,7 +585,7 @@ public override int Read(byte[] buffer, int offset, int count) // BUT - this is a breaking change. // So: If we could not read all bytes the user asked for from the buffer, we will try once from the underlying // stream thus ensuring the same blocking behaviour as if the underlying stream was not wrapped in this BufferedStream. - if (bytesFromBuffer == count) + if (bytesFromBuffer == count && (count > 0 || !_actLikeFileStream)) return bytesFromBuffer; int alreadySatisfied = bytesFromBuffer; @@ -509,7 +601,7 @@ public override int Read(byte[] buffer, int offset, int count) // If there was anything in the write buffer, clear it. if (_writePos > 0) - FlushWrite(); + FlushWrite(true); // If the requested read is larger than buffer size, avoid the buffer and still use a single read: if (count >= _bufferSize) @@ -541,7 +633,7 @@ public override int Read(Span destination) // Try to read from the buffer. int bytesFromBuffer = ReadFromBuffer(destination); - if (bytesFromBuffer == destination.Length) + if (bytesFromBuffer == destination.Length && (destination.Length > 0 || !_actLikeFileStream)) { // We got as many bytes as were asked for; we're done. return bytesFromBuffer; @@ -561,7 +653,7 @@ public override int Read(Span destination) // If there was anything in the write buffer, clear it. if (_writePos > 0) { - FlushWrite(); + FlushWrite(true); } if (destination.Length >= _bufferSize) @@ -609,13 +701,13 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel // an Async operation. SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); Task semaphoreLockTask = sem.WaitAsync(cancellationToken); - if (semaphoreLockTask.IsCompletedSuccessfully) + bool locked = semaphoreLockTask.IsCompletedSuccessfully; + if (locked) { - bool completeSynchronously = true; - try + // hot path #1: there is data in the buffer + if (_readLen - _readPos > 0 || (count == 0 && !_actLikeFileStream)) { - Exception? error; - bytesFromBuffer = ReadFromBuffer(buffer, offset, count, out error); + bytesFromBuffer = ReadFromBuffer(buffer, offset, count, out Exception? error); // If we satisfied enough data from the buffer, we can complete synchronously. // Reading again for more data may cause us to block if we're using a device with no clear end of file, @@ -624,27 +716,37 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel // BUT - this is a breaking change. // So: If we could not read all bytes the user asked for from the buffer, we will try once from the underlying // stream thus ensuring the same blocking behaviour as if the underlying stream was not wrapped in this BufferedStream. - completeSynchronously = (bytesFromBuffer == count || error != null); - - if (completeSynchronously) + if (bytesFromBuffer == count || error != null) { + // if the above is false, we will be entering ReadFromUnderlyingStreamAsync and releasing there. + sem.Release(); return (error == null) - ? LastSyncCompletedReadTask(bytesFromBuffer) - : Task.FromException(error); + ? LastSyncCompletedReadTask(bytesFromBuffer) + : Task.FromException(error); } } - finally + // hot path #2: there is nothing to Flush and buffering would not be beneficial + // it's allowed only for FileStream, as the public contract is that BufferedStream + // serializes ALL async operations + else if (_actLikeFileStream && _writePos == 0 && count >= _bufferSize) { - if (completeSynchronously) // if this is FALSE, we will be entering ReadFromUnderlyingStreamAsync and releasing there. - sem.Release(); + _readPos = _readLen = 0; + + // start the async operation + ValueTask result = _stream!.ReadAsync(new Memory(buffer, offset, count), cancellationToken); + + // release the lock (we are not using shared state anymore) + sem.Release(); + + return result.AsTask(); } } // Delegate to the async implementation. return ReadFromUnderlyingStreamAsync( new Memory(buffer, offset + bytesFromBuffer, count - bytesFromBuffer), - cancellationToken, bytesFromBuffer, semaphoreLockTask).AsTask(); + cancellationToken, bytesFromBuffer, semaphoreLockTask, locked).AsTask(); } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) @@ -660,30 +762,44 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken int bytesFromBuffer = 0; SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); Task semaphoreLockTask = sem.WaitAsync(cancellationToken); - if (semaphoreLockTask.IsCompletedSuccessfully) + bool locked = semaphoreLockTask.IsCompletedSuccessfully; + if (locked) { - bool completeSynchronously = true; - try + // hot path #1: there is data in the buffer + if (_readLen - _readPos > 0 || (buffer.Length == 0 && !_actLikeFileStream)) { bytesFromBuffer = ReadFromBuffer(buffer.Span); - completeSynchronously = bytesFromBuffer == buffer.Length; - if (completeSynchronously) + + if (bytesFromBuffer == buffer.Length) { + // if above is FALSE, we will be entering ReadFromUnderlyingStreamAsync and releasing there. + sem.Release(); + // If we satisfied enough data from the buffer, we can complete synchronously. return new ValueTask(bytesFromBuffer); } + + buffer = buffer.Slice(bytesFromBuffer); } - finally + // hot path #2: there is nothing to Flush and buffering would not be beneficial + // it's allowed only for FileStream, as the public contract is that BufferedStream + // serializes ALL async operations + else if (_actLikeFileStream && _writePos == 0 && buffer.Length >= _bufferSize) { - if (completeSynchronously) // if this is FALSE, we will be entering ReadFromUnderlyingStreamAsync and releasing there. - { - sem.Release(); - } + _readPos = _readLen = 0; + + // start the async operation + ValueTask result = _stream!.ReadAsync(buffer, cancellationToken); + + // release the lock (we are not using shared state anymore) + sem.Release(); + + return result; } } // Delegate to the async implementation. - return ReadFromUnderlyingStreamAsync(buffer.Slice(bytesFromBuffer), cancellationToken, bytesFromBuffer, semaphoreLockTask); + return ReadFromUnderlyingStreamAsync(buffer, cancellationToken, bytesFromBuffer, semaphoreLockTask, locked); } /// BufferedStream should be as thin a wrapper as possible. We want ReadAsync to delegate to @@ -691,7 +807,7 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken /// This allows BufferedStream to affect the semantics of the stream it wraps as little as possible. /// -2 if _bufferSize was set to 0 while waiting on the semaphore; otherwise num of bytes read. private async ValueTask ReadFromUnderlyingStreamAsync( - Memory buffer, CancellationToken cancellationToken, int bytesAlreadySatisfied, Task semaphoreLockTask) + Memory buffer, CancellationToken cancellationToken, int bytesAlreadySatisfied, Task semaphoreLockTask, bool locked) { // Same conditions validated with exceptions in ReadAsync: Debug.Assert(_stream != null); @@ -700,22 +816,32 @@ private async ValueTask ReadFromUnderlyingStreamAsync( Debug.Assert(_asyncActiveSemaphore != null); Debug.Assert(semaphoreLockTask != null); - // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. - await semaphoreLockTask.ConfigureAwait(false); + if (!locked) + { + // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. + await semaphoreLockTask.ConfigureAwait(false); + } + try { - // The buffer might have been changed by another async task while we were waiting on the semaphore. - // Check it now again. - int bytesFromBuffer = ReadFromBuffer(buffer.Span); - if (bytesFromBuffer == buffer.Length) - { - return bytesAlreadySatisfied + bytesFromBuffer; - } + int bytesFromBuffer = 0; - if (bytesFromBuffer > 0) + // we have already tried to read it from the buffer + if (!locked && (buffer.Length > 0 || !_actLikeFileStream)) { - buffer = buffer.Slice(bytesFromBuffer); - bytesAlreadySatisfied += bytesFromBuffer; + // The buffer might have been changed by another async task while we were waiting on the semaphore. + // Check it now again. + bytesFromBuffer = ReadFromBuffer(buffer.Span); + if (bytesFromBuffer == buffer.Length) + { + return bytesAlreadySatisfied + bytesFromBuffer; + } + + if (bytesFromBuffer > 0) + { + buffer = buffer.Slice(bytesFromBuffer); + bytesAlreadySatisfied += bytesFromBuffer; + } } Debug.Assert(_readLen == _readPos); @@ -773,7 +899,7 @@ private int ReadByteSlow() Debug.Assert(_stream != null); if (_writePos > 0) - FlushWrite(); + FlushWrite(true); EnsureBufferAllocated(); _readLen = _stream.Read(_buffer!, 0, _bufferSize); @@ -1035,7 +1161,8 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo // Try to satisfy the request from the buffer synchronously. SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); Task semaphoreLockTask = sem.WaitAsync(cancellationToken); - if (semaphoreLockTask.IsCompletedSuccessfully) + bool locked = semaphoreLockTask.IsCompletedSuccessfully; + if (locked) { bool completeSynchronously = true; try @@ -1047,7 +1174,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo Debug.Assert(_writePos < _bufferSize); - // If the write completely fits into the buffer, we can complete synchronously: + // hot path #1 If the write completely fits into the buffer, we can complete synchronously: completeSynchronously = buffer.Length < _bufferSize - _writePos; if (completeSynchronously) { @@ -1061,10 +1188,22 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo if (completeSynchronously) // if this is FALSE, we will be entering WriteToUnderlyingStreamAsync and releasing there. sem.Release(); } + + // hot path #2: there is nothing to Flush and buffering would not be beneficial + // it's allowed only for FileStream, as the public contract is that BufferedStream + // serializes ALL async operations + if (_actLikeFileStream && _writePos == 0 && buffer.Length >= _bufferSize) + { + ValueTask result = _stream!.WriteAsync(buffer, cancellationToken); + + sem.Release(); + + return result; + } } // Delegate to the async implementation. - return WriteToUnderlyingStreamAsync(buffer, cancellationToken, semaphoreLockTask); + return WriteToUnderlyingStreamAsync(buffer, cancellationToken, semaphoreLockTask, locked); } /// BufferedStream should be as thin a wrapper as possible. We want WriteAsync to delegate to @@ -1073,7 +1212,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo /// little as possible. /// private async ValueTask WriteToUnderlyingStreamAsync( - ReadOnlyMemory buffer, CancellationToken cancellationToken, Task semaphoreLockTask) + ReadOnlyMemory buffer, CancellationToken cancellationToken, Task semaphoreLockTask, bool locked) { Debug.Assert(_stream != null); Debug.Assert(_stream.CanWrite); @@ -1083,14 +1222,23 @@ private async ValueTask WriteToUnderlyingStreamAsync( // See the LARGE COMMENT in Write(..) for the explanation of the write buffer algorithm. - await semaphoreLockTask.ConfigureAwait(false); + if (!locked) + { + await semaphoreLockTask.ConfigureAwait(false); + } + try { - // The buffer might have been changed by another async task while we were waiting on the semaphore. - // However, note that if we recalculate the sync completion condition to TRUE, then useBuffer will also be TRUE. + if (!locked) + { + // The buffer might have been changed by another async task while we were waiting on the semaphore. + // However, note that if we recalculate the sync completion condition to TRUE, then useBuffer will also be TRUE. - if (_writePos == 0) - ClearReadBufferBeforeWrite(); + if (_writePos == 0) + { + ClearReadBufferBeforeWrite(); + } + } int totalUserBytes; bool useBuffer; @@ -1164,6 +1312,18 @@ public override void EndWrite(IAsyncResult asyncResult) => TaskToApm.End(asyncResult); public override void WriteByte(byte value) + { + if (_writePos > 0 && _writePos < _bufferSize - 1) + { + _buffer![_writePos++] = value; + } + else + { + WriteByteSlow(value); + } + } + + private void WriteByteSlow(byte value) { EnsureNotClosed(); @@ -1176,7 +1336,7 @@ public override void WriteByte(byte value) // We should not be flushing here, but only writing to the underlying stream, but previous version flushed, so we keep this. if (_writePos >= _bufferSize - 1) - FlushWrite(); + FlushWrite(true); _buffer![_writePos++] = value; @@ -1194,7 +1354,7 @@ public override long Seek(long offset, SeekOrigin origin) { // We should be only writing the buffer and not flushing, // but the previous version did flush and we stick to it for back-compat reasons. - FlushWrite(); + FlushWrite(true); return _stream.Seek(offset, origin); } @@ -1267,7 +1427,7 @@ public override void CopyTo(Stream destination, int bufferSize) else if (_writePos > 0) { // If there's write data in the buffer, flush it back to the underlying stream, as does ReadAsync. - FlushWrite(); + FlushWrite(true); } // Our buffer is now clear. Copy data directly from the source stream to the destination stream. diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs index 03620f33bf555..6109cf014298c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -477,5 +477,21 @@ internal IAsyncResult BaseBeginWrite(byte[] buffer, int offset, int count, Async => base.BeginWrite(buffer, offset, count, callback, state); internal void BaseEndWrite(IAsyncResult asyncResult) => base.EndWrite(asyncResult); + + internal static bool IsIoRelatedException(Exception e) => + // These all derive from IOException + // DirectoryNotFoundException + // DriveNotFoundException + // EndOfStreamException + // FileLoadException + // FileNotFoundException + // PathTooLongException + // PipeException + e is IOException || + // Note that SecurityException is only thrown on runtimes that support CAS + // e is SecurityException || + e is UnauthorizedAccessException || + e is NotSupportedException || + (e is ArgumentException && !(e is ArgumentNullException)); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs index 6eff2056257ed..2541f5ec96d16 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs @@ -23,28 +23,36 @@ internal static class FileStreamHelpers internal static FileStreamStrategy ChooseStrategy(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) { - if (!UseLegacyStrategy && bufferSize == 1) + if (UseLegacyStrategy) { - return isAsync - ? new AsyncWindowsFileStreamStrategy(handle, access) - : new SyncWindowsFileStreamStrategy(handle, access); + return new LegacyFileStreamStrategy(handle, access, bufferSize, isAsync); } - return new LegacyFileStreamStrategy(handle, access, bufferSize, isAsync); + WindowsFileStreamStrategy strategy = isAsync + ? new AsyncWindowsFileStreamStrategy(handle, access) + : new SyncWindowsFileStreamStrategy(handle, access); + + return EnableBufferingIfNeeded(strategy, bufferSize); } internal static FileStreamStrategy ChooseStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) { - if (!UseLegacyStrategy && bufferSize == 1) + if (UseLegacyStrategy) { - return (options & FileOptions.Asynchronous) != 0 - ? new AsyncWindowsFileStreamStrategy(path, mode, access, share, options) - : new SyncWindowsFileStreamStrategy(path, mode, access, share, options); + return new LegacyFileStreamStrategy(path, mode, access, share, bufferSize, options); } - return new LegacyFileStreamStrategy(path, mode, access, share, bufferSize, options); + WindowsFileStreamStrategy strategy = (options & FileOptions.Asynchronous) != 0 + ? new AsyncWindowsFileStreamStrategy(path, mode, access, share, options) + : new SyncWindowsFileStreamStrategy(path, mode, access, share, options); + + return EnableBufferingIfNeeded(strategy, bufferSize); } + // TODO: we might want to consider strategy.IsPipe here and never enable buffering for async pipes + internal static FileStreamStrategy EnableBufferingIfNeeded(WindowsFileStreamStrategy strategy, int bufferSize) + => bufferSize == 1 ? strategy : new BufferedFileStreamStrategy(strategy, bufferSize); + internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) => CreateFileOpenHandle(path, mode, access, share, options); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs index 550b2acab2505..c4cbd842c1ee7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs @@ -172,7 +172,7 @@ protected override void Dispose(bool disposing) { FlushWriteBuffer(); } - catch (Exception e) when (IsIoRelatedException(e) && !disposing) + catch (Exception e) when (!disposing && FileStream.IsIoRelatedException(e)) { // On finalization, ignore failures from trying to flush the write buffer, // e.g. if this stream is wrapping a pipe and the pipe is now broken. diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs index 3849f620ebfc3..0e236cfa5097f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs @@ -192,7 +192,7 @@ protected override void Dispose(bool disposing) { FlushWriteBuffer(!disposing); } - catch (Exception e) when (IsIoRelatedException(e) && !disposing) + catch (Exception e) when (!disposing && FileStream.IsIoRelatedException(e)) { // On finalization, ignore failures from trying to flush the write buffer, // e.g. if this stream is wrapping a pipe and the pipe is now broken. diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs index ea7b982da30b3..e11d395d936b9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs @@ -377,22 +377,6 @@ public override long Position internal override bool IsClosed => _fileHandle.IsClosed; - private static bool IsIoRelatedException(Exception e) => - // These all derive from IOException - // DirectoryNotFoundException - // DriveNotFoundException - // EndOfStreamException - // FileLoadException - // FileNotFoundException - // PathTooLongException - // PipeException - e is IOException || - // Note that SecurityException is only thrown on runtimes that support CAS - // e is SecurityException || - e is UnauthorizedAccessException || - e is NotSupportedException || - (e is ArgumentException && !(e is ArgumentNullException)); - /// /// Gets the array used for buffering reading and writing. /// If the array hasn't been allocated, this will lazily allocate it. From fb5b611b293631431078562c536f0946e848713f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 1 Mar 2021 17:47:22 +0100 Subject: [PATCH 10/28] fix the Mono build --- .../src/System/Threading/ClrThreadPoolPreAllocatedOverlapped.cs | 2 ++ .../src/System/IO/FileStreamCompletionSource.Windows.cs | 2 +- .../src/System/Threading/PreAllocatedOverlapped.cs | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Threading/ClrThreadPoolPreAllocatedOverlapped.cs b/src/coreclr/System.Private.CoreLib/src/System/Threading/ClrThreadPoolPreAllocatedOverlapped.cs index 06df433df4660..d856d1d9231b3 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Threading/ClrThreadPoolPreAllocatedOverlapped.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Threading/ClrThreadPoolPreAllocatedOverlapped.cs @@ -95,5 +95,7 @@ unsafe void IDeferredDisposable.OnFinalRelease(bool disposed) } } } + + internal bool IsUserObject(byte[]? buffer) => _overlapped.IsUserObject(buffer); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs index da3075211567d..edee6fce1315d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs @@ -237,7 +237,7 @@ public static FileStreamCompletionSource Create(IFileStreamCompletionSourceStrat // MemoryFileStreamCompletionSource, which Retains the memory, which will result in less pinning in the case // where the underlying memory is backed by pre-pinned buffers. return preallocatedOverlapped != null && MemoryMarshal.TryGetArray(memory, out ArraySegment buffer) - && preallocatedOverlapped._overlapped.IsUserObject(buffer.Array) // preallocatedOverlapped is allocated when BufferedStream|LegacyFileStreamStrategy allocates the buffer + && preallocatedOverlapped.IsUserObject(buffer.Array) // preallocatedOverlapped is allocated when BufferedStream|LegacyFileStreamStrategy allocates the buffer ? new FileStreamCompletionSource(strategy, preallocatedOverlapped, numBufferedBytesRead, buffer.Array) : new MemoryFileStreamCompletionSource(strategy, numBufferedBytesRead, memory); } diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PreAllocatedOverlapped.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PreAllocatedOverlapped.cs index 2bd19d2e9a2e6..d4a0daeff29c7 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/PreAllocatedOverlapped.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PreAllocatedOverlapped.cs @@ -8,5 +8,6 @@ public sealed class PreAllocatedOverlapped : System.IDisposable [System.CLSCompliantAttribute(false)] public PreAllocatedOverlapped(System.Threading.IOCompletionCallback callback, object? state, object? pinData) { } public void Dispose() { } + internal bool IsUserObject(byte[]? buffer) => false; } } From b1decbfd37790b72ce643c4a6551b109285580f9 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 2 Mar 2021 11:54:25 +0100 Subject: [PATCH 11/28] use the Legacy strategy by default for now, add tests for the other implementation --- .../TestUtilities/System/PlatformDetection.cs | 12 ++++-- .../System.IO.FileSystem.sln | 7 ++++ .../tests/FileStream/WriteAsync.cs | 2 +- .../tests/LegacyTests/LegacySwitchTests.cs | 29 +++++++++++++ .../System.IO.FileSystem.Legacy.Tests.csproj | 42 +++++++++++++++++++ .../LegacyTests/runtimeconfig.template.json | 5 +++ src/libraries/System.IO/System.IO.sln | 7 ++++ .../LegacyTests/System.IO.Legacy.Tests.csproj | 24 +++++++++++ .../LegacyTests/runtimeconfig.template.json | 5 +++ .../src/System/AppContextConfigHelper.cs | 14 +++++++ .../GlobalizationMode.Windows.cs | 2 +- .../System/Globalization/GlobalizationMode.cs | 16 +------ .../System/IO/FileStreamHelpers.Windows.cs | 4 +- 13 files changed, 147 insertions(+), 22 deletions(-) create mode 100644 src/libraries/System.IO.FileSystem/tests/LegacyTests/LegacySwitchTests.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj create mode 100644 src/libraries/System.IO.FileSystem/tests/LegacyTests/runtimeconfig.template.json create mode 100644 src/libraries/System.IO/tests/LegacyTests/System.IO.Legacy.Tests.csproj create mode 100644 src/libraries/System.IO/tests/LegacyTests/runtimeconfig.template.json diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index f56404ff27024..bb0c84f5523ca 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -182,14 +182,14 @@ public static string GetDistroVersionString() } } - private static readonly Lazy m_isInvariant = new Lazy(GetIsInvariantGlobalization); + private static readonly Lazy m_isInvariant = new Lazy(() => GetStaticNonPublicBooleanPropertyValue("System.Globalization.GlobalizationMode", "Invariant")); - private static bool GetIsInvariantGlobalization() + private static bool GetStaticNonPublicBooleanPropertyValue(string typeName, string propertyName) { - Type globalizationMode = Type.GetType("System.Globalization.GlobalizationMode"); + Type globalizationMode = Type.GetType(typeName); if (globalizationMode != null) { - MethodInfo methodInfo = globalizationMode.GetProperty("Invariant", BindingFlags.NonPublic | BindingFlags.Static)?.GetMethod; + MethodInfo methodInfo = globalizationMode.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetMethod; if (methodInfo != null) { return (bool)methodInfo.Invoke(null, null); @@ -230,6 +230,10 @@ private static Version GetICUVersion() version & 0xFF); } + private static readonly Lazy _legacyFileStream = new Lazy(() => GetStaticNonPublicBooleanPropertyValue("System.IO.FileStreamHelpers", "UseLegacyStrategy")); + + public static bool IsLegacyFileStreamEnabled => _legacyFileStream.Value; + private static bool GetIsInContainer() { if (IsWindows) diff --git a/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln b/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln index 4c3dc58180ee8..91fcdfa5375e5 100644 --- a/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln +++ b/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln @@ -25,6 +25,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{32A31E04-255 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D9FB1730-B750-4C0D-8D24-8C992DEB6034}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.FileSystem.Legacy.Tests", "tests\LegacyTests\System.IO.FileSystem.Legacy.Tests.csproj", "{48E07F12-8597-40DE-8A37-CCBEB9D54012}" +EndProject Global GlobalSection(NestedProjects) = preSolution {D350D6E7-52F1-40A4-B646-C178F6BBB689} = {1A727AF9-4F39-4109-BB8F-813286031DC9} @@ -37,6 +39,7 @@ Global {06DD5AA8-FDA6-495B-A8D1-8CE83C78DE6C} = {32A31E04-2554-4223-BED8-45757408B4F6} {877E39A8-51CB-463A-AF4C-6FAE4F438075} = {D9FB1730-B750-4C0D-8D24-8C992DEB6034} {D7DF8034-3AE5-4DEF-BCC4-6353239391BF} = {D9FB1730-B750-4C0D-8D24-8C992DEB6034} + {48E07F12-8597-40DE-8A37-CCBEB9D54012} = {1A727AF9-4F39-4109-BB8F-813286031DC9} EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +86,10 @@ Global {06DD5AA8-FDA6-495B-A8D1-8CE83C78DE6C}.Debug|Any CPU.Build.0 = Debug|Any CPU {06DD5AA8-FDA6-495B-A8D1-8CE83C78DE6C}.Release|Any CPU.ActiveCfg = Release|Any CPU {06DD5AA8-FDA6-495B-A8D1-8CE83C78DE6C}.Release|Any CPU.Build.0 = Release|Any CPU + {48E07F12-8597-40DE-8A37-CCBEB9D54012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48E07F12-8597-40DE-8A37-CCBEB9D54012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48E07F12-8597-40DE-8A37-CCBEB9D54012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48E07F12-8597-40DE-8A37-CCBEB9D54012}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs index 6ed4d1db2c4cc..358cf11310838 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs @@ -220,7 +220,7 @@ public async Task ManyConcurrentWriteAsyncs_OuterLoop( { writes[i] = WriteAsync(fs, expectedData, i * writeSize, writeSize, cancellationToken); Assert.Null(writes[i].Exception); - if (useAsync) + if (useAsync && PlatformDetection.IsLegacyFileStreamEnabled) { Assert.Equal((i + 1) * writeSize, fs.Position); } diff --git a/src/libraries/System.IO.FileSystem/tests/LegacyTests/LegacySwitchTests.cs b/src/libraries/System.IO.FileSystem/tests/LegacyTests/LegacySwitchTests.cs new file mode 100644 index 0000000000000..55d162306989c --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/LegacyTests/LegacySwitchTests.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Xunit; + +namespace System.IO.Tests +{ + public class LegacySwitchTests + { + [Fact] + public static void LegacySwitchIsHonored() + { + string filePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + + using (FileStream fileStream = File.Create(filePath)) + { + Stream strategy = fileStream + .GetType() + .GetField("_strategy", BindingFlags.NonPublic | BindingFlags.Instance) + .GetValue(fileStream) as Stream; + + Assert.DoesNotContain(strategy.GetType().FullName, "Legacy"); + } + + File.Delete(filePath); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj new file mode 100644 index 0000000000000..71efe7c433e43 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj @@ -0,0 +1,42 @@ + + + true + true + + $(NetCoreAppCurrent)-windows + + --working-dir=/test-dir + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.IO.FileSystem/tests/LegacyTests/runtimeconfig.template.json b/src/libraries/System.IO.FileSystem/tests/LegacyTests/runtimeconfig.template.json new file mode 100644 index 0000000000000..010891ed8fc6c --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/LegacyTests/runtimeconfig.template.json @@ -0,0 +1,5 @@ +{ + "configProperties": { + "System.IO.UseLegacyFileStream": true + } +} diff --git a/src/libraries/System.IO/System.IO.sln b/src/libraries/System.IO/System.IO.sln index fa248eb5af822..7d8ebc2c3da28 100644 --- a/src/libraries/System.IO/System.IO.sln +++ b/src/libraries/System.IO/System.IO.sln @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{9FDAA57A-696 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D9FD8082-D04C-4DA8-9F4C-261D1C65A6D3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.Legacy.Tests", "tests\LegacyTests\System.IO.Legacy.Tests.csproj", "{0217540D-FA86-41B3-9754-7BB5096ABA3E}" +EndProject Global GlobalSection(NestedProjects) = preSolution {D11D3624-1322-45D1-A604-7E68CDB85BE8} = {5AD2C433-C661-4AD1-BD9F-D164ADC43512} @@ -34,6 +36,7 @@ Global {D0D1CDAC-16F8-4382-A219-74A513CC1790} = {9FDAA57A-696B-4CB1-99AE-BCDF91848B75} {0769544B-1A5D-4D74-94FD-899DF6C39D62} = {D9FD8082-D04C-4DA8-9F4C-261D1C65A6D3} {AA5E80B2-A0AA-46F1-B319-5B528BAC382B} = {D9FD8082-D04C-4DA8-9F4C-261D1C65A6D3} + {0217540D-FA86-41B3-9754-7BB5096ABA3E} = {5AD2C433-C661-4AD1-BD9F-D164ADC43512} EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -76,6 +79,10 @@ Global {D0D1CDAC-16F8-4382-A219-74A513CC1790}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0D1CDAC-16F8-4382-A219-74A513CC1790}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0D1CDAC-16F8-4382-A219-74A513CC1790}.Release|Any CPU.Build.0 = Release|Any CPU + {0217540D-FA86-41B3-9754-7BB5096ABA3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0217540D-FA86-41B3-9754-7BB5096ABA3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0217540D-FA86-41B3-9754-7BB5096ABA3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0217540D-FA86-41B3-9754-7BB5096ABA3E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/libraries/System.IO/tests/LegacyTests/System.IO.Legacy.Tests.csproj b/src/libraries/System.IO/tests/LegacyTests/System.IO.Legacy.Tests.csproj new file mode 100644 index 0000000000000..370bf05d6fa5d --- /dev/null +++ b/src/libraries/System.IO/tests/LegacyTests/System.IO.Legacy.Tests.csproj @@ -0,0 +1,24 @@ + + + System.IO + true + true + true + + $(NetCoreAppCurrent)-windows + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.IO/tests/LegacyTests/runtimeconfig.template.json b/src/libraries/System.IO/tests/LegacyTests/runtimeconfig.template.json new file mode 100644 index 0000000000000..010891ed8fc6c --- /dev/null +++ b/src/libraries/System.IO/tests/LegacyTests/runtimeconfig.template.json @@ -0,0 +1,5 @@ +{ + "configProperties": { + "System.IO.UseLegacyFileStream": true + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs b/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs index 9175a3e8ba912..331ef281bb32a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs @@ -10,6 +10,20 @@ internal static class AppContextConfigHelper internal static bool GetBooleanConfig(string configName, bool defaultValue) => AppContext.TryGetSwitch(configName, out bool value) ? value : defaultValue; + internal static bool GetBooleanConfig(string switchName, string envVariable) + { + if (!AppContext.TryGetSwitch(switchName, out bool ret)) + { + string? switchValue = Environment.GetEnvironmentVariable(envVariable); + if (switchValue != null) + { + ret = bool.IsTrueStringIgnoreCase(switchValue) || switchValue.Equals("1"); + } + } + + return ret; + } + internal static int GetInt32Config(string configName, int defaultValue, bool allowNegative = true) { try diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs index 1a0bf66c42e56..590aae9b3bdc7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs @@ -10,7 +10,7 @@ internal static partial class GlobalizationMode internal static bool Invariant { get; } = GetInvariantSwitchValue(); internal static bool UseNls { get; } = !Invariant && - (GetSwitchValue("System.Globalization.UseNls", "DOTNET_SYSTEM_GLOBALIZATION_USENLS") || + (AppContextConfigHelper.GetBooleanConfig("System.Globalization.UseNls", "DOTNET_SYSTEM_GLOBALIZATION_USENLS") || !LoadIcu()); private static bool LoadIcu() diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs index 6b90460a2e2b6..2f481dd8f7311 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs @@ -9,25 +9,11 @@ namespace System.Globalization internal static partial class GlobalizationMode { private static bool GetInvariantSwitchValue() => - GetSwitchValue("System.Globalization.Invariant", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); + AppContextConfigHelper.GetBooleanConfig("System.Globalization.Invariant", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); private static bool TryGetAppLocalIcuSwitchValue([NotNullWhen(true)] out string? value) => TryGetStringValue("System.Globalization.AppLocalIcu", "DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU", out value); - private static bool GetSwitchValue(string switchName, string envVariable) - { - if (!AppContext.TryGetSwitch(switchName, out bool ret)) - { - string? switchValue = Environment.GetEnvironmentVariable(envVariable); - if (switchValue != null) - { - ret = bool.IsTrueStringIgnoreCase(switchValue) || switchValue.Equals("1"); - } - } - - return ret; - } - private static bool TryGetStringValue(string switchName, string envVariable, [NotNullWhen(true)] out string? value) { value = AppContext.GetData(switchName) as string; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs index 2541f5ec96d16..840d62cb03b80 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs @@ -19,7 +19,9 @@ internal static class FileStreamHelpers private const int ERROR_HANDLE_EOF = 38; private const int ERROR_IO_PENDING = 997; - private static readonly bool UseLegacyStrategy = Environment.GetEnvironmentVariable("DOTNET_LEGACY_FILE_IO") == "1"; + // It's enabled by default. We are going to change that (by removing !) once we fix #16354, #25905 and #24847. + internal static bool UseLegacyStrategy { get; } + = !AppContextConfigHelper.GetBooleanConfig("System.IO.UseLegacyFileStream", "DOTNET_SYSTEM_IO_USELEGACYFILESTREAM"); internal static FileStreamStrategy ChooseStrategy(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) { From 8d584376515b61a4a74868861cada2e72c843f29 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 2 Mar 2021 13:08:47 +0100 Subject: [PATCH 12/28] restore old file name to make it easier to review the code (as diff within the file, not a removal and addition) --- .../src/System.Private.CoreLib.Shared.projitems | 2 +- ...ionSource.Windows.cs => FileStreamCompletionSource.Win32.cs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/libraries/System.Private.CoreLib/src/System/IO/{FileStreamCompletionSource.Windows.cs => FileStreamCompletionSource.Win32.cs} (100%) diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index c9a7a9e8bdd0c..89ae16e953ce1 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1639,7 +1639,7 @@ - + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Win32.cs similarity index 100% rename from src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Windows.cs rename to src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Win32.cs From 33e6b8e5e63f729010d729d525b51a09af458b3c Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 2 Mar 2021 15:09:13 +0100 Subject: [PATCH 13/28] Don't set the buffer to null, to avoid a NullReferenceException when users have a race condition in their code (i.e. they call Close when calling another method on Stream like Read). --- .../src/System/IO/BufferedStream.cs | 24 ++++++++++++++++--- .../System/IO/WindowsFileStreamStrategy.cs | 10 ++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs index 926a1a2f326eb..401eb8ce2fc3e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs @@ -147,13 +147,17 @@ private void EnsureShadowBufferAllocated() _buffer = shadowBuffer; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EnsureBufferAllocated() { Debug.Assert(_bufferSize > 0); // BufferedStream is not intended for multi-threaded use, so no worries about the get/set race on _buffer. if (_buffer == null) + { + AllocateBuffer(); + } + + void AllocateBuffer() // logic kept in a separate method to get the method inlined { _buffer = new byte[_bufferSize]; @@ -294,7 +298,14 @@ protected override void Dispose(bool disposing) finally { _stream = null; - _buffer = null; + + // Don't set the buffer to null, to avoid a NullReferenceException + // when users have a race condition in their code (i.e. they call + // Close when calling another method on Stream like Read). + if (!_actLikeFileStream) + { + _buffer = null; + } // Call base.Dispose(bool) to cleanup async IO resources base.Dispose(disposing); @@ -320,7 +331,14 @@ public override async ValueTask DisposeAsync() finally { _stream = null; - _buffer = null; + + // Don't set the buffer to null, to avoid a NullReferenceException + // when users have a race condition in their code (i.e. they call + // Close when calling another method on Stream like Read). + if (!_actLikeFileStream) + { + _buffer = null; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs index a3100794ca3d8..e6f8116ddddb2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs @@ -130,8 +130,7 @@ public override void WriteByte(byte value) Write(buffer); } - // this method just disposes everything as there is no buffer here - // and we don't really need to Flush anything here + // this method just disposes everything (no buffer, no need to flush) public override ValueTask DisposeAsync() { if (_fileHandle != null && !_fileHandle.IsClosed) @@ -143,10 +142,9 @@ public override ValueTask DisposeAsync() return ValueTask.CompletedTask; } - // this method in the future will be called in no-buffering scenarios internal sealed override void DisposeInternal(bool disposing) => Dispose(disposing); - // this method is called from BufferedStream.Dispose so the content is already flushed + // this method just disposes everything (no buffer, no need to flush) protected override void Dispose(bool disposing) { if (_fileHandle != null && !_fileHandle.IsClosed) @@ -154,10 +152,6 @@ protected override void Dispose(bool disposing) _fileHandle.ThreadPoolBinding?.Dispose(); _fileHandle.Dispose(); } - - // Don't set the buffer to null, to avoid a NullReferenceException - // when users have a race condition in their code (i.e. they call - // Close when calling another method on Stream like Read). } public sealed override void Flush() => Flush(flushToDisk: false); // we have nothing to flush as there is no buffer here From 77b6d97ff2ae0817a9e843a82ef650de8bbdc6c8 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 2 Mar 2021 15:11:58 +0100 Subject: [PATCH 14/28] fix the browser build? --- .../src/System.Private.CoreLib.Shared.projitems | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 89ae16e953ce1..6f18374c8b71c 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1896,6 +1896,7 @@ + From 33c7c83feb0eecfcfbff687ea89e4544f71f9ffc Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 2 Mar 2021 20:20:06 +0100 Subject: [PATCH 15/28] reverting the flushing changes as it looks that they have introduced a bug --- .../src/System/IO/BufferedStream.cs | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs index 401eb8ce2fc3e..3260e1e4d7909 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs @@ -398,32 +398,14 @@ public override Task FlushAsync(CancellationToken cancellationToken) EnsureNotClosed(); - // try to get the lock and exit in synchronous way if there is nothing to Flush - SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); - Task semaphoreLockTask = sem.WaitAsync(cancellationToken); - bool lockAcquired = semaphoreLockTask.IsCompletedSuccessfully; - if (lockAcquired) - { - if (_writePos == 0 && _readPos == _readLen) - { - sem.Release(); - - return CanWrite ? _stream!.FlushAsync(cancellationToken) : Task.CompletedTask; - } - } - - return FlushAsyncInternal(semaphoreLockTask, lockAcquired, cancellationToken); + return FlushAsyncInternal(cancellationToken); } - private async Task FlushAsyncInternal(Task semaphoreLockTask, bool lockAcquired, CancellationToken cancellationToken) + private async Task FlushAsyncInternal(CancellationToken cancellationToken) { Debug.Assert(_stream != null); - if (!lockAcquired) - { - await semaphoreLockTask.ConfigureAwait(false); - } - + await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); try { if (_writePos > 0) @@ -464,7 +446,7 @@ private async Task FlushAsyncInternal(Task semaphoreLockTask, bool lockAcquired, } finally { - _asyncActiveSemaphore!.Release(); + _asyncActiveSemaphore.Release(); } } From e8d6f776ba0bf502f752826ca94c6f0fbfcf8b1d Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 3 Mar 2021 12:21:58 +0100 Subject: [PATCH 16/28] simplify the source code by removing some of the optimizations that are not providing too much gains --- .../src/System/IO/BufferedStream.cs | 93 ++++++++----------- 1 file changed, 41 insertions(+), 52 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs index ad8f0e8d50383..0353b9ea080e7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs @@ -468,7 +468,6 @@ private void FlushRead() /// /// Called by Write methods to clear the Read Buffer /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ClearReadBufferBeforeWrite() { Debug.Assert(_stream != null); @@ -525,7 +524,6 @@ private async ValueTask FlushWriteAsync(CancellationToken cancellationToken) await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private int ReadFromBuffer(byte[] buffer, int offset, int count) { int readbytes = _readLen - _readPos; @@ -703,8 +701,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel // an Async operation. SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); Task semaphoreLockTask = sem.WaitAsync(cancellationToken); - bool locked = semaphoreLockTask.IsCompletedSuccessfully; - if (locked) + if (semaphoreLockTask.IsCompletedSuccessfully) { // hot path #1: there is data in the buffer if (_readLen - _readPos > 0 || (count == 0 && !_actLikeFileStream)) @@ -736,20 +733,21 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); _readPos = _readLen = 0; - // start the async operation - ValueTask result = _stream!.ReadAsync(new Memory(buffer, offset, count), cancellationToken); - - // release the lock (we are not using shared state anymore) - sem.Release(); - - return result.AsTask(); + try + { + return _stream!.ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + finally + { + sem.Release(); + } } } // Delegate to the async implementation. return ReadFromUnderlyingStreamAsync( new Memory(buffer, offset + bytesFromBuffer, count - bytesFromBuffer), - cancellationToken, bytesFromBuffer, semaphoreLockTask, locked).AsTask(); + cancellationToken, bytesFromBuffer, semaphoreLockTask).AsTask(); } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) @@ -765,8 +763,7 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken int bytesFromBuffer = 0; SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); Task semaphoreLockTask = sem.WaitAsync(cancellationToken); - bool locked = semaphoreLockTask.IsCompletedSuccessfully; - if (locked) + if (semaphoreLockTask.IsCompletedSuccessfully) { // hot path #1: there is data in the buffer if (_readLen - _readPos > 0 || (buffer.Length == 0 && !_actLikeFileStream)) @@ -792,18 +789,19 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); _readPos = _readLen = 0; - // start the async operation - ValueTask result = _stream!.ReadAsync(buffer, cancellationToken); - - // release the lock (we are not using shared state anymore) - sem.Release(); - - return result; + try + { + return _stream!.ReadAsync(buffer, cancellationToken); + } + finally + { + sem.Release(); + } } } // Delegate to the async implementation. - return ReadFromUnderlyingStreamAsync(buffer, cancellationToken, bytesFromBuffer, semaphoreLockTask, locked); + return ReadFromUnderlyingStreamAsync(buffer, cancellationToken, bytesFromBuffer, semaphoreLockTask); } /// BufferedStream should be as thin a wrapper as possible. We want ReadAsync to delegate to @@ -811,7 +809,7 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken /// This allows BufferedStream to affect the semantics of the stream it wraps as little as possible. /// -2 if _bufferSize was set to 0 while waiting on the semaphore; otherwise num of bytes read. private async ValueTask ReadFromUnderlyingStreamAsync( - Memory buffer, CancellationToken cancellationToken, int bytesAlreadySatisfied, Task semaphoreLockTask, bool locked) + Memory buffer, CancellationToken cancellationToken, int bytesAlreadySatisfied, Task semaphoreLockTask) { // Same conditions validated with exceptions in ReadAsync: Debug.Assert(_stream != null); @@ -820,18 +818,14 @@ private async ValueTask ReadFromUnderlyingStreamAsync( Debug.Assert(_asyncActiveSemaphore != null); Debug.Assert(semaphoreLockTask != null); - if (!locked) - { - // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. - await semaphoreLockTask.ConfigureAwait(false); - } + // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. + await semaphoreLockTask.ConfigureAwait(false); try { int bytesFromBuffer = 0; - // we have already tried to read it from the buffer - if (!locked && (buffer.Length > 0 || !_actLikeFileStream)) + if (!(buffer.Length == 0 && _actLikeFileStream && _readLen == _readPos)) { // The buffer might have been changed by another async task while we were waiting on the semaphore. // Check it now again. @@ -1165,8 +1159,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo // Try to satisfy the request from the buffer synchronously. SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); Task semaphoreLockTask = sem.WaitAsync(cancellationToken); - bool locked = semaphoreLockTask.IsCompletedSuccessfully; - if (locked) + if (semaphoreLockTask.IsCompletedSuccessfully) { bool completeSynchronously = true; try @@ -1198,16 +1191,19 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo // serializes ALL async operations if (_actLikeFileStream && _writePos == 0 && buffer.Length >= _bufferSize) { - ValueTask result = _stream!.WriteAsync(buffer, cancellationToken); - - sem.Release(); - - return result; + try + { + return _stream!.WriteAsync(buffer, cancellationToken); + } + finally + { + sem.Release(); + } } } // Delegate to the async implementation. - return WriteToUnderlyingStreamAsync(buffer, cancellationToken, semaphoreLockTask, locked); + return WriteToUnderlyingStreamAsync(buffer, cancellationToken, semaphoreLockTask); } /// BufferedStream should be as thin a wrapper as possible. We want WriteAsync to delegate to @@ -1216,7 +1212,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo /// little as possible. /// private async ValueTask WriteToUnderlyingStreamAsync( - ReadOnlyMemory buffer, CancellationToken cancellationToken, Task semaphoreLockTask, bool locked) + ReadOnlyMemory buffer, CancellationToken cancellationToken, Task semaphoreLockTask) { Debug.Assert(_stream != null); Debug.Assert(_stream.CanWrite); @@ -1226,22 +1222,15 @@ private async ValueTask WriteToUnderlyingStreamAsync( // See the LARGE COMMENT in Write(..) for the explanation of the write buffer algorithm. - if (!locked) - { - await semaphoreLockTask.ConfigureAwait(false); - } - + await semaphoreLockTask.ConfigureAwait(false); try { - if (!locked) - { - // The buffer might have been changed by another async task while we were waiting on the semaphore. - // However, note that if we recalculate the sync completion condition to TRUE, then useBuffer will also be TRUE. + // The buffer might have been changed by another async task while we were waiting on the semaphore. + // However, note that if we recalculate the sync completion condition to TRUE, then useBuffer will also be TRUE. - if (_writePos == 0) - { - ClearReadBufferBeforeWrite(); - } + if (_writePos == 0) + { + ClearReadBufferBeforeWrite(); } int totalUserBytes; From 7913ebf57051dddceb42cf5bf1609469c9bb5018 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 10 Mar 2021 10:20:50 +0100 Subject: [PATCH 17/28] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Cantú Co-authored-by: Stephen Toub --- .../tests/LegacyTests/LegacySwitchTests.cs | 4 ++-- .../src/System/IO/AsyncWindowsFileStreamStrategy.cs | 4 ++-- .../System.Private.CoreLib/src/System/IO/BufferedStream.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/LegacyTests/LegacySwitchTests.cs b/src/libraries/System.IO.FileSystem/tests/LegacyTests/LegacySwitchTests.cs index 55d162306989c..c930cee893352 100644 --- a/src/libraries/System.IO.FileSystem/tests/LegacyTests/LegacySwitchTests.cs +++ b/src/libraries/System.IO.FileSystem/tests/LegacyTests/LegacySwitchTests.cs @@ -15,10 +15,10 @@ public static void LegacySwitchIsHonored() using (FileStream fileStream = File.Create(filePath)) { - Stream strategy = fileStream + object strategy = fileStream .GetType() .GetField("_strategy", BindingFlags.NonPublic | BindingFlags.Instance) - .GetValue(fileStream) as Stream; + .GetValue(fileStream); Assert.DoesNotContain(strategy.GetType().FullName, "Legacy"); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs index dadffb0164ffb..8f8f012d1efff 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs @@ -58,7 +58,7 @@ protected override void OnInitFromHandle(SafeFileHandle handle) // If, however, we've already bound this file handle to our completion port, // don't try to bind it again because it will fail. A handle can only be // bound to a single completion port at a time. - if (!(handle.IsAsync ?? false)) + if (handle.IsAsync != true) { try { @@ -148,7 +148,7 @@ private unsafe Task ReadAsyncInternal(Memory destination, Cancellatio // Make sure we are reading from the position that we think we are VerifyOSHandlePosition(); - if (_filePosition + destination.Length > len) + if (destination.Length > len - _filePosition) { if (_filePosition <= len) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs index 0353b9ea080e7..66302d9cd18f4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs @@ -258,7 +258,7 @@ public override long Position EnsureCanSeek(); if (_writePos > 0) - FlushWrite(true); + FlushWrite(performActualFlush: true); _readPos = 0; _readLen = 0; From 3f6b541a874746070fd3f6739c3eb08fd2fd7d4c Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 10 Mar 2021 13:10:50 +0100 Subject: [PATCH 18/28] address code review feedback --- .../tests/FileStream/SafeFileHandle.cs | 13 +++++ .../tests/FileStream/WriteAsync.cs | 12 ++++- .../IO/AsyncWindowsFileStreamStrategy.cs | 12 +++-- .../System/IO/BufferedFileStreamStrategy.cs | 5 +- .../src/System/IO/BufferedStream.cs | 53 ++++++++----------- .../src/System/IO/FileStream.cs | 2 + .../IO/LegacyFileStreamStrategy.Windows.cs | 6 +-- .../System/IO/WindowsFileStreamStrategy.cs | 18 ++----- .../src/System/ThrowHelper.cs | 30 +++++++++++ 9 files changed, 96 insertions(+), 55 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs index 020a25e48871e..92ad0ff503bdc 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs @@ -37,6 +37,19 @@ public void DisposeClosesHandle() } } + [Fact] + public void DisposingBufferedStreamThatWrapsAFileStreamWhichHasBennClosedViaSafeFileHandleCloseDoesNotThrow() + { + using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create)) + { + var bufferedStream = new BufferedStream(fs, 100); + + fs.SafeFileHandle.Dispose(); + + bufferedStream.Dispose(); // must not throw + } + } + [Fact] public void AccessFlushesFileClosesHandle() { diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs index 358cf11310838..fb4c02f0a064a 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs @@ -220,9 +220,17 @@ public async Task ManyConcurrentWriteAsyncs_OuterLoop( { writes[i] = WriteAsync(fs, expectedData, i * writeSize, writeSize, cancellationToken); Assert.Null(writes[i].Exception); - if (useAsync && PlatformDetection.IsLegacyFileStreamEnabled) + if (useAsync) { - Assert.Equal((i + 1) * writeSize, fs.Position); + // To ensure that the buffer of a FileStream opened for async IO is flushed + // by FlushAsync in asynchronous way, we aquire a lock for every buffered WriteAsync. + // The side effect of this is that the Position of FileStream is not updated until + // the lock is released by a previous operation. + // So now all WriteAsync calls should be awaited before starting another async file operation. + if (PlatformDetection.IsLegacyFileStreamEnabled) + { + Assert.Equal((i + 1) * writeSize, fs.Position); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs index 8f8f012d1efff..305544d061ef0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs @@ -27,7 +27,8 @@ internal AsyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess a public override ValueTask DisposeAsync() { - // the order matters, let the base class Dispose handle first + // the base class must dispose ThreadPoolBinding and FileHandle + // before _preallocatedOverlapped is disposed ValueTask result = base.DisposeAsync(); Debug.Assert(result.IsCompleted, "the method must be sync, as it performs no flushing"); @@ -38,7 +39,8 @@ public override ValueTask DisposeAsync() protected override void Dispose(bool disposing) { - // the order matters, let the base class Dispose handle first + // the base class must dispose ThreadPoolBinding and FileHandle + // before _preallocatedOverlapped is disposed base.Dispose(disposing); _preallocatedOverlapped?.Dispose(); @@ -389,9 +391,9 @@ private async Task AsyncModeCopyToAsync(Stream destination, int bufferSize, Canc try { -#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task - await FileStreamHelpers.AsyncModeCopyToAsync(_fileHandle, _path, canSeek, _filePosition, destination, bufferSize, cancellationToken); -#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + await FileStreamHelpers + .AsyncModeCopyToAsync(_fileHandle, _path, canSeek, _filePosition, destination, bufferSize, cancellationToken) + .ConfigureAwait(false); } finally { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs index bf5c50e2cfbfb..dc04c4bb1724b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs @@ -33,7 +33,7 @@ internal BufferedFileStreamStrategy(FileStreamStrategy strategy, int bufferSize) public override long Position { - get => _bufferedStream.GetPositionWithoutFlushing(); + get => _bufferedStream.Position; set => _bufferedStream.Position = value; } @@ -106,6 +106,9 @@ public override Task FlushAsync(CancellationToken cancellationToken) public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => _bufferedStream.CopyToAsync(destination, bufferSize, cancellationToken); + public override void CopyTo(Stream destination, int bufferSize) + => _bufferedStream.CopyTo(destination, bufferSize); + public override ValueTask DisposeAsync() => _bufferedStream.DisposeAsync(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs index 66302d9cd18f4..195b3002d2b2c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs @@ -97,9 +97,7 @@ internal BufferedStream(Stream stream, int bufferSize, bool actLikeFileStream) : private void EnsureNotClosed() { if (_stream == null) - Throw(); - - static void Throw() => throw new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); } private void EnsureCanSeek() @@ -107,9 +105,7 @@ private void EnsureCanSeek() Debug.Assert(_stream != null); if (!_stream.CanSeek) - Throw(); - - static void Throw() => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } private void EnsureCanRead() @@ -117,9 +113,7 @@ private void EnsureCanRead() Debug.Assert(_stream != null); if (!_stream.CanRead) - Throw(); - - static void Throw() => throw new NotSupportedException(SR.NotSupported_UnreadableStream); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } private void EnsureCanWrite() @@ -127,9 +121,7 @@ private void EnsureCanWrite() Debug.Assert(_stream != null); if (!_stream.CanWrite) - Throw(); - - static void Throw() => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } private void EnsureShadowBufferAllocated() @@ -224,7 +216,6 @@ public override long Length } } - // this method exists to keep old FileStream behaviour and don't perform a Flush when getting Length internal long GetLengthWithoutFlushing() { Debug.Assert(_actLikeFileStream); @@ -233,6 +224,9 @@ internal long GetLengthWithoutFlushing() long len = _stream!.Length; + // If we're writing near the end of the file, we must include our + // internal buffer in our Length calculation. Don't flush because + // we use the length of the file in our async write method. if (_writePos > 0 && _stream!.Position + _writePos > len) len = _writePos + _stream!.Position; @@ -252,7 +246,7 @@ public override long Position set { if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); EnsureNotClosed(); EnsureCanSeek(); @@ -266,17 +260,6 @@ public override long Position } } - // this method exists to keep old FileStream behaviour and don't perform a Flush when getting Position - internal long GetPositionWithoutFlushing() - { - Debug.Assert(_actLikeFileStream); - - EnsureNotClosed(); - EnsureCanSeek(); - - return (_stream!.Position - _readLen) + _readPos + _writePos; - } - internal void DisposeInternal(bool disposing) => Dispose(disposing); protected override void Dispose(bool disposing) @@ -309,6 +292,9 @@ protected override void Dispose(bool disposing) // Call base.Dispose(bool) to cleanup async IO resources base.Dispose(disposing); + + // ensure that all positions are set to 0 + Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); } } @@ -339,12 +325,16 @@ public override async ValueTask DisposeAsync() { _buffer = null; } + + // ensure that all positions are set to 0 + Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); } } public override void Flush() => Flush(true); - internal void Flush(bool performActualFlush) + // flushUnderlyingStream can be set to false by BufferedFileStreamStrategy.Flush(bool flushToDisk) + internal void Flush(bool flushUnderlyingStream) { EnsureNotClosed(); @@ -356,7 +346,7 @@ internal void Flush(bool performActualFlush) // so to avoid getting exception here, we just ensure that we can Write before doing it if (_stream!.CanWrite) { - FlushWrite(performActualFlush); + FlushWrite(flushUnderlyingStream); Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); return; } @@ -376,7 +366,7 @@ internal void Flush(bool performActualFlush) // User streams may have opted to throw from Flush if CanWrite is false (although the abstract Stream does not do so). // However, if we do not forward the Flush to the underlying stream, we may have problems when chaining several streams. // Let us make a best effort attempt: - if (performActualFlush && _stream.CanWrite) + if (flushUnderlyingStream && _stream.CanWrite) _stream.Flush(); // If the Stream was seekable, then we should have called FlushRead which resets _readPos & _readLen. @@ -385,7 +375,7 @@ internal void Flush(bool performActualFlush) } // We had no data in the buffer, but we still need to tell the underlying stream to flush. - if (performActualFlush && _stream!.CanWrite) + if (flushUnderlyingStream && _stream!.CanWrite) _stream.Flush(); _writePos = _readPos = _readLen = 0; @@ -487,11 +477,12 @@ private void ClearReadBufferBeforeWrite() // However, since the user did not call a method that is intuitively expected to seek, a better message is in order. // Ideally, we would throw an InvalidOperation here, but for backward compat we have to stick with NotSupported. if (!_stream.CanSeek) - Throw(); + ThrowNotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed(); FlushRead(); - static void Throw() => throw new NotSupportedException(SR.NotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed); + static void ThrowNotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed() + => throw new NotSupportedException(SR.NotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed); } private void FlushWrite(bool performActualFlush) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs index 6109cf014298c..2af6d8125e20e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -403,6 +403,8 @@ public override long Position public override ValueTask DisposeAsync() => _strategy.DisposeAsync(); + public override void CopyTo(Stream destination, int bufferSize) => _strategy.CopyTo(destination, bufferSize); + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => _strategy.CopyToAsync(destination, bufferSize, cancellationToken); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs index 0e236cfa5097f..ed8f5151992cd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs @@ -1153,9 +1153,9 @@ private async Task AsyncModeCopyToAsync(Stream destination, int bufferSize, Canc try { -#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task - await FileStreamHelpers.AsyncModeCopyToAsync(_fileHandle, _path, canSeek, _filePosition, destination, bufferSize, cancellationToken); -#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + await FileStreamHelpers + .AsyncModeCopyToAsync(_fileHandle, _path, canSeek, _filePosition, destination, bufferSize, cancellationToken) + .ConfigureAwait(false); } finally { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs index e6f8116ddddb2..89654bfa90551 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs @@ -19,21 +19,13 @@ internal abstract class WindowsFileStreamStrategy : FileStreamStrategy protected const int ERROR_IO_PENDING = 997; protected readonly SafeFileHandle _fileHandle; // only ever null if ctor throws - - /// Whether the file is opened for reading, writing, or both. - private readonly FileAccess _access; - - /// The path to the opened file. - protected readonly string? _path; + protected readonly string? _path; // The path to the opened file. + private readonly FileAccess _access; // What file was opened for. + private readonly bool _canSeek; // Whether can seek (file) or not (pipe). + private readonly bool _isPipe; // Whether to disable async buffering code. protected long _filePosition; - - private readonly bool _canSeek; - private readonly bool _isPipe; // Whether to disable async buffering code. - - /// Whether the file stream's handle has been exposed. - protected bool _exposedHandle; - + protected bool _exposedHandle; // Whether the file stream's handle has been exposed. private long _appendStart; // When appending, prevent overwriting file. internal WindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access) diff --git a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs index 8667c198913bc..ad9f0e563271d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs @@ -307,6 +307,24 @@ internal static void ThrowNotSupportedException(ExceptionResource resource) throw new NotSupportedException(GetResourceString(resource)); } + [DoesNotReturn] + internal static void ThrowNotSupportedException_UnseekableStream() + { + throw new NotSupportedException(SR.NotSupported_UnseekableStream); + } + + [DoesNotReturn] + internal static void ThrowNotSupportedException_UnreadableStream() + { + throw new NotSupportedException(SR.NotSupported_UnreadableStream); + } + + [DoesNotReturn] + internal static void ThrowNotSupportedException_UnwritableStream() + { + throw new NotSupportedException(SR.NotSupported_UnwritableStream); + } + [DoesNotReturn] internal static void ThrowUnauthorizedAccessException(ExceptionResource resource) { @@ -319,6 +337,18 @@ internal static void ThrowObjectDisposedException(string objectName, ExceptionRe throw new ObjectDisposedException(objectName, GetResourceString(resource)); } + [DoesNotReturn] + internal static void ThrowObjectDisposedException_StreamClosed(string? objectName) + { + throw new ObjectDisposedException(objectName, SR.ObjectDisposed_StreamClosed); + } + + [DoesNotReturn] + internal static void ThrowObjectDisposedException_FileClosed() + { + throw new ObjectDisposedException(null, SR.ObjectDisposed_FileClosed); + } + [DoesNotReturn] internal static void ThrowObjectDisposedException(ExceptionResource resource) { From 75569e3174f6cbf8386c19df755e611525ef0a40 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 10 Mar 2021 13:26:09 +0100 Subject: [PATCH 19/28] use ThrowHelper in other places in System.IO --- .../src/Internal/IO/File.cs | 2 +- .../System.Private.CoreLib.Shared.projitems | 1 - .../IO/AsyncWindowsFileStreamStrategy.cs | 12 ++--- .../src/System/IO/BinaryReader.cs | 14 +++--- .../src/System/IO/BufferedStream.cs | 6 +-- .../src/System/IO/Error.cs | 41 ----------------- .../src/System/IO/FileStream.cs | 46 +++++++++---------- .../IO/LegacyFileStreamStrategy.Unix.cs | 10 ++-- .../IO/LegacyFileStreamStrategy.Windows.cs | 20 ++++---- .../src/System/IO/LegacyFileStreamStrategy.cs | 12 ++--- .../src/System/IO/MemoryStream.cs | 6 +-- .../src/System/IO/Stream.cs | 31 ++++++++----- .../src/System/IO/StreamReader.cs | 2 +- .../src/System/IO/StreamWriter.cs | 2 +- .../IO/SyncWindowsFileStreamStrategy.cs | 6 +-- .../src/System/IO/UnmanagedMemoryStream.cs | 10 ++-- .../System/IO/UnmanagedMemoryStreamWrapper.cs | 9 ++-- .../System/IO/WindowsFileStreamStrategy.cs | 4 +- .../src/System/Resources/ResourceReader.cs | 2 +- .../src/System/Text/TranscodingStream.cs | 12 ++--- .../src/System/ThrowHelper.cs | 17 ++++--- .../src/System/TimeZoneInfo.Unix.cs | 2 +- 22 files changed, 118 insertions(+), 149 deletions(-) delete mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/Error.cs diff --git a/src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs b/src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs index 34bff3febe863..127d72daa2e36 100644 --- a/src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs @@ -64,7 +64,7 @@ public static byte[] ReadAllBytes(string path) { int n = fs.Read(bytes, index, count); if (n == 0) - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); index += n; count -= n; } diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index fc0165200312a..91cf76a307f24 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -398,7 +398,6 @@ - diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs index 305544d061ef0..f9b061094222c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs @@ -133,7 +133,7 @@ private unsafe Task ReadAsyncInternal(Memory destination, Cancellatio { if (!CanRead) { - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); @@ -216,7 +216,7 @@ private unsafe Task ReadAsyncInternal(Memory destination, Cancellatio if (errorCode == ERROR_HANDLE_EOF) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } else { @@ -261,7 +261,7 @@ private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, Cancella { if (!CanWrite) { - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); @@ -328,7 +328,7 @@ private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, Cancella if (errorCode == ERROR_HANDLE_EOF) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } else { @@ -362,11 +362,11 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio // Fail if the file was closed if (_fileHandle.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } if (!CanRead) { - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } // Bail early for cancellation if cancellation has been requested diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs index 2f1b8ba21d509..39532de1f3e11 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs @@ -114,7 +114,7 @@ private void ThrowIfDisposed() { if (_disposed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } } @@ -214,7 +214,7 @@ private byte InternalReadByte() int b = _stream.ReadByte(); if (b == -1) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } return (byte)b; @@ -229,7 +229,7 @@ public virtual char ReadChar() int value = Read(); if (value == -1) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } return (char)value; } @@ -296,7 +296,7 @@ public virtual string ReadString() n = _stream.Read(_charBytes, 0, readLength); if (n == 0) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } charsRead = _decoder.GetChars(_charBytes, 0, n, _charBuffer, 0); @@ -536,7 +536,7 @@ private ReadOnlySpan InternalRead(int numBytes) int n = _stream.Read(_buffer, bytesRead, numBytes - bytesRead); if (n == 0) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } bytesRead += n; } while (bytesRead < numBytes); @@ -569,7 +569,7 @@ protected virtual void FillBuffer(int numBytes) n = _stream.ReadByte(); if (n == -1) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } _buffer[0] = (byte)n; @@ -581,7 +581,7 @@ protected virtual void FillBuffer(int numBytes) n = _stream.Read(_buffer, bytesRead, numBytes - bytesRead); if (n == 0) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } bytesRead += n; } while (bytesRead < numBytes); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs index 195b3002d2b2c..c2ed2286dd775 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs @@ -74,7 +74,7 @@ public BufferedStream(Stream stream) public BufferedStream(Stream stream, int bufferSize) { if (stream == null) - throw new ArgumentNullException(nameof(stream)); + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stream); if (bufferSize <= 0) throw new ArgumentOutOfRangeException(nameof(bufferSize), SR.Format(SR.ArgumentOutOfRange_MustBePositive, nameof(bufferSize))); @@ -86,7 +86,7 @@ public BufferedStream(Stream stream, int bufferSize) // & writes are greater than or equal to buffer size. if (!_stream.CanRead && !_stream.CanWrite) - throw new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); } internal BufferedStream(Stream stream, int bufferSize, bool actLikeFileStream) : this(stream, bufferSize) @@ -1380,7 +1380,7 @@ public override long Seek(long offset, SeekOrigin origin) public override void SetLength(long value) { if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); EnsureNotClosed(); EnsureCanSeek(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Error.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Error.cs deleted file mode 100644 index d2e483433a872..0000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Error.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.IO -{ - /// - /// Provides centralized methods for creating exceptions for System.IO.FileSystem. - /// - internal static class Error - { - internal static Exception GetStreamIsClosed() - { - return new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); - } - - internal static Exception GetEndOfFile() - { - return new EndOfStreamException(SR.IO_EOF_ReadBeyondEOF); - } - - internal static Exception GetFileNotOpen() - { - return new ObjectDisposedException(null, SR.ObjectDisposed_FileClosed); - } - - internal static Exception GetReadNotSupported() - { - return new NotSupportedException(SR.NotSupported_UnreadableStream); - } - - internal static Exception GetSeekNotSupported() - { - return new NotSupportedException(SR.NotSupported_UnseekableStream); - } - - internal static Exception GetWriteNotSupported() - { - return new NotSupportedException(SR.NotSupported_UnwritableStream); - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs index 2af6d8125e20e..20e5f73bb0efd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -73,7 +73,7 @@ private static void ValidateHandle(SafeFileHandle handle, FileAccess access, int throw new ArgumentOutOfRangeException(nameof(bufferSize), SR.ArgumentOutOfRange_NeedPosNum); if (handle.IsClosed) - throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); if (handle.IsAsync.HasValue && isAsync != handle.IsAsync.GetValueOrDefault()) throw new ArgumentException(SR.Arg_HandleNotAsync, nameof(handle)); } @@ -180,7 +180,7 @@ public virtual void Lock(long position, long length) if (_strategy.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } _strategy.Lock(position, length); @@ -196,7 +196,7 @@ public virtual void Unlock(long position, long length) if (_strategy.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } _strategy.Unlock(position, length); @@ -210,7 +210,7 @@ public override Task FlushAsync(CancellationToken cancellationToken) } if (_strategy.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } return _strategy.FlushAsync(cancellationToken); @@ -233,7 +233,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel return Task.FromCanceled(cancellationToken); if (_strategy.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); return _strategy.ReadAsync(buffer, offset, count, cancellationToken); } @@ -247,7 +247,7 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken if (_strategy.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } return _strategy.ReadAsync(buffer, cancellationToken); @@ -270,7 +270,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati return Task.FromCanceled(cancellationToken); if (_strategy.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); return _strategy.WriteAsync(buffer, offset, count, cancellationToken); } @@ -284,7 +284,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo if (_strategy.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } return _strategy.WriteAsync(buffer, cancellationToken); @@ -305,7 +305,7 @@ public override void Flush() /// public virtual void Flush(bool flushToDisk) { - if (_strategy.IsClosed) throw Error.GetFileNotOpen(); + if (_strategy.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); _strategy.Flush(flushToDisk); } @@ -324,7 +324,7 @@ private void ValidateReadWriteArgs(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); if (_strategy.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } /// Sets the length of this stream to the given value. @@ -332,13 +332,13 @@ private void ValidateReadWriteArgs(byte[] buffer, int offset, int count) public override void SetLength(long value) { if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); if (_strategy.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); if (!CanSeek) - throw Error.GetSeekNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); if (!CanWrite) - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); _strategy.SetLength(value); } @@ -356,8 +356,8 @@ public override long Length { get { - if (_strategy.IsClosed) throw Error.GetFileNotOpen(); - if (!CanSeek) throw Error.GetSeekNotSupported(); + if (_strategy.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); + if (!CanSeek) ThrowHelper.ThrowNotSupportedException_UnseekableStream(); return _strategy.Length; } } @@ -368,17 +368,17 @@ public override long Position get { if (_strategy.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); if (!CanSeek) - throw Error.GetSeekNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); return _strategy.Position; } set { if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); _strategy.Seek(value, SeekOrigin.Begin); } @@ -411,8 +411,8 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { ValidateBufferArguments(buffer, offset, count); - if (_strategy.IsClosed) throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed); - if (!CanRead) throw new NotSupportedException(SR.NotSupported_UnreadableStream); + if (_strategy.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); + if (!CanRead) ThrowHelper.ThrowNotSupportedException_UnreadableStream(); return _strategy.BeginRead(buffer, offset, count, callback, state); } @@ -428,8 +428,8 @@ public override int EndRead(IAsyncResult asyncResult) public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { ValidateBufferArguments(buffer, offset, count); - if (_strategy.IsClosed) throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed); - if (!CanWrite) throw new NotSupportedException(SR.NotSupported_UnwritableStream); + if (_strategy.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); + if (!CanWrite) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); return _strategy.BeginWrite(buffer, offset, count, callback, state); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs index c4cbd842c1ee7..26818014bbbcf 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs @@ -395,7 +395,7 @@ private unsafe int ReadNative(Span buffer) if (!CanRead) // match Windows behavior; this gets thrown synchronously { - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } // Serialize operations using the semaphore. @@ -559,11 +559,11 @@ private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationTo return ValueTask.FromCanceled(cancellationToken); if (_fileHandle.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); if (!CanWrite) // match Windows behavior; this gets thrown synchronously { - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } // Serialize operations using the semaphore. @@ -637,11 +637,11 @@ public override long Seek(long offset, SeekOrigin origin) } if (_fileHandle.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } if (!CanSeek) { - throw Error.GetSeekNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } VerifyOSHandlePosition(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs index ed8f5151992cd..ab8cc977a5e05 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs @@ -348,7 +348,7 @@ private int ReadSpan(Span destination) // buffer, depending on number of bytes user asked for and buffer size. if (n == 0) { - if (!CanRead) throw Error.GetReadNotSupported(); + if (!CanRead) ThrowHelper.ThrowNotSupportedException_UnreadableStream(); if (_writePos > 0) FlushWriteBuffer(); if (!CanSeek || (destination.Length >= _bufferLength)) { @@ -449,8 +449,8 @@ public override long Seek(long offset, SeekOrigin origin) { if (origin < SeekOrigin.Begin || origin > SeekOrigin.End) throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)); - if (_fileHandle.IsClosed) throw Error.GetFileNotOpen(); - if (!CanSeek) throw Error.GetSeekNotSupported(); + if (_fileHandle.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); + if (!CanSeek) ThrowHelper.ThrowNotSupportedException_UnseekableStream(); Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); @@ -561,7 +561,7 @@ private void WriteSpan(ReadOnlySpan source) if (_writePos == 0) { // Ensure we can write to the stream, and ready buffer for writing. - if (!CanWrite) throw Error.GetWriteNotSupported(); + if (!CanWrite) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); if (_readPos < _readLength) FlushReadBuffer(); _readPos = 0; _readLength = 0; @@ -655,7 +655,7 @@ private unsafe void WriteCore(ReadOnlySpan source) private Task? ReadAsyncInternal(Memory destination, CancellationToken cancellationToken, out int synchronousResult) { Debug.Assert(_useAsyncIO); - if (!CanRead) throw Error.GetReadNotSupported(); + if (!CanRead) ThrowHelper.ThrowNotSupportedException_UnreadableStream(); Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); @@ -838,7 +838,7 @@ private unsafe Task ReadNativeAsync(Memory destination, int numBuffer if (errorCode == ERROR_HANDLE_EOF) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } else { @@ -871,7 +871,7 @@ private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationTo Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); Debug.Assert(!_isPipe || (_readPos == 0 && _readLength == 0), "Win32FileStream must not have buffered data here! Pipes should be unidirectional."); - if (!CanWrite) throw Error.GetWriteNotSupported(); + if (!CanWrite) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); bool writeDataStoredInBuffer = false; if (!_isPipe) // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadInternalAsyncCore) @@ -1040,7 +1040,7 @@ private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, Cancella if (errorCode == ERROR_HANDLE_EOF) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } else { @@ -1103,11 +1103,11 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio // Fail if the file was closed if (_fileHandle.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } if (!CanRead) { - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } // Bail early for cancellation if cancellation has been requested diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs index e11d395d936b9..5d90aecac6ac7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs @@ -144,7 +144,7 @@ public override int Read(Span buffer) { if (_fileHandle.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } return ReadSpan(buffer); @@ -224,7 +224,7 @@ public override void Write(ReadOnlySpan buffer) { if (_fileHandle.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } WriteSpan(buffer); @@ -345,9 +345,9 @@ private void AssertBufferInvariants() private void PrepareForReading() { if (_fileHandle.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); if (_readLength == 0 && !CanRead) - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); AssertBufferInvariants(); } @@ -480,14 +480,14 @@ public override void WriteByte(byte value) private void PrepareForWriting() { if (_fileHandle.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); // Make sure we're good to write. We only need to do this if there's nothing already // in our write buffer, since if there is something in the buffer, we've already done // this checking and flushing. if (_writePos == 0) { - if (!CanWrite) throw Error.GetWriteNotSupported(); + if (!CanWrite) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); FlushReadBuffer(); Debug.Assert(_bufferLength > 0, "_bufferSize > 0"); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs index 59316e780c843..d9b30c0b2aaad 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs @@ -108,13 +108,13 @@ public MemoryStream(byte[] buffer, int index, int count, bool writable, bool pub private void EnsureNotClosed() { if (!_isOpen) - throw Error.GetStreamIsClosed(); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); } private void EnsureWriteable() { if (!CanWrite) - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } protected override void Dispose(bool disposing) @@ -233,7 +233,7 @@ internal ReadOnlySpan InternalReadSpan(int count) if ((uint)newPos > (uint)_length) { _position = _length; - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } var span = new ReadOnlySpan(_buffer, origPos, count); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs index 6a364614ef99a..71ad1dbeddc48 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs @@ -55,9 +55,12 @@ public virtual void CopyTo(Stream destination, int bufferSize) ValidateCopyToArguments(destination, bufferSize); if (!CanRead) { - throw CanWrite ? (Exception) - new NotSupportedException(SR.NotSupported_UnreadableStream) : - new ObjectDisposedException(GetType().Name, SR.ObjectDisposed_StreamClosed); + if (CanWrite) + { + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); + } + + ThrowHelper.ThrowObjectDisposedException_StreamClosed(GetType().Name); } byte[] buffer = ArrayPool.Shared.Rent(bufferSize); @@ -86,9 +89,12 @@ public virtual Task CopyToAsync(Stream destination, int bufferSize, Cancellation ValidateCopyToArguments(destination, bufferSize); if (!CanRead) { - throw CanWrite ? (Exception) - new NotSupportedException(SR.NotSupported_UnreadableStream) : - new ObjectDisposedException(GetType().Name, SR.ObjectDisposed_StreamClosed); + if (CanWrite) + { + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); + } + + ThrowHelper.ThrowObjectDisposedException_StreamClosed(GetType().Name); } return Core(this, destination, bufferSize, cancellationToken); @@ -202,7 +208,7 @@ internal IAsyncResult BeginReadInternal( ValidateBufferArguments(buffer, offset, count); if (!CanRead) { - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } // To avoid a race with a stream's position pointer & generating race conditions @@ -359,7 +365,7 @@ internal IAsyncResult BeginWriteInternal( ValidateBufferArguments(buffer, offset, count); if (!CanWrite) { - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } // To avoid a race condition with a stream's position pointer & generating conditions @@ -759,9 +765,12 @@ protected static void ValidateCopyToArguments(Stream destination, int bufferSize if (!destination.CanWrite) { - throw destination.CanRead ? (Exception) - new NotSupportedException(SR.NotSupported_UnwritableStream) : - new ObjectDisposedException(destination.GetType().Name, SR.ObjectDisposed_StreamClosed); + if (destination.CanRead) + { + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); + } + + ThrowHelper.ThrowObjectDisposedException_StreamClosed(destination.GetType().Name); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs index 848da5bc1e62b..2e2821a2b17d6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @@ -138,7 +138,7 @@ public StreamReader(Stream stream, Encoding? encoding = null, bool detectEncodin { if (stream == null) { - throw new ArgumentNullException(nameof(stream)); + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stream); } if (encoding == null) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs index 7b1004b0800c9..ce84fbfe85b7e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs @@ -95,7 +95,7 @@ public StreamWriter(Stream stream, Encoding? encoding = null, int bufferSize = - { if (stream == null) { - throw new ArgumentNullException(nameof(stream)); + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stream); } if (encoding == null) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs index 44563299c579f..30d01134481cb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs @@ -67,7 +67,7 @@ public override void Write(ReadOnlySpan buffer) { if (_fileHandle.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } WriteSpan(buffer); @@ -99,7 +99,7 @@ private unsafe int ReadSpan(Span destination) { if (!CanRead) { - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); @@ -134,7 +134,7 @@ private unsafe void WriteSpan(ReadOnlySpan source) { if (!CanWrite) { - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStream.cs index f0d48773eebf5..c116c17e69ad8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStream.cs @@ -215,19 +215,19 @@ protected override void Dispose(bool disposing) private void EnsureNotClosed() { if (!_isOpen) - throw Error.GetStreamIsClosed(); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); } private void EnsureReadable() { if (!CanRead) - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } private void EnsureWriteable() { if (!CanWrite) - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } /// @@ -290,13 +290,13 @@ public override long Position { get { - if (!CanSeek) throw Error.GetStreamIsClosed(); + if (!CanSeek) ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); return Interlocked.Read(ref _position); } set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); - if (!CanSeek) throw Error.GetStreamIsClosed(); + if (!CanSeek) ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); Interlocked.Exchange(ref _position, value); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStreamWrapper.cs b/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStreamWrapper.cs index 4851eb6098d2e..b1adbb1c022c4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStreamWrapper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStreamWrapper.cs @@ -148,17 +148,16 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio throw new ArgumentOutOfRangeException(nameof(bufferSize), SR.ArgumentOutOfRange_NeedPosNum); if (!CanRead && !CanWrite) - throw new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); if (!destination.CanRead && !destination.CanWrite) - throw new ObjectDisposedException(nameof(destination), SR.ObjectDisposed_StreamClosed); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(nameof(destination)); if (!CanRead) - throw new NotSupportedException(SR.NotSupported_UnreadableStream); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); if (!destination.CanWrite) - throw new NotSupportedException(SR.NotSupported_UnwritableStream); - + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); return _unmanagedStream.CopyToAsync(destination, bufferSize, cancellationToken); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs index 89654bfa90551..2dfabe1025a47 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs @@ -160,8 +160,8 @@ public sealed override long Seek(long offset, SeekOrigin origin) { if (origin < SeekOrigin.Begin || origin > SeekOrigin.End) throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)); - if (_fileHandle.IsClosed) throw Error.GetFileNotOpen(); - if (!CanSeek) throw Error.GetSeekNotSupported(); + if (_fileHandle.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); + if (!CanSeek) ThrowHelper.ThrowNotSupportedException_UnseekableStream(); // Verify that internal position is in sync with the handle VerifyOSHandlePosition(); diff --git a/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs b/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs index 270df3a8b9cb1..bf2de12776a23 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs @@ -116,7 +116,7 @@ public sealed partial class #endif { if (stream == null) - throw new ArgumentNullException(nameof(stream)); + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stream); if (!stream.CanRead) throw new ArgumentException(SR.Argument_StreamNotReadable); diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/TranscodingStream.cs b/src/libraries/System.Private.CoreLib/src/System/Text/TranscodingStream.cs index d807d910b429d..5ad89cd3f98a6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/TranscodingStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/TranscodingStream.cs @@ -79,7 +79,7 @@ internal TranscodingStream(Stream innerStream, Encoding innerEncoding, Encoding public override long Position { get => throw new NotSupportedException(SR.NotSupported_UnseekableStream); - set => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + set => ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) @@ -185,7 +185,7 @@ void InitializeReadDataStructures() { if (!CanRead) { - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } _innerDecoder = _innerEncoding.GetDecoder(); @@ -217,7 +217,7 @@ void InitializeReadDataStructures() { if (!CanWrite) { - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } _innerEncoder = _innerEncoding.GetEncoder(); @@ -428,7 +428,7 @@ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(SR.NotSupported_UnseekableStream); public override void SetLength(long value) - => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + => ThrowHelper.ThrowNotSupportedException_UnseekableStream(); [StackTraceHidden] private void ThrowIfDisposed() @@ -443,9 +443,7 @@ private void ThrowIfDisposed() [StackTraceHidden] private void ThrowObjectDisposedException() { - throw new ObjectDisposedException( - objectName: GetType().Name, - message: SR.ObjectDisposed_StreamClosed); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(GetType().Name); } public override void Write(byte[] buffer, int offset, int count) diff --git a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs index ad9f0e563271d..7862e29b070a3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs @@ -38,6 +38,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.Serialization; @@ -218,15 +219,10 @@ internal static void ThrowArgumentException(ExceptionResource resource, Exceptio throw GetArgumentException(resource, argument); } - private static ArgumentNullException GetArgumentNullException(ExceptionArgument argument) - { - return new ArgumentNullException(GetArgumentName(argument)); - } - [DoesNotReturn] internal static void ThrowArgumentNullException(ExceptionArgument argument) { - throw GetArgumentNullException(argument); + throw new ArgumentNullException(GetArgumentName(argument)); } [DoesNotReturn] @@ -259,6 +255,12 @@ internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument throw GetArgumentOutOfRangeException(argument, paramNumber, resource); } + [DoesNotReturn] + internal static void ThrowEndOfFileException() + { + throw new EndOfStreamException(SR.IO_EOF_ReadBeyondEOF); + } + [DoesNotReturn] internal static void ThrowInvalidOperationException() { @@ -745,6 +747,8 @@ private static string GetArgumentName(ExceptionArgument argument) return "buffer"; case ExceptionArgument.offset: return "offset"; + case ExceptionArgument.stream: + return "stream"; default: Debug.Fail("The enum value is not defined, please check the ExceptionArgument Enum."); return ""; @@ -1005,6 +1009,7 @@ internal enum ExceptionArgument suffix, buffer, offset, + stream } // diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs index 614541a0cf93d..e5724c4c52a63 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -547,7 +547,7 @@ private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] r { int n = stream.Read(buffer, index, count); if (n == 0) - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); int end = index + n; for (; index < end; index++) From 19bb52483c1962687d4ac7f11d98f843d6f120c7 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 10 Mar 2021 13:37:18 +0100 Subject: [PATCH 20/28] update test projects based on most recent changes in main branch --- .../System.IO.FileSystem.Legacy.Tests.csproj | 14 ++++---------- .../LegacyTests/System.IO.Legacy.Tests.csproj | 5 +++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj index 71efe7c433e43..656a16c52b0c9 100644 --- a/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj @@ -28,15 +28,9 @@ - - - - - - - - - - + + + + diff --git a/src/libraries/System.IO/tests/LegacyTests/System.IO.Legacy.Tests.csproj b/src/libraries/System.IO/tests/LegacyTests/System.IO.Legacy.Tests.csproj index 370bf05d6fa5d..bfc33e36807b6 100644 --- a/src/libraries/System.IO/tests/LegacyTests/System.IO.Legacy.Tests.csproj +++ b/src/libraries/System.IO/tests/LegacyTests/System.IO.Legacy.Tests.csproj @@ -14,11 +14,12 @@ - - + + + From dd8c4b95017136b9496b2943ca7463d83b8f24be Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 10 Mar 2021 13:41:46 +0100 Subject: [PATCH 21/28] fix a path that I've broken in previous commit --- .../tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj index 656a16c52b0c9..b1cf5ce99b0c9 100644 --- a/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj @@ -28,7 +28,7 @@ - + From 737c73c5c45f194db60bb9b17a784d6c989760ba Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 10 Mar 2021 14:09:46 +0100 Subject: [PATCH 22/28] revert the ResourceReader change that broke Mono build --- .../src/System/Resources/ResourceReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs b/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs index bf2de12776a23..270df3a8b9cb1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs @@ -116,7 +116,7 @@ public sealed partial class #endif { if (stream == null) - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stream); + throw new ArgumentNullException(nameof(stream)); if (!stream.CanRead) throw new ArgumentException(SR.Argument_StreamNotReadable); From 4a5497a2d4fca27aa2b018f2f5545d217ce8ca6c Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 10 Mar 2021 14:51:55 +0100 Subject: [PATCH 23/28] restore old test behavior, but keep it only for Legacy FileStream --- .../tests/FileStream/SafeFileHandle.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs index 92ad0ff503bdc..f644f996c8550 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs @@ -109,7 +109,20 @@ private async Task ThrowWhenHandlePositionIsChanged(bool useAsync) // Put data in FS write buffer and update position from FSR fs.WriteByte(0); fsr.Position = 0; - Assert.Throws(() => fs.Position); + + if (useAsync + // Async I/O behaviors differ due to kernel-based implementation on Windows + && OperatingSystem.IsWindows() + // ReadAsync which in this case (single byte written to buffer) calls FlushAsync is now 100% async + // so it does not complete synchronously anymore + && PlatformDetection.IsLegacyFileStreamEnabled) + { + Assert.Throws(() => FSAssert.CompletesSynchronously(fs.ReadAsync(new byte[1], 0, 1))); + } + else + { + await Assert.ThrowsAsync(() => fs.ReadAsync(new byte[1], 0, 1)); + } fs.WriteByte(0); fsr.Position++; From f01948dd21f39447a818f8addf2561a119d0d059 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 10 Mar 2021 16:24:33 +0100 Subject: [PATCH 24/28] move buffering logic to BufferedFileStreamStrategy --- .../tests/FileStream/SafeFileHandle.cs | 13 +- .../IO/AsyncWindowsFileStreamStrategy.cs | 5 +- .../System/IO/BufferedFileStreamStrategy.cs | 1233 ++++++++++++++++- .../src/System/IO/BufferedStream.cs | 231 +-- .../src/System/IO/FileStreamStrategy.cs | 2 + 5 files changed, 1245 insertions(+), 239 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs index f644f996c8550..57ce00dea3d21 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs @@ -38,16 +38,11 @@ public void DisposeClosesHandle() } [Fact] - public void DisposingBufferedStreamThatWrapsAFileStreamWhichHasBennClosedViaSafeFileHandleCloseDoesNotThrow() + public void DisposingBufferedFileStreamThatWasClosedViaSafeFileHandleCloseDoesNotThrow() { - using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create)) - { - var bufferedStream = new BufferedStream(fs, 100); - - fs.SafeFileHandle.Dispose(); - - bufferedStream.Dispose(); // must not throw - } + FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, bufferSize: 100); + fs.SafeFileHandle.Dispose(); + fs.Dispose(); // must not throw } [Fact] diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs index f9b061094222c..918d9afc88337 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs @@ -104,10 +104,9 @@ protected override void OnInit() } } - // called by BufferedStream - internal void OnBufferAllocated(byte[] buffer) + // called by BufferedFileStreamStrategy + internal override void OnBufferAllocated(byte[] buffer) { - Debug.Assert(buffer != null); Debug.Assert(_preallocatedOverlapped == null); _preallocatedOverlapped = new PreAllocatedOverlapped(FileStreamCompletionSource.s_ioCallback, this, buffer); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs index dc04c4bb1724b..7fecd17e2ed65 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; @@ -8,17 +9,26 @@ namespace System.IO { // this type exists so we can avoid duplicating the buffering logic in every FileStreamStrategy implementation - // for simple properties that would just call the wrapped stream properties, we call strategy directly - // for everything else, we are calling BufferedStream methods that take care of all the buffering work internal sealed class BufferedFileStreamStrategy : FileStreamStrategy { + private const int MaxShadowBufferSize = 81920; // Make sure not to get to the Large Object Heap. + private readonly FileStreamStrategy _strategy; - private readonly BufferedStream _bufferedStream; + private readonly int _bufferSize; + + private byte[]? _buffer; + private int _writePos; + private int _readPos; + private int _readLen; + // The last successful Task returned from ReadAsync (perf optimization for successive reads of the same size) + private Task? _lastSyncCompletedReadTask; internal BufferedFileStreamStrategy(FileStreamStrategy strategy, int bufferSize) { + Debug.Assert(bufferSize > 1); + _strategy = strategy; - _bufferedStream = new BufferedStream(strategy, bufferSize, actLikeFileStream: true); + _bufferSize = bufferSize; } ~BufferedFileStreamStrategy() => DisposeInternal(false); @@ -29,107 +39,1234 @@ internal BufferedFileStreamStrategy(FileStreamStrategy strategy, int bufferSize) public override bool CanSeek => _strategy.CanSeek; - public override long Length => _bufferedStream.GetLengthWithoutFlushing(); + public override long Length + { + get + { + long len = _strategy.Length; + + // If we're writing near the end of the file, we must include our + // internal buffer in our Length calculation. Don't flush because + // we use the length of the file in AsyncWindowsFileStreamStrategy.WriteAsync + if (_writePos > 0 && _strategy.Position + _writePos > len) + { + len = _writePos + _strategy.Position; + } + + return len; + } + } public override long Position { - get => _bufferedStream.Position; - set => _bufferedStream.Position = value; + get + { + Debug.Assert(!(_writePos > 0 && _readPos != _readLen), "Read and Write buffers cannot both have data in them at the same time."); + + return _strategy.Position + (_readPos - _readLen + _writePos); + } + set + { + if (_writePos > 0) + { + FlushWrite(); + } + + _readPos = 0; + _readLen = 0; + + _strategy.Position = value; + } } internal override bool IsAsync => _strategy.IsAsync; + internal override bool IsClosed => _strategy.IsClosed; + internal override string Name => _strategy.Name; internal override SafeFileHandle SafeFileHandle { get { - _bufferedStream.Flush(); + // BufferedFileStreamStrategy must flush before the handle is exposed + // so whoever uses SafeFileHandle to access disk data can see + // the changes that were buffered in memory so far + Flush(); + return _strategy.SafeFileHandle; } } - internal override bool IsClosed => _strategy.IsClosed; + public override async ValueTask DisposeAsync() + { + try + { + if (!_strategy.IsClosed) + { + try + { + await FlushAsync().ConfigureAwait(false); + } + finally + { + await _strategy.DisposeAsync().ConfigureAwait(false); + } + } + } + finally + { + // Don't set the buffer to null, to avoid a NullReferenceException + // when users have a race condition in their code (i.e. they call + // FileStream.Close when calling another method on FileStream like Read). - internal override void Lock(long position, long length) => _strategy.Lock(position, length); + Debug.Assert(_writePos == 0, "Everything must have been already flushed"); + } + } - internal override void Unlock(long position, long length) => _strategy.Unlock(position, length); + internal override void DisposeInternal(bool disposing) + { + try + { + // the finalizer must at least try to flush the write buffer + // so we enforce it by passing always true + Dispose(true); + } + catch (Exception e) when (!disposing && FileStream.IsIoRelatedException(e)) + { + // On finalization, ignore failures from trying to flush the write buffer, + // e.g. if this stream is wrapping a pipe and the pipe is now broken. + } + + if (disposing) + { + GC.SuppressFinalize(this); + } + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && !_strategy.IsClosed) + { + try + { + Flush(); + } + finally + { + _strategy.Dispose(); + } + } + } + finally + { + // Don't set the buffer to null, to avoid a NullReferenceException + // when users have a race condition in their code (i.e. they call + // FileStream.Close when calling another method on FileStream like Read). - public override long Seek(long offset, SeekOrigin origin) => _bufferedStream.Seek(offset, origin); + // Call base.Dispose(bool) to cleanup async IO resources + base.Dispose(disposing); - public override void SetLength(long value) => _bufferedStream.SetLength(value); + Debug.Assert(_writePos == 0, "Everything must have been already flushed"); + } + } - public override int ReadByte() => _bufferedStream.ReadByte(); + public override void SetLength(long value) + { + Flush(); - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - => _bufferedStream.BeginRead(buffer, offset, count, callback, state); + _strategy.SetLength(value); + } - public override int EndRead(IAsyncResult asyncResult) - => _bufferedStream.EndRead(asyncResult); + // the Read(Array) overload does not just create a Span and call Read(Span) + // because for async file stream strategies the call to Read(Span) + // is translated to Stream.Read(Span), which rents an array from the pool + // copies the data, and then calls Read(Array) + public override int Read(byte[] buffer, int offset, int count) + { + AssertBufferArguments(buffer, offset, count); + EnsureCanRead(); + + int bytesFromBuffer = ReadFromBuffer(buffer, offset, count); + + // We may have read less than the number of bytes the user asked for, but that is part of the Stream Debug. + // Reading again for more data may cause us to block if we're using a device with no clear end of file, + // such as a serial port or pipe. If we blocked here and this code was used with redirected pipes for a + // process's standard output, this can lead to deadlocks involving two processes. + // BUT - this is a breaking change. + // So: If we could not read all bytes the user asked for from the buffer, we will try once from the underlying + // stream thus ensuring the same blocking behaviour as if the underlying stream was not wrapped in this BufferedStream. + if (bytesFromBuffer == count + && !(count == 0 && _readLen == _readPos)) // blocking 0 bytes reads are OK only when the read buffer is empty + { + return bytesFromBuffer; + } + + int alreadySatisfied = bytesFromBuffer; + if (bytesFromBuffer > 0) + { + count -= bytesFromBuffer; + offset += bytesFromBuffer; + } + + Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); + _readPos = _readLen = 0; + + // If there was anything in the write buffer, clear it. + if (_writePos > 0) + { + FlushWrite(); + } + + // If the requested read is larger than buffer size, avoid the buffer and still use a single read: + if (count >= _bufferSize) + { + return _strategy.Read(buffer, offset, count) + alreadySatisfied; + } + + // Ok. We can fill the buffer: + EnsureBufferAllocated(); + _readLen = _strategy.Read(_buffer!, 0, _bufferSize); + + bytesFromBuffer = ReadFromBuffer(buffer, offset, count); + + // We may have read less than the number of bytes the user asked for, but that is part of the Stream Debug. + // Reading again for more data may cause us to block if we're using a device with no clear end of stream, + // such as a serial port or pipe. If we blocked here & this code was used with redirected pipes for a process's + // standard output, this can lead to deadlocks involving two processes. Additionally, translating one read on the + // BufferedStream to more than one read on the underlying Stream may defeat the whole purpose of buffering of the + // underlying reads are significantly more expensive. + + return bytesFromBuffer + alreadySatisfied; + } + + public override int Read(Span destination) + { + EnsureNotClosed(); + EnsureCanRead(); + + // Try to read from the buffer. + int bytesFromBuffer = ReadFromBuffer(destination); + if (bytesFromBuffer == destination.Length + && !(destination.Length == 0 && _readLen == _readPos)) // 0 bytes reads are OK only for FileStream when the read buffer is empty + { + // We got as many bytes as were asked for; we're done. + return bytesFromBuffer; + } + + // We didn't get as many bytes as were asked for from the buffer, so try filling the buffer once. + + if (bytesFromBuffer > 0) + { + destination = destination.Slice(bytesFromBuffer); + } + + Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); + _readPos = _readLen = 0; + + // If there was anything in the write buffer, clear it. + if (_writePos > 0) + { + FlushWrite(); + } + + if (destination.Length >= _bufferSize) + { + // If the requested read is larger than buffer size, avoid the buffer and just read + // directly into the destination. + return _strategy.Read(destination) + bytesFromBuffer; + } + else + { + // Otherwise, fill the buffer, then read from that. + EnsureBufferAllocated(); + _readLen = _strategy.Read(_buffer!, 0, _bufferSize); + return ReadFromBuffer(destination) + bytesFromBuffer; + } + } + + public override int ReadByte() => _readPos != _readLen ? _buffer![_readPos++] : ReadByteSlow(); + + private int ReadByteSlow() + { + Debug.Assert(_readPos == _readLen); + + // We want to check for whether the underlying stream has been closed and whether + // it's readable, but we only need to do so if we don't have data in our buffer, + // as any data we have came from reading it from an open stream, and we don't + // care if the stream has been closed or become unreadable since. Further, if + // the stream is closed, its read buffer is flushed, so we'll take this slow path. + EnsureNotClosed(); + EnsureCanRead(); - public override int Read(byte[] buffer, int offset, int count) => _bufferedStream.Read(buffer, offset, count); + if (_writePos > 0) + { + FlushWrite(); + } - public override int Read(Span buffer) => _bufferedStream.Read(buffer); + EnsureBufferAllocated(); + _readLen = _strategy.Read(_buffer!, 0, _bufferSize); + _readPos = 0; + + if (_readLen == 0) + { + return -1; + } + + return _buffer![_readPos++]; + } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - => _bufferedStream.ReadAsync(buffer, offset, count, cancellationToken); + { + AssertBufferArguments(buffer, offset, count); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + ValueTask readResult = ReadAsync(new Memory(buffer, offset, count), cancellationToken); + + return readResult.IsCompletedSuccessfully + ? LastSyncCompletedReadTask(readResult.Result) + : readResult.AsTask(); + } + + private Task LastSyncCompletedReadTask(int val) + { + Task? t = _lastSyncCompletedReadTask; + Debug.Assert(t == null || t.IsCompletedSuccessfully); + + if (t != null && t.Result == val) + return t; + + t = Task.FromResult(val); + _lastSyncCompletedReadTask = t; + return t; + } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - => _bufferedStream.ReadAsync(buffer, cancellationToken); + { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - => _bufferedStream.BeginWrite(buffer, offset, count, callback, state); + Debug.Assert(!_strategy.IsClosed, "Strategy.IsClosed was supposed to be validated by FileStream itself"); + EnsureCanRead(); - public override void EndWrite(IAsyncResult asyncResult) - => _bufferedStream.EndWrite(asyncResult); + int bytesFromBuffer = 0; + SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); + Task semaphoreLockTask = sem.WaitAsync(cancellationToken); + if (semaphoreLockTask.IsCompletedSuccessfully) + { + // hot path #1: there is data in the buffer + if (_readLen - _readPos > 0) + { + bytesFromBuffer = ReadFromBuffer(buffer.Span); + + if (bytesFromBuffer == buffer.Length) + { + // if above is FALSE, we will be entering ReadFromUnderlyingStreamAsync and releasing there. + sem.Release(); + + // If we satisfied enough data from the buffer, we can complete synchronously. + return new ValueTask(bytesFromBuffer); + } + + buffer = buffer.Slice(bytesFromBuffer); + } + // hot path #2: there is nothing to Flush and buffering would not be beneficial + else if (_writePos == 0 && buffer.Length >= _bufferSize) + { + Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); + _readPos = _readLen = 0; + + try + { + return _strategy.ReadAsync(buffer, cancellationToken); + } + finally + { + sem.Release(); + } + } + } + + // Delegate to the async implementation. + return ReadFromUnderlyingStreamAsync(buffer, cancellationToken, bytesFromBuffer, semaphoreLockTask); + } + + /// BufferedStream should be as thin a wrapper as possible. We want ReadAsync to delegate to + /// ReadAsync of the underlying _stream rather than calling the base Stream which implements the one in terms of the other. + /// This allows BufferedStream to affect the semantics of the stream it wraps as little as possible. + /// -2 if _bufferSize was set to 0 while waiting on the semaphore; otherwise num of bytes read. + private async ValueTask ReadFromUnderlyingStreamAsync( + Memory buffer, CancellationToken cancellationToken, int bytesAlreadySatisfied, Task semaphoreLockTask) + { + // Same conditions validated with exceptions in ReadAsync: + Debug.Assert(_strategy.CanRead); + Debug.Assert(_bufferSize > 0); + Debug.Assert(_asyncActiveSemaphore != null); + Debug.Assert(semaphoreLockTask != null); + + // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. + await semaphoreLockTask.ConfigureAwait(false); + + try + { + int bytesFromBuffer = 0; + + if (_readLen - _readPos > 0) + { + // The buffer might have been changed by another async task while we were waiting on the semaphore. + // Check it now again. + bytesFromBuffer = ReadFromBuffer(buffer.Span); + if (bytesFromBuffer == buffer.Length) + { + return bytesAlreadySatisfied + bytesFromBuffer; + } + + if (bytesFromBuffer > 0) + { + buffer = buffer.Slice(bytesFromBuffer); + bytesAlreadySatisfied += bytesFromBuffer; + } + } + + Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); + _readPos = _readLen = 0; + + // If there was anything in the write buffer, clear it. + if (_writePos > 0) + { + await FlushWriteAsync(cancellationToken).ConfigureAwait(false); // no Begin-End read version for Flush. Use Async. + } + + // If the requested read is larger than buffer size, avoid the buffer and still use a single read: + if (buffer.Length >= _bufferSize) + { + return bytesAlreadySatisfied + await _strategy.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } + + // Ok. We can fill the buffer: + EnsureBufferAllocated(); + _readLen = await _strategy.ReadAsync(new Memory(_buffer, 0, _bufferSize), cancellationToken).ConfigureAwait(false); + + bytesFromBuffer = ReadFromBuffer(buffer.Span); + return bytesAlreadySatisfied + bytesFromBuffer; + } + finally + { + _asyncActiveSemaphore.Release(); + } + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => TaskToApm.Begin(ReadAsync(buffer, offset, count, CancellationToken.None), callback, state); + + public override int EndRead(IAsyncResult asyncResult) + => TaskToApm.End(asyncResult); + + public override void Write(byte[] buffer, int offset, int count) + { + AssertBufferArguments(buffer, offset, count); + EnsureCanWrite(); + + if (_writePos == 0) + ClearReadBufferBeforeWrite(); + + #region Write algorithm comment + // We need to use the buffer, while avoiding unnecessary buffer usage / memory copies. + // We ASSUME that memory copies are much cheaper than writes to the underlying stream, so if an extra copy is + // guaranteed to reduce the number of writes, we prefer it. + // We pick a simple strategy that makes degenerate cases rare if our assumptions are right. + // + // For ever write, we use a simple heuristic (below) to decide whether to use the buffer. + // The heuristic has the desirable property (*) that if the specified user data can fit into the currently available + // buffer space without filling it up completely, the heuristic will always tell us to use the buffer. It will also + // tell us to use the buffer in cases where the current write would fill the buffer, but the remaining data is small + // enough such that subsequent operations can use the buffer again. + // + // Algorithm: + // Determine whether or not to buffer according to the heuristic (below). + // If we decided to use the buffer: + // Copy as much user data as we can into the buffer. + // If we consumed all data: We are finished. + // Otherwise, write the buffer out. + // Copy the rest of user data into the now cleared buffer (no need to write out the buffer again as the heuristic + // will prevent it from being filled twice). + // If we decided not to use the buffer: + // Can the data already in the buffer and current user data be combines to a single write + // by allocating a "shadow" buffer of up to twice the size of _bufferSize (up to a limit to avoid LOH)? + // Yes, it can: + // Allocate a larger "shadow" buffer and ensure the buffered data is moved there. + // Copy user data to the shadow buffer. + // Write shadow buffer to the underlying stream in a single operation. + // No, it cannot (amount of data is still too large): + // Write out any data possibly in the buffer. + // Write out user data directly. + // + // Heuristic: + // If the subsequent write operation that follows the current write operation will result in a write to the + // underlying stream in case that we use the buffer in the current write, while it would not have if we avoided + // using the buffer in the current write (by writing current user data to the underlying stream directly), then we + // prefer to avoid using the buffer since the corresponding memory copy is wasted (it will not reduce the number + // of writes to the underlying stream, which is what we are optimising for). + // ASSUME that the next write will be for the same amount of bytes as the current write (most common case) and + // determine if it will cause a write to the underlying stream. If the next write is actually larger, our heuristic + // still yields the right behaviour, if the next write is actually smaller, we may making an unnecessary write to + // the underlying stream. However, this can only occur if the current write is larger than half the buffer size and + // we will recover after one iteration. + // We have: + // useBuffer = (_writePos + count + count < _bufferSize + _bufferSize) + // + // Example with _bufferSize = 20, _writePos = 6, count = 10: + // + // +---------------------------------------+---------------------------------------+ + // | current buffer | next iteration's "future" buffer | + // +---------------------------------------+---------------------------------------+ + // |0| | | | | | | | | |1| | | | | | | | | |2| | | | | | | | | |3| | | | | | | | | | + // |0|1|2|3|4|5|6|7|8|9|0|1|2|3|4|5|6|7|8|9|0|1|2|3|4|5|6|7|8|9|0|1|2|3|4|5|6|7|8|9| + // +-----------+-------------------+-------------------+---------------------------+ + // | _writePos | current count | assumed next count|avail buff after next write| + // +-----------+-------------------+-------------------+---------------------------+ + // + // A nice property (*) of this heuristic is that it will always succeed if the user data completely fits into the + // available buffer, i.e. if count < (_bufferSize - _writePos). + #endregion Write algorithm comment + + Debug.Assert(_writePos < _bufferSize); + + int totalUserbytes; + bool useBuffer; + checked + { // We do not expect buffer sizes big enough for an overflow, but if it happens, lets fail early: + totalUserbytes = _writePos + count; + useBuffer = (totalUserbytes + count < (_bufferSize + _bufferSize)); + } + + if (useBuffer) + { + WriteToBuffer(buffer, ref offset, ref count); + + if (_writePos < _bufferSize) + { + Debug.Assert(count == 0); + return; + } + + Debug.Assert(count >= 0); + Debug.Assert(_writePos == _bufferSize); + Debug.Assert(_buffer != null); + + _strategy.Write(_buffer, 0, _writePos); + _writePos = 0; + + WriteToBuffer(buffer, ref offset, ref count); + + Debug.Assert(count == 0); + Debug.Assert(_writePos < _bufferSize); + } + else + { // if (!useBuffer) + // Write out the buffer if necessary. + if (_writePos > 0) + { + Debug.Assert(_buffer != null); + Debug.Assert(totalUserbytes >= _bufferSize); + + // Try avoiding extra write to underlying stream by combining previously buffered data with current user data: + if (totalUserbytes <= (_bufferSize + _bufferSize) && totalUserbytes <= MaxShadowBufferSize) + { + EnsureShadowBufferAllocated(); + Buffer.BlockCopy(buffer, offset, _buffer, _writePos, count); + _strategy.Write(_buffer, 0, totalUserbytes); + _writePos = 0; + return; + } + + _strategy.Write(_buffer, 0, _writePos); + _writePos = 0; + } + + // Write out user data. + _strategy.Write(buffer, offset, count); + } + } + + public override void Write(ReadOnlySpan buffer) + { + EnsureNotClosed(); + EnsureCanWrite(); + + if (_writePos == 0) + { + ClearReadBufferBeforeWrite(); + } + Debug.Assert(_writePos < _bufferSize, $"Expected {_writePos} < {_bufferSize}"); - public override void WriteByte(byte value) => _bufferedStream.WriteByte(value); + int totalUserbytes; + bool useBuffer; + checked + { + // We do not expect buffer sizes big enough for an overflow, but if it happens, lets fail early: + totalUserbytes = _writePos + buffer.Length; + useBuffer = (totalUserbytes + buffer.Length < (_bufferSize + _bufferSize)); + } + + if (useBuffer) + { + // Copy as much data to the buffer as will fit. If there's still room in the buffer, + // everything must have fit. + int bytesWritten = WriteToBuffer(buffer); + if (_writePos < _bufferSize) + { + Debug.Assert(bytesWritten == buffer.Length); + return; + } + buffer = buffer.Slice(bytesWritten); + + Debug.Assert(_writePos == _bufferSize); + Debug.Assert(_buffer != null); + + // Output the buffer to the underlying strategy. + _strategy.Write(_buffer, 0, _writePos); + _writePos = 0; + + // Now write the remainder. It must fit, as we're only on this path if that's true. + bytesWritten = WriteToBuffer(buffer); + Debug.Assert(bytesWritten == buffer.Length); + + Debug.Assert(_writePos < _bufferSize); + } + else // skip the buffer + { + // Flush anything existing in the buffer. + if (_writePos > 0) + { + Debug.Assert(_buffer != null); + Debug.Assert(totalUserbytes >= _bufferSize); + + // Try avoiding extra write to underlying stream by combining previously buffered data with current user data: + if (totalUserbytes <= (_bufferSize + _bufferSize) && totalUserbytes <= MaxShadowBufferSize) + { + EnsureShadowBufferAllocated(); + buffer.CopyTo(new Span(_buffer, _writePos, buffer.Length)); + _strategy.Write(_buffer, 0, totalUserbytes); + _writePos = 0; + return; + } + + _strategy.Write(_buffer, 0, _writePos); + _writePos = 0; + } + + // Write out user data. + _strategy.Write(buffer); + } + } - public override void Write(byte[] buffer, int offset, int count) => _bufferedStream.Write(buffer, offset, count); + public override void WriteByte(byte value) + { + if (_writePos > 0 && _writePos < _bufferSize - 1) + { + _buffer![_writePos++] = value; + } + else + { + WriteByteSlow(value); + } + } + + private void WriteByteSlow(byte value) + { + EnsureNotClosed(); + + if (_writePos == 0) + { + EnsureCanWrite(); + ClearReadBufferBeforeWrite(); + EnsureBufferAllocated(); + } - public override void Write(ReadOnlySpan buffer) => _bufferedStream.Write(buffer); + if (_writePos >= _bufferSize - 1) + FlushWrite(); + + _buffer![_writePos++] = value; + + Debug.Assert(_writePos < _bufferSize); + } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - => _bufferedStream.WriteAsync(buffer, offset, count, cancellationToken); + { + AssertBufferArguments(buffer, offset, count); + + return WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - => _bufferedStream.WriteAsync(buffer, cancellationToken); + { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + EnsureNotClosed(); + EnsureCanWrite(); + + // Try to satisfy the request from the buffer synchronously. + SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); + Task semaphoreLockTask = sem.WaitAsync(cancellationToken); + if (semaphoreLockTask.IsCompletedSuccessfully) + { + bool completeSynchronously = true; + try + { + if (_writePos == 0) + { + ClearReadBufferBeforeWrite(); + } + + Debug.Assert(_writePos < _bufferSize); + + // hot path #1 If the write completely fits into the buffer, we can complete synchronously: + completeSynchronously = buffer.Length < _bufferSize - _writePos; + if (completeSynchronously) + { + int bytesWritten = WriteToBuffer(buffer.Span); + Debug.Assert(bytesWritten == buffer.Length); + return default; + } + } + finally + { + if (completeSynchronously) // if this is FALSE, we will be entering WriteToUnderlyingStreamAsync and releasing there. + { + sem.Release(); + } + } + + // hot path #2: there is nothing to Flush and buffering would not be beneficial + if (_writePos == 0 && buffer.Length >= _bufferSize) + { + try + { + return _strategy.WriteAsync(buffer, cancellationToken); + } + finally + { + sem.Release(); + } + } + } + + // Delegate to the async implementation. + return WriteToUnderlyingStreamAsync(buffer, cancellationToken, semaphoreLockTask); + } + + /// BufferedStream should be as thin a wrapper as possible. We want WriteAsync to delegate to + /// WriteAsync of the underlying _stream rather than calling the base Stream which implements the one + /// in terms of the other. This allows BufferedStream to affect the semantics of the stream it wraps as + /// little as possible. + /// + private async ValueTask WriteToUnderlyingStreamAsync( + ReadOnlyMemory buffer, CancellationToken cancellationToken, Task semaphoreLockTask) + { + Debug.Assert(_strategy.CanWrite); + Debug.Assert(_bufferSize > 0); + Debug.Assert(_asyncActiveSemaphore != null); + Debug.Assert(semaphoreLockTask != null); + + // See the LARGE COMMENT in Write(..) for the explanation of the write buffer algorithm. + + await semaphoreLockTask.ConfigureAwait(false); + try + { + // The buffer might have been changed by another async task while we were waiting on the semaphore. + // However, note that if we recalculate the sync completion condition to TRUE, then useBuffer will also be TRUE. + + if (_writePos == 0) + { + ClearReadBufferBeforeWrite(); + } + + int totalUserBytes; + bool useBuffer; + checked + { + // We do not expect buffer sizes big enough for an overflow, but if it happens, lets fail early: + totalUserBytes = _writePos + buffer.Length; + useBuffer = (totalUserBytes + buffer.Length < (_bufferSize + _bufferSize)); + } + + if (useBuffer) + { + buffer = buffer.Slice(WriteToBuffer(buffer.Span)); + + if (_writePos < _bufferSize) + { + Debug.Assert(buffer.Length == 0); + return; + } + + Debug.Assert(buffer.Length >= 0); + Debug.Assert(_writePos == _bufferSize); + Debug.Assert(_buffer != null); + + await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); + _writePos = 0; + + int bytesWritten = WriteToBuffer(buffer.Span); + Debug.Assert(bytesWritten == buffer.Length); + + Debug.Assert(_writePos < _bufferSize); + + } + else // !useBuffer + { + // Write out the buffer if necessary. + if (_writePos > 0) + { + Debug.Assert(_buffer != null); + Debug.Assert(totalUserBytes >= _bufferSize); + + // Try avoiding extra write to underlying stream by combining previously buffered data with current user data: + if (totalUserBytes <= (_bufferSize + _bufferSize) && totalUserBytes <= MaxShadowBufferSize) + { + EnsureShadowBufferAllocated(); + buffer.Span.CopyTo(new Span(_buffer, _writePos, buffer.Length)); + + await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, totalUserBytes), cancellationToken).ConfigureAwait(false); + _writePos = 0; + return; + } + + await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); + _writePos = 0; + } + + // Write out user data. + await _strategy.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + } + } + finally + { + _asyncActiveSemaphore.Release(); + } + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => TaskToApm.Begin(WriteAsync(buffer, offset, count, CancellationToken.None), callback, state); + + public override void EndWrite(IAsyncResult asyncResult) + => TaskToApm.End(asyncResult); + + public override void Flush() => Flush(flushToDisk: false); + + internal override void Flush(bool flushToDisk) + { + EnsureNotClosed(); + + // Has write data in the buffer: + if (_writePos > 0) + { + // EnsureNotClosed does not guarantee that the Stream has not been closed + // an example could be a call to fileStream.SafeFileHandle.Dispose() + // so to avoid getting exception here, we just ensure that we can Write before doing it + if (_strategy.CanWrite) + { + FlushWrite(); + Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); + return; + } + } + + // Has read data in the buffer: + if (_readPos < _readLen) + { + // If the underlying strategy is not seekable AND we have something in the read buffer, then FlushRead would throw. + // We can either throw away the buffer resulting in data loss (!) or ignore the Flush. + // (We cannot throw because it would be a breaking change.) We opt into ignoring the Flush in that situation. + if (_strategy.CanSeek) + { + FlushRead(); + } + + // If the Stream was seekable, then we should have called FlushRead which resets _readPos & _readLen. + Debug.Assert(_writePos == 0 && (!_strategy.CanSeek || (_readPos == 0 && _readLen == 0))); + return; + } - public override void Flush() => _bufferedStream.Flush(); + // We had no data in the buffer, but we still need to tell the underlying strategy to flush. + _strategy.Flush(flushToDisk); + + _writePos = _readPos = _readLen = 0; + } - internal override void Flush(bool flushToDisk) => _bufferedStream.Flush(flushToDisk); public override Task FlushAsync(CancellationToken cancellationToken) - => _bufferedStream.FlushAsync(cancellationToken); + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + EnsureNotClosed(); + + return FlushAsyncInternal(cancellationToken); + } + + private async Task FlushAsyncInternal(CancellationToken cancellationToken) + { + await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (_writePos > 0) + { + await FlushWriteAsync(cancellationToken).ConfigureAwait(false); + Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); + return; + } + + if (_readPos < _readLen) + { + // If the underlying strategy is not seekable AND we have something in the read buffer, then FlushRead would throw. + // We can either throw away the buffer resulting in date loss (!) or ignore the Flush. (We cannot throw because it + // would be a breaking change.) We opt into ignoring the Flush in that situation. + if (_strategy.CanSeek) + { + FlushRead(); // not async; it uses Seek, but there's no SeekAsync + } + + // If the Strategy was seekable, then we should have called FlushRead which resets _readPos & _readLen. + Debug.Assert(_writePos == 0 && (!_strategy.CanSeek || (_readPos == 0 && _readLen == 0))); + return; + } + + // There was nothing in the buffer: + Debug.Assert(_writePos == 0 && _readPos == _readLen); + } + finally + { + _asyncActiveSemaphore.Release(); + } + } public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - => _bufferedStream.CopyToAsync(destination, bufferSize, cancellationToken); + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + EnsureCanRead(); + + return cancellationToken.IsCancellationRequested ? + Task.FromCanceled(cancellationToken) : + CopyToAsyncCore(destination, bufferSize, cancellationToken); + } + + private async Task CopyToAsyncCore(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + // Synchronize async operations as does Read/WriteAsync. + await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + int readBytes = _readLen - _readPos; + Debug.Assert(readBytes >= 0, $"Expected a non-negative number of bytes in buffer, got {readBytes}"); + + if (readBytes > 0) + { + // If there's any read data in the buffer, write it all to the destination stream. + Debug.Assert(_writePos == 0, "Write buffer must be empty if there's data in the read buffer"); + await destination.WriteAsync(new ReadOnlyMemory(_buffer, _readPos, readBytes), cancellationToken).ConfigureAwait(false); + _readPos = _readLen = 0; + } + else if (_writePos > 0) + { + // If there's write data in the buffer, flush it back to the underlying stream, as does ReadAsync. + await FlushWriteAsync(cancellationToken).ConfigureAwait(false); + } + + // Our buffer is now clear. Copy data directly from the source stream to the destination stream. + await _strategy.CopyToAsync(destination, bufferSize, cancellationToken).ConfigureAwait(false); + } + finally + { + _asyncActiveSemaphore.Release(); + } + } public override void CopyTo(Stream destination, int bufferSize) - => _bufferedStream.CopyTo(destination, bufferSize); + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + EnsureCanRead(); - public override ValueTask DisposeAsync() - => _bufferedStream.DisposeAsync(); + int readBytes = _readLen - _readPos; + Debug.Assert(readBytes >= 0, $"Expected a non-negative number of bytes in buffer, got {readBytes}"); - internal override void DisposeInternal(bool disposing) + if (readBytes > 0) + { + // If there's any read data in the buffer, write it all to the destination stream. + Debug.Assert(_writePos == 0, "Write buffer must be empty if there's data in the read buffer"); + destination.Write(_buffer!, _readPos, readBytes); + _readPos = _readLen = 0; + } + else if (_writePos > 0) + { + // If there's write data in the buffer, flush it back to the underlying stream, as does ReadAsync. + FlushWrite(); + } + + // Our buffer is now clear. Copy data directly from the source stream to the destination stream. + _strategy.CopyTo(destination, bufferSize); + } + + public override long Seek(long offset, SeekOrigin origin) { - try + EnsureNotClosed(); + EnsureCanSeek(); + + // If we have bytes in the write buffer, flush them out, seek and be done. + if (_writePos > 0) { - // the finalizer must at least try to flush the write buffer - // so we enforce it by passing always true - _bufferedStream.DisposeInternal(true); + FlushWrite(); + return _strategy.Seek(offset, origin); } - catch (Exception e) when (!disposing && FileStream.IsIoRelatedException(e)) + + // The buffer is either empty or we have a buffered read. + if (_readLen - _readPos > 0 && origin == SeekOrigin.Current) { - // On finalization, ignore failures from trying to flush the write buffer, - // e.g. if this stream is wrapping a pipe and the pipe is now broken. + // If we have bytes in the read buffer, adjust the seek offset to account for the resulting difference + // between this stream's position and the underlying stream's position. + offset -= (_readLen - _readPos); } - if (disposing) + long oldPos = Position; + Debug.Assert(oldPos == _strategy.Position + (_readPos - _readLen)); + + long newPos = _strategy.Seek(offset, origin); + + // If the seek destination is still within the data currently in the buffer, we want to keep the buffer data and continue using it. + // Otherwise we will throw away the buffer. This can only happen on read, as we flushed write data above. + + // The offset of the new/updated seek pointer within _buffer: + _readPos = (int)(newPos - (oldPos - _readPos)); + + // If the offset of the updated seek pointer in the buffer is still legal, then we can keep using the buffer: + if (0 <= _readPos && _readPos < _readLen) { - GC.SuppressFinalize(this); + // Adjust the seek pointer of the underlying stream to reflect the amount of useful bytes in the read buffer: + _strategy.Seek(_readLen - _readPos, SeekOrigin.Current); + } + else + { // The offset of the updated seek pointer is not a legal offset. Loose the buffer. + _readPos = _readLen = 0; + } + + Debug.Assert(newPos == Position, "newPos (=" + newPos + ") == Position (=" + Position + ")"); + return newPos; + } + + internal override void Lock(long position, long length) => _strategy.Lock(position, length); + + internal override void Unlock(long position, long length) => _strategy.Unlock(position, length); + + // Reading is done in blocks, but someone could read 1 byte from the buffer then write. + // At that point, the underlying stream's pointer is out of sync with this stream's position. + // All write functions should call this function to ensure that the buffered data is not lost. + private void FlushRead() + { + Debug.Assert(_writePos == 0, "Write buffer must be empty in FlushRead!"); + + if (_readPos - _readLen != 0) + { + _strategy.Seek(_readPos - _readLen, SeekOrigin.Current); + } + + _readPos = 0; + _readLen = 0; + } + + private void FlushWrite() + { + Debug.Assert(_readPos == 0 && _readLen == 0, "Read buffer must be empty in FlushWrite!"); + Debug.Assert(_buffer != null && _bufferSize >= _writePos, "Write buffer must be allocated and write position must be in the bounds of the buffer in FlushWrite!"); + + _strategy.Write(_buffer, 0, _writePos); + _writePos = 0; + } + + private async ValueTask FlushWriteAsync(CancellationToken cancellationToken) + { + Debug.Assert(_readPos == 0 && _readLen == 0, "Read buffer must be empty in FlushWriteAsync!"); + Debug.Assert(_buffer != null && _bufferSize >= _writePos, "Write buffer must be allocated and write position must be in the bounds of the buffer in FlushWriteAsync!"); + + // TODO: we might get rid of the await + await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); + _writePos = 0; + } + + private int ReadFromBuffer(byte[] buffer, int offset, int count) + { + int readbytes = _readLen - _readPos; + Debug.Assert(readbytes >= 0); + + if (readbytes == 0) + { + return 0; + } + + if (readbytes > count) + { + readbytes = count; + } + + Buffer.BlockCopy(_buffer!, _readPos, buffer, offset, readbytes); + _readPos += readbytes; + + return readbytes; + } + + private int ReadFromBuffer(Span destination) + { + int readbytes = Math.Min(_readLen - _readPos, destination.Length); + Debug.Assert(readbytes >= 0); + if (readbytes > 0) + { + new ReadOnlySpan(_buffer, _readPos, readbytes).CopyTo(destination); + _readPos += readbytes; } + return readbytes; + } + + private void WriteToBuffer(byte[] buffer, ref int offset, ref int count) + { + int bytesToWrite = Math.Min(_bufferSize - _writePos, count); + + if (bytesToWrite <= 0) + { + return; + } + + EnsureBufferAllocated(); + Buffer.BlockCopy(buffer, offset, _buffer!, _writePos, bytesToWrite); + + _writePos += bytesToWrite; + count -= bytesToWrite; + offset += bytesToWrite; + } + + private int WriteToBuffer(ReadOnlySpan buffer) + { + int bytesToWrite = Math.Min(_bufferSize - _writePos, buffer.Length); + if (bytesToWrite > 0) + { + EnsureBufferAllocated(); + buffer.Slice(0, bytesToWrite).CopyTo(new Span(_buffer, _writePos, bytesToWrite)); + _writePos += bytesToWrite; + } + return bytesToWrite; + } + + /// + /// Called by Write methods to clear the Read Buffer + /// + private void ClearReadBufferBeforeWrite() + { + Debug.Assert(_readPos <= _readLen, "_readPos <= _readLen [" + _readPos + " <= " + _readLen + "]"); + + // No read data in the buffer: + if (_readPos == _readLen) + { + _readPos = _readLen = 0; + return; + } + + // Must have read data. + Debug.Assert(_readPos < _readLen); + + // If the underlying stream cannot seek, FlushRead would end up throwing NotSupported. + // However, since the user did not call a method that is intuitively expected to seek, a better message is in order. + // Ideally, we would throw an InvalidOperation here, but for backward compat we have to stick with NotSupported. + if (!_strategy.CanSeek) + ThrowNotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed(); + + FlushRead(); + + static void ThrowNotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed() + => throw new NotSupportedException(SR.NotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed); + } + + private void EnsureNotClosed() + { + if (_strategy.IsClosed) + { + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); + } + } + + private void EnsureCanSeek() + { + if (!_strategy.CanSeek) + { + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); + } + } + + private void EnsureCanRead() + { + if (!_strategy.CanRead) + { + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); + } + } + + private void EnsureCanWrite() + { + if (!_strategy.CanWrite) + { + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); + } + } + + private void EnsureBufferAllocated() + { + Debug.Assert(_bufferSize > 0); + + // BufferedFileStreamStrategy is not intended for multi-threaded use, so no worries about the get/set race on _buffer. + if (_buffer == null) + { + AllocateBuffer(); + } + + void AllocateBuffer() // logic kept in a separate method to get EnsureBufferAllocated() inlined + { + _strategy.OnBufferAllocated(_buffer = new byte[_bufferSize]); + } + } + + private void EnsureShadowBufferAllocated() + { + Debug.Assert(_buffer != null); + Debug.Assert(_bufferSize > 0); + + // Already have a shadow buffer? + // Or is the user-specified buffer size already so large that we don't want to create one? + if (_buffer.Length != _bufferSize || _bufferSize >= MaxShadowBufferSize) + return; + + byte[] shadowBuffer = new byte[Math.Min(_bufferSize + _bufferSize, MaxShadowBufferSize)]; + Buffer.BlockCopy(_buffer, 0, shadowBuffer, 0, _writePos); + _buffer = shadowBuffer; + } + + [Conditional("DEBUG")] + private void AssertBufferArguments(byte[] buffer, int offset, int count) + { + ValidateBufferArguments(buffer, offset, count); // FileStream is supposed to call this + Debug.Assert(!_strategy.IsClosed, "Strategy.IsClosed was supposed to be validated by FileStream itself"); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs index c2ed2286dd775..8c3b907fd0bdc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -52,12 +51,6 @@ public sealed class BufferedStream : Stream private Stream? _stream; // Underlying stream. Close sets _stream to null. private byte[]? _buffer; // Shared read/write buffer. Alloc on first use. private readonly int _bufferSize; // Length of internal buffer (not counting the shadow buffer). - /// - /// allows for: - /// 1. blocking zero byte reads - /// 2. skipping the serialization of async operations that don't use the buffer - /// - private readonly bool _actLikeFileStream; private int _readPos; // Read pointer within shared buffer. private int _readLen; // Number of bytes read in buffer from _stream. private int _writePos; // Write pointer within shared buffer. @@ -89,11 +82,6 @@ public BufferedStream(Stream stream, int bufferSize) ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); } - internal BufferedStream(Stream stream, int bufferSize, bool actLikeFileStream) : this(stream, bufferSize) - { - _actLikeFileStream = actLikeFileStream; - } - private void EnsureNotClosed() { if (_stream == null) @@ -145,21 +133,7 @@ private void EnsureBufferAllocated() // BufferedStream is not intended for multi-threaded use, so no worries about the get/set race on _buffer. if (_buffer == null) - { - AllocateBuffer(); - } - - void AllocateBuffer() // logic kept in a separate method to get EnsureBufferAllocated() inlined - { _buffer = new byte[_bufferSize]; - -#if TARGET_WINDOWS - if (_actLikeFileStream && _stream is AsyncWindowsFileStreamStrategy asyncStrategy) - { - asyncStrategy.OnBufferAllocated(_buffer); - } -#endif - } } public Stream UnderlyingStream @@ -210,29 +184,12 @@ public override long Length EnsureNotClosed(); if (_writePos > 0) - FlushWrite(true); + FlushWrite(); return _stream!.Length; } } - internal long GetLengthWithoutFlushing() - { - Debug.Assert(_actLikeFileStream); - - EnsureNotClosed(); - - long len = _stream!.Length; - - // If we're writing near the end of the file, we must include our - // internal buffer in our Length calculation. Don't flush because - // we use the length of the file in our async write method. - if (_writePos > 0 && _stream!.Position + _writePos > len) - len = _writePos + _stream!.Position; - - return len; - } - public override long Position { get @@ -252,7 +209,7 @@ public override long Position EnsureCanSeek(); if (_writePos > 0) - FlushWrite(performActualFlush: true); + FlushWrite(); _readPos = 0; _readLen = 0; @@ -260,8 +217,6 @@ public override long Position } } - internal void DisposeInternal(bool disposing) => Dispose(disposing); - protected override void Dispose(bool disposing) { try @@ -281,20 +236,12 @@ protected override void Dispose(bool disposing) finally { _stream = null; - - // Don't set the buffer to null, to avoid a NullReferenceException - // when users have a race condition in their code (i.e. they call - // Close when calling another method on Stream like Read). - if (!_actLikeFileStream) - { - _buffer = null; - } + _buffer = null; // Call base.Dispose(bool) to cleanup async IO resources base.Dispose(disposing); - // ensure that all positions are set to 0 - Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); + Debug.Assert(_writePos == 0, "Everything must have been already flushed"); } } @@ -317,39 +264,22 @@ public override async ValueTask DisposeAsync() finally { _stream = null; + _buffer = null; - // Don't set the buffer to null, to avoid a NullReferenceException - // when users have a race condition in their code (i.e. they call - // Close when calling another method on Stream like Read). - if (!_actLikeFileStream) - { - _buffer = null; - } - - // ensure that all positions are set to 0 - Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); + Debug.Assert(_writePos == 0, "Everything must have been already flushed"); } } - public override void Flush() => Flush(true); - - // flushUnderlyingStream can be set to false by BufferedFileStreamStrategy.Flush(bool flushToDisk) - internal void Flush(bool flushUnderlyingStream) + public override void Flush() { EnsureNotClosed(); // Has write data in the buffer: if (_writePos > 0) { - // EnsureNotClosed does not guarantee that the Stream has not been closed - // an example could be a call to fileStream.SafeFileHandle.Dispose() - // so to avoid getting exception here, we just ensure that we can Write before doing it - if (_stream!.CanWrite) - { - FlushWrite(flushUnderlyingStream); - Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); - return; - } + FlushWrite(); + Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); + return; } // Has read data in the buffer: @@ -366,7 +296,7 @@ internal void Flush(bool flushUnderlyingStream) // User streams may have opted to throw from Flush if CanWrite is false (although the abstract Stream does not do so). // However, if we do not forward the Flush to the underlying stream, we may have problems when chaining several streams. // Let us make a best effort attempt: - if (flushUnderlyingStream && _stream.CanWrite) + if (_stream.CanWrite) _stream.Flush(); // If the Stream was seekable, then we should have called FlushRead which resets _readPos & _readLen. @@ -375,7 +305,7 @@ internal void Flush(bool flushUnderlyingStream) } // We had no data in the buffer, but we still need to tell the underlying stream to flush. - if (flushUnderlyingStream && _stream!.CanWrite) + if (_stream!.CanWrite) _stream.Flush(); _writePos = _readPos = _readLen = 0; @@ -485,7 +415,7 @@ static void ThrowNotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlu => throw new NotSupportedException(SR.NotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed); } - private void FlushWrite(bool performActualFlush) + private void FlushWrite() { Debug.Assert(_stream != null); Debug.Assert(_readPos == 0 && _readLen == 0, @@ -495,11 +425,7 @@ private void FlushWrite(bool performActualFlush) _stream.Write(_buffer, 0, _writePos); _writePos = 0; - - if (performActualFlush) - { - _stream.Flush(); - } + _stream.Flush(); } private async ValueTask FlushWriteAsync(CancellationToken cancellationToken) @@ -574,11 +500,8 @@ public override int Read(byte[] buffer, int offset, int count) // BUT - this is a breaking change. // So: If we could not read all bytes the user asked for from the buffer, we will try once from the underlying // stream thus ensuring the same blocking behaviour as if the underlying stream was not wrapped in this BufferedStream. - if (bytesFromBuffer == count - && !(count == 0 && _actLikeFileStream && _readLen == _readPos)) // 0 bytes reads are OK only for FileStream when the read buffer is empty - { + if (bytesFromBuffer == count) return bytesFromBuffer; - } int alreadySatisfied = bytesFromBuffer; if (bytesFromBuffer > 0) @@ -592,7 +515,7 @@ public override int Read(byte[] buffer, int offset, int count) // If there was anything in the write buffer, clear it. if (_writePos > 0) - FlushWrite(true); + FlushWrite(); // If the requested read is larger than buffer size, avoid the buffer and still use a single read: if (count >= _bufferSize) @@ -624,8 +547,7 @@ public override int Read(Span destination) // Try to read from the buffer. int bytesFromBuffer = ReadFromBuffer(destination); - if (bytesFromBuffer == destination.Length - && !(destination.Length == 0 && _actLikeFileStream && _readLen == _readPos)) // 0 bytes reads are OK only for FileStream when the read buffer is empty + if (bytesFromBuffer == destination.Length) { // We got as many bytes as were asked for; we're done. return bytesFromBuffer; @@ -644,7 +566,7 @@ public override int Read(Span destination) // If there was anything in the write buffer, clear it. if (_writePos > 0) { - FlushWrite(true); + FlushWrite(); } if (destination.Length >= _bufferSize) @@ -694,10 +616,11 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel Task semaphoreLockTask = sem.WaitAsync(cancellationToken); if (semaphoreLockTask.IsCompletedSuccessfully) { - // hot path #1: there is data in the buffer - if (_readLen - _readPos > 0 || (count == 0 && !_actLikeFileStream)) + bool completeSynchronously = true; + try { - bytesFromBuffer = ReadFromBuffer(buffer, offset, count, out Exception? error); + Exception? error; + bytesFromBuffer = ReadFromBuffer(buffer, offset, count, out error); // If we satisfied enough data from the buffer, we can complete synchronously. // Reading again for more data may cause us to block if we're using a device with no clear end of file, @@ -706,32 +629,20 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel // BUT - this is a breaking change. // So: If we could not read all bytes the user asked for from the buffer, we will try once from the underlying // stream thus ensuring the same blocking behaviour as if the underlying stream was not wrapped in this BufferedStream. - if (bytesFromBuffer == count || error != null) + completeSynchronously = (bytesFromBuffer == count || error != null); + + if (completeSynchronously) { - // if the above is false, we will be entering ReadFromUnderlyingStreamAsync and releasing there. - sem.Release(); return (error == null) - ? LastSyncCompletedReadTask(bytesFromBuffer) - : Task.FromException(error); + ? LastSyncCompletedReadTask(bytesFromBuffer) + : Task.FromException(error); } } - // hot path #2: there is nothing to Flush and buffering would not be beneficial - // it's allowed only for FileStream, as the public contract is that BufferedStream - // serializes ALL async operations - else if (_actLikeFileStream && _writePos == 0 && count >= _bufferSize) + finally { - Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); - _readPos = _readLen = 0; - - try - { - return _stream!.ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); - } - finally - { + if (completeSynchronously) // if this is FALSE, we will be entering ReadFromUnderlyingStreamAsync and releasing there. sem.Release(); - } } } @@ -756,35 +667,20 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken Task semaphoreLockTask = sem.WaitAsync(cancellationToken); if (semaphoreLockTask.IsCompletedSuccessfully) { - // hot path #1: there is data in the buffer - if (_readLen - _readPos > 0 || (buffer.Length == 0 && !_actLikeFileStream)) + bool completeSynchronously = true; + try { bytesFromBuffer = ReadFromBuffer(buffer.Span); - - if (bytesFromBuffer == buffer.Length) + completeSynchronously = bytesFromBuffer == buffer.Length; + if (completeSynchronously) { - // if above is FALSE, we will be entering ReadFromUnderlyingStreamAsync and releasing there. - sem.Release(); - // If we satisfied enough data from the buffer, we can complete synchronously. return new ValueTask(bytesFromBuffer); } - - buffer = buffer.Slice(bytesFromBuffer); } - // hot path #2: there is nothing to Flush and buffering would not be beneficial - // it's allowed only for FileStream, as the public contract is that BufferedStream - // serializes ALL async operations - else if (_actLikeFileStream && _writePos == 0 && buffer.Length >= _bufferSize) + finally { - Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); - _readPos = _readLen = 0; - - try - { - return _stream!.ReadAsync(buffer, cancellationToken); - } - finally + if (completeSynchronously) // if this is FALSE, we will be entering ReadFromUnderlyingStreamAsync and releasing there. { sem.Release(); } @@ -792,7 +688,7 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken } // Delegate to the async implementation. - return ReadFromUnderlyingStreamAsync(buffer, cancellationToken, bytesFromBuffer, semaphoreLockTask); + return ReadFromUnderlyingStreamAsync(buffer.Slice(bytesFromBuffer), cancellationToken, bytesFromBuffer, semaphoreLockTask); } /// BufferedStream should be as thin a wrapper as possible. We want ReadAsync to delegate to @@ -811,29 +707,23 @@ private async ValueTask ReadFromUnderlyingStreamAsync( // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. await semaphoreLockTask.ConfigureAwait(false); - try { - int bytesFromBuffer = 0; - - if (!(buffer.Length == 0 && _actLikeFileStream && _readLen == _readPos)) + // The buffer might have been changed by another async task while we were waiting on the semaphore. + // Check it now again. + int bytesFromBuffer = ReadFromBuffer(buffer.Span); + if (bytesFromBuffer == buffer.Length) { - // The buffer might have been changed by another async task while we were waiting on the semaphore. - // Check it now again. - bytesFromBuffer = ReadFromBuffer(buffer.Span); - if (bytesFromBuffer == buffer.Length) - { - return bytesAlreadySatisfied + bytesFromBuffer; - } + return bytesAlreadySatisfied + bytesFromBuffer; + } - if (bytesFromBuffer > 0) - { - buffer = buffer.Slice(bytesFromBuffer); - bytesAlreadySatisfied += bytesFromBuffer; - } + if (bytesFromBuffer > 0) + { + buffer = buffer.Slice(bytesFromBuffer); + bytesAlreadySatisfied += bytesFromBuffer; } - Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); + Debug.Assert(_readLen == _readPos); _readPos = _readLen = 0; // If there was anything in the write buffer, clear it. @@ -888,7 +778,7 @@ private int ReadByteSlow() Debug.Assert(_stream != null); if (_writePos > 0) - FlushWrite(true); + FlushWrite(); EnsureBufferAllocated(); _readLen = _stream.Read(_buffer!, 0, _bufferSize); @@ -1162,7 +1052,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo Debug.Assert(_writePos < _bufferSize); - // hot path #1 If the write completely fits into the buffer, we can complete synchronously: + // If the write completely fits into the buffer, we can complete synchronously: completeSynchronously = buffer.Length < _bufferSize - _writePos; if (completeSynchronously) { @@ -1176,21 +1066,6 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo if (completeSynchronously) // if this is FALSE, we will be entering WriteToUnderlyingStreamAsync and releasing there. sem.Release(); } - - // hot path #2: there is nothing to Flush and buffering would not be beneficial - // it's allowed only for FileStream, as the public contract is that BufferedStream - // serializes ALL async operations - if (_actLikeFileStream && _writePos == 0 && buffer.Length >= _bufferSize) - { - try - { - return _stream!.WriteAsync(buffer, cancellationToken); - } - finally - { - sem.Release(); - } - } } // Delegate to the async implementation. @@ -1220,9 +1095,7 @@ private async ValueTask WriteToUnderlyingStreamAsync( // However, note that if we recalculate the sync completion condition to TRUE, then useBuffer will also be TRUE. if (_writePos == 0) - { ClearReadBufferBeforeWrite(); - } int totalUserBytes; bool useBuffer; @@ -1320,7 +1193,7 @@ private void WriteByteSlow(byte value) // We should not be flushing here, but only writing to the underlying stream, but previous version flushed, so we keep this. if (_writePos >= _bufferSize - 1) - FlushWrite(true); + FlushWrite(); _buffer![_writePos++] = value; @@ -1338,7 +1211,7 @@ public override long Seek(long offset, SeekOrigin origin) { // We should be only writing the buffer and not flushing, // but the previous version did flush and we stick to it for back-compat reasons. - FlushWrite(true); + FlushWrite(); return _stream.Seek(offset, origin); } @@ -1411,7 +1284,7 @@ public override void CopyTo(Stream destination, int bufferSize) else if (_writePos > 0) { // If there's write data in the buffer, flush it back to the underlying stream, as does ReadAsync. - FlushWrite(true); + FlushWrite(); } // Our buffer is now clear. Copy data directly from the source stream to the destination stream. diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs index 54166cd68d82f..982dd40f89d47 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs @@ -24,5 +24,7 @@ internal abstract class FileStreamStrategy : Stream internal abstract void Flush(bool flushToDisk); internal abstract void DisposeInternal(bool disposing); + + internal virtual void OnBufferAllocated(byte[] buffer) { } } } From 3c152b7dc737536a6e02267fd11798b2bae07a2b Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 11 Mar 2021 07:27:09 +0100 Subject: [PATCH 25/28] use buffering code from LegacyFileStream --- .../System/IO/BufferedFileStreamStrategy.cs | 891 +++++++----------- .../src/System/IO/FileStreamStrategy.cs | 2 + .../System/IO/WindowsFileStreamStrategy.cs | 2 + 3 files changed, 370 insertions(+), 525 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs index 7fecd17e2ed65..3be2408f6181f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs @@ -11,8 +11,6 @@ namespace System.IO // this type exists so we can avoid duplicating the buffering logic in every FileStreamStrategy implementation internal sealed class BufferedFileStreamStrategy : FileStreamStrategy { - private const int MaxShadowBufferSize = 81920; // Make sure not to get to the Large Object Heap. - private readonly FileStreamStrategy _strategy; private readonly int _bufferSize; @@ -173,118 +171,108 @@ protected override void Dispose(bool disposing) } } - public override void SetLength(long value) - { - Flush(); - - _strategy.SetLength(value); - } - - // the Read(Array) overload does not just create a Span and call Read(Span) - // because for async file stream strategies the call to Read(Span) - // is translated to Stream.Read(Span), which rents an array from the pool - // copies the data, and then calls Read(Array) public override int Read(byte[] buffer, int offset, int count) { AssertBufferArguments(buffer, offset, count); - EnsureCanRead(); - int bytesFromBuffer = ReadFromBuffer(buffer, offset, count); - - // We may have read less than the number of bytes the user asked for, but that is part of the Stream Debug. - // Reading again for more data may cause us to block if we're using a device with no clear end of file, - // such as a serial port or pipe. If we blocked here and this code was used with redirected pipes for a - // process's standard output, this can lead to deadlocks involving two processes. - // BUT - this is a breaking change. - // So: If we could not read all bytes the user asked for from the buffer, we will try once from the underlying - // stream thus ensuring the same blocking behaviour as if the underlying stream was not wrapped in this BufferedStream. - if (bytesFromBuffer == count - && !(count == 0 && _readLen == _readPos)) // blocking 0 bytes reads are OK only when the read buffer is empty - { - return bytesFromBuffer; - } - - int alreadySatisfied = bytesFromBuffer; - if (bytesFromBuffer > 0) - { - count -= bytesFromBuffer; - offset += bytesFromBuffer; - } - - Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); - _readPos = _readLen = 0; - - // If there was anything in the write buffer, clear it. - if (_writePos > 0) - { - FlushWrite(); - } - - // If the requested read is larger than buffer size, avoid the buffer and still use a single read: - if (count >= _bufferSize) - { - return _strategy.Read(buffer, offset, count) + alreadySatisfied; - } - - // Ok. We can fill the buffer: - EnsureBufferAllocated(); - _readLen = _strategy.Read(_buffer!, 0, _bufferSize); - - bytesFromBuffer = ReadFromBuffer(buffer, offset, count); - - // We may have read less than the number of bytes the user asked for, but that is part of the Stream Debug. - // Reading again for more data may cause us to block if we're using a device with no clear end of stream, - // such as a serial port or pipe. If we blocked here & this code was used with redirected pipes for a process's - // standard output, this can lead to deadlocks involving two processes. Additionally, translating one read on the - // BufferedStream to more than one read on the underlying Stream may defeat the whole purpose of buffering of the - // underlying reads are significantly more expensive. - - return bytesFromBuffer + alreadySatisfied; + return ReadSpan(new Span(buffer, offset, count), new ArraySegment(buffer, offset, count)); } public override int Read(Span destination) { EnsureNotClosed(); - EnsureCanRead(); - // Try to read from the buffer. - int bytesFromBuffer = ReadFromBuffer(destination); - if (bytesFromBuffer == destination.Length - && !(destination.Length == 0 && _readLen == _readPos)) // 0 bytes reads are OK only for FileStream when the read buffer is empty - { - // We got as many bytes as were asked for; we're done. - return bytesFromBuffer; - } + return ReadSpan(destination, default); + } - // We didn't get as many bytes as were asked for from the buffer, so try filling the buffer once. + private int ReadSpan(Span destination, ArraySegment arraySegment) + { + Debug.Assert((_readPos == 0 && _readLen == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLen), + "We're either reading or writing, but not both."); - if (bytesFromBuffer > 0) + bool isBlocked = false; + int n = _readLen - _readPos; + // if the read buffer is empty, read into either user's array or our + // buffer, depending on number of bytes user asked for and buffer size. + if (n == 0) { - destination = destination.Slice(bytesFromBuffer); - } + EnsureCanRead(); - Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); - _readPos = _readLen = 0; + if (_writePos > 0) + { + FlushWrite(); + } - // If there was anything in the write buffer, clear it. - if (_writePos > 0) - { - FlushWrite(); - } + if (!_strategy.CanSeek || (destination.Length >= _bufferSize)) + { + // For async file stream strategies the call to Read(Span) is translated to Stream.Read(Span), + // which rents an array from the pool, copies the data, and then calls Read(Array). This is expensive! + // To avoid that (and code duplication), the Read(Array) method passes ArraySegment to this method + // which allows for calling Strategy.Read(Array) instead of Strategy.Read(Span). + n = arraySegment.Array != null + ? _strategy.Read(arraySegment.Array, arraySegment.Offset, arraySegment.Count) + : _strategy.Read(destination); + + // Throw away read buffer. + _readPos = 0; + _readLen = 0; + return n; + } - if (destination.Length >= _bufferSize) - { - // If the requested read is larger than buffer size, avoid the buffer and just read - // directly into the destination. - return _strategy.Read(destination) + bytesFromBuffer; - } - else - { - // Otherwise, fill the buffer, then read from that. EnsureBufferAllocated(); - _readLen = _strategy.Read(_buffer!, 0, _bufferSize); - return ReadFromBuffer(destination) + bytesFromBuffer; + n = _strategy.Read(_buffer!, 0, _bufferSize); + + if (n == 0) + { + return 0; + } + + isBlocked = n < _bufferSize; + _readPos = 0; + _readLen = n; + } + // Now copy min of count or numBytesAvailable (i.e. near EOF) to array. + if (n > destination.Length) + { + n = destination.Length; + } + new ReadOnlySpan(_buffer!, _readPos, n).CopyTo(destination); + _readPos += n; + + // We may have read less than the number of bytes the user asked + // for, but that is part of the Stream contract. Reading again for + // more data may cause us to block if we're using a device with + // no clear end of file, such as a serial port or pipe. If we + // blocked here & this code was used with redirected pipes for a + // process's standard output, this can lead to deadlocks involving + // two processes. But leave this here for files to avoid what would + // probably be a breaking change. -- + + // If we are reading from a device with no clear EOF like a + // serial port or a pipe, this will cause us to block incorrectly. + if (!_strategy.IsPipe) + { + // If we hit the end of the buffer and didn't have enough bytes, we must + // read some more from the underlying stream. However, if we got + // fewer bytes from the underlying stream than we asked for (i.e. we're + // probably blocked), don't ask for more bytes. + if (n < destination.Length && !isBlocked) + { + Debug.Assert(_readPos == _readLen, "Read buffer should be empty!"); + + int moreBytesRead = arraySegment.Array != null + ? _strategy.Read(arraySegment.Array, arraySegment.Offset + n, arraySegment.Count - n) + : _strategy.Read(destination.Slice(n)); + + n += moreBytesRead; + // We've just made our buffer inconsistent with our position + // pointer. We must throw away the read buffer. + _readPos = 0; + _readLen = 0; + } } + + return n; } public override int ReadByte() => _readPos != _readLen ? _buffer![_readPos++] : ReadByteSlow(); @@ -354,79 +342,131 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return ValueTask.FromCanceled(cancellationToken); } - Debug.Assert(!_strategy.IsClosed, "Strategy.IsClosed was supposed to be validated by FileStream itself"); EnsureCanRead(); - int bytesFromBuffer = 0; - SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); - Task semaphoreLockTask = sem.WaitAsync(cancellationToken); - if (semaphoreLockTask.IsCompletedSuccessfully) + Debug.Assert(!_strategy.IsClosed, "FileStream ensures that strategy is not closed"); + Debug.Assert((_readPos == 0 && _readLen == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLen), + "We're either reading or writing, but not both."); + + SemaphoreSlim semaphore = EnsureAsyncActiveSemaphoreInitialized(); + Task semaphoreLockTask = semaphore.WaitAsync(cancellationToken); + + if (_strategy.IsPipe) { - // hot path #1: there is data in the buffer - if (_readLen - _readPos > 0) - { - bytesFromBuffer = ReadFromBuffer(buffer.Span); + // pipes have a very limited support for buffering + return ReadAsyncPipe(semaphoreLockTask, buffer, cancellationToken); + } - if (bytesFromBuffer == buffer.Length) - { - // if above is FALSE, we will be entering ReadFromUnderlyingStreamAsync and releasing there. - sem.Release(); + if (!semaphoreLockTask.IsCompletedSuccessfully) + { + // we could not acquire the lock, so we fall to slow path that is going to wait for the lock to be released + return ReadAsyncNotPipe(semaphoreLockTask, buffer, cancellationToken); + } - // If we satisfied enough data from the buffer, we can complete synchronously. - return new ValueTask(bytesFromBuffer); - } + int availableToRead = _readLen - _readPos; + if (!(availableToRead >= buffer.Length || (_writePos == 0 && buffer.Length >= _bufferSize))) + { + // there is not enough bytes in the buffer which has already been partially filled + return ReadAsyncNotPipe(semaphoreLockTask, buffer, cancellationToken); + } - buffer = buffer.Slice(bytesFromBuffer); + try + { + // hot path #1: there is enough data in the buffer + if (availableToRead >= buffer.Length) + { + _buffer.AsSpan(_readPos, buffer.Length).CopyTo(buffer.Span); + _readPos += buffer.Length; + return new ValueTask(buffer.Length); } + // hot path #2: there is nothing to Flush and buffering would not be beneficial - else if (_writePos == 0 && buffer.Length >= _bufferSize) - { - Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); - _readPos = _readLen = 0; + Debug.Assert(_writePos == 0 && buffer.Length >= _bufferSize, "Bug introduced in the conditions above"); + Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); + _readPos = _readLen = 0; + return _strategy.ReadAsync(buffer, cancellationToken); + } + finally + { + semaphore.Release(); + } + } - try - { - return _strategy.ReadAsync(buffer, cancellationToken); - } - finally - { - sem.Release(); - } - } + private async ValueTask ReadAsyncPipe(Task semaphoreLockTask, Memory destination, CancellationToken cancellationToken) + { + Debug.Assert(_strategy.IsPipe); + Debug.Assert(_asyncActiveSemaphore != null); + + if (!semaphoreLockTask.IsCompletedSuccessfully) + { + // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. + await semaphoreLockTask.ConfigureAwait(false); } - // Delegate to the async implementation. - return ReadFromUnderlyingStreamAsync(buffer, cancellationToken, bytesFromBuffer, semaphoreLockTask); + try + { + // Pipes are tricky, at least when you have 2 different pipes + // that you want to use simultaneously. When redirecting stdout + // & stderr with the Process class, it's easy to deadlock your + // parent & child processes when doing writes 4K at a time. The + // OS appears to use a 4K buffer internally. If you write to a + // pipe that is full, you will block until someone read from + // that pipe. If you try reading from an empty pipe and + // Win32FileStream's ReadAsync blocks waiting for data to fill it's + // internal buffer, you will be blocked. In a case where a child + // process writes to stdout & stderr while a parent process tries + // reading from both, you can easily get into a deadlock here. + // To avoid this deadlock, don't buffer when doing async IO on + // pipes. But don't completely ignore buffered data either. + if (_readPos < _readLen) + { + int n = Math.Min(_readLen - _readPos, destination.Length); + new Span(_buffer!, _readPos, n).CopyTo(destination.Span); + _readPos += n; + return n; + } + else + { + Debug.Assert(_writePos == 0, "Win32FileStream must not have buffered write data here! Pipes should be unidirectional."); + return await _strategy.ReadAsync(destination, cancellationToken).ConfigureAwait(false); + } + } + finally + { + _asyncActiveSemaphore.Release(); + } } - /// BufferedStream should be as thin a wrapper as possible. We want ReadAsync to delegate to - /// ReadAsync of the underlying _stream rather than calling the base Stream which implements the one in terms of the other. - /// This allows BufferedStream to affect the semantics of the stream it wraps as little as possible. - /// -2 if _bufferSize was set to 0 while waiting on the semaphore; otherwise num of bytes read. - private async ValueTask ReadFromUnderlyingStreamAsync( - Memory buffer, CancellationToken cancellationToken, int bytesAlreadySatisfied, Task semaphoreLockTask) + private async ValueTask ReadAsyncNotPipe(Task semaphoreLockTask, Memory buffer, CancellationToken cancellationToken) { - // Same conditions validated with exceptions in ReadAsync: - Debug.Assert(_strategy.CanRead); - Debug.Assert(_bufferSize > 0); Debug.Assert(_asyncActiveSemaphore != null); - Debug.Assert(semaphoreLockTask != null); + Debug.Assert(!_strategy.IsPipe); - // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. - await semaphoreLockTask.ConfigureAwait(false); + if (!semaphoreLockTask.IsCompletedSuccessfully) + { + // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. + await semaphoreLockTask.ConfigureAwait(false); + } try { int bytesFromBuffer = 0; + int bytesAlreadySatisfied = 0; if (_readLen - _readPos > 0) { // The buffer might have been changed by another async task while we were waiting on the semaphore. // Check it now again. - bytesFromBuffer = ReadFromBuffer(buffer.Span); + bytesFromBuffer = Math.Min(buffer.Length, _readLen - _readPos); + + if (bytesFromBuffer > 0) // don't try to copy 0 bytes + { + _buffer.AsSpan(_readPos, bytesFromBuffer).CopyTo(buffer.Span); + } + if (bytesFromBuffer == buffer.Length) { - return bytesAlreadySatisfied + bytesFromBuffer; + return bytesFromBuffer; } if (bytesFromBuffer > 0) @@ -442,7 +482,8 @@ private async ValueTask ReadFromUnderlyingStreamAsync( // If there was anything in the write buffer, clear it. if (_writePos > 0) { - await FlushWriteAsync(cancellationToken).ConfigureAwait(false); // no Begin-End read version for Flush. Use Async. + await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); + _writePos = 0; } // If the requested read is larger than buffer size, avoid the buffer and still use a single read: @@ -455,7 +496,9 @@ private async ValueTask ReadFromUnderlyingStreamAsync( EnsureBufferAllocated(); _readLen = await _strategy.ReadAsync(new Memory(_buffer, 0, _bufferSize), cancellationToken).ConfigureAwait(false); - bytesFromBuffer = ReadFromBuffer(buffer.Span); + bytesFromBuffer = Math.Min(_readLen, buffer.Length); + _buffer.AsSpan(0, bytesFromBuffer).CopyTo(buffer.Span); + _readPos += bytesFromBuffer; return bytesAlreadySatisfied + bytesFromBuffer; } finally @@ -473,200 +516,85 @@ public override int EndRead(IAsyncResult asyncResult) public override void Write(byte[] buffer, int offset, int count) { AssertBufferArguments(buffer, offset, count); - EnsureCanWrite(); - - if (_writePos == 0) - ClearReadBufferBeforeWrite(); - #region Write algorithm comment - // We need to use the buffer, while avoiding unnecessary buffer usage / memory copies. - // We ASSUME that memory copies are much cheaper than writes to the underlying stream, so if an extra copy is - // guaranteed to reduce the number of writes, we prefer it. - // We pick a simple strategy that makes degenerate cases rare if our assumptions are right. - // - // For ever write, we use a simple heuristic (below) to decide whether to use the buffer. - // The heuristic has the desirable property (*) that if the specified user data can fit into the currently available - // buffer space without filling it up completely, the heuristic will always tell us to use the buffer. It will also - // tell us to use the buffer in cases where the current write would fill the buffer, but the remaining data is small - // enough such that subsequent operations can use the buffer again. - // - // Algorithm: - // Determine whether or not to buffer according to the heuristic (below). - // If we decided to use the buffer: - // Copy as much user data as we can into the buffer. - // If we consumed all data: We are finished. - // Otherwise, write the buffer out. - // Copy the rest of user data into the now cleared buffer (no need to write out the buffer again as the heuristic - // will prevent it from being filled twice). - // If we decided not to use the buffer: - // Can the data already in the buffer and current user data be combines to a single write - // by allocating a "shadow" buffer of up to twice the size of _bufferSize (up to a limit to avoid LOH)? - // Yes, it can: - // Allocate a larger "shadow" buffer and ensure the buffered data is moved there. - // Copy user data to the shadow buffer. - // Write shadow buffer to the underlying stream in a single operation. - // No, it cannot (amount of data is still too large): - // Write out any data possibly in the buffer. - // Write out user data directly. - // - // Heuristic: - // If the subsequent write operation that follows the current write operation will result in a write to the - // underlying stream in case that we use the buffer in the current write, while it would not have if we avoided - // using the buffer in the current write (by writing current user data to the underlying stream directly), then we - // prefer to avoid using the buffer since the corresponding memory copy is wasted (it will not reduce the number - // of writes to the underlying stream, which is what we are optimising for). - // ASSUME that the next write will be for the same amount of bytes as the current write (most common case) and - // determine if it will cause a write to the underlying stream. If the next write is actually larger, our heuristic - // still yields the right behaviour, if the next write is actually smaller, we may making an unnecessary write to - // the underlying stream. However, this can only occur if the current write is larger than half the buffer size and - // we will recover after one iteration. - // We have: - // useBuffer = (_writePos + count + count < _bufferSize + _bufferSize) - // - // Example with _bufferSize = 20, _writePos = 6, count = 10: - // - // +---------------------------------------+---------------------------------------+ - // | current buffer | next iteration's "future" buffer | - // +---------------------------------------+---------------------------------------+ - // |0| | | | | | | | | |1| | | | | | | | | |2| | | | | | | | | |3| | | | | | | | | | - // |0|1|2|3|4|5|6|7|8|9|0|1|2|3|4|5|6|7|8|9|0|1|2|3|4|5|6|7|8|9|0|1|2|3|4|5|6|7|8|9| - // +-----------+-------------------+-------------------+---------------------------+ - // | _writePos | current count | assumed next count|avail buff after next write| - // +-----------+-------------------+-------------------+---------------------------+ - // - // A nice property (*) of this heuristic is that it will always succeed if the user data completely fits into the - // available buffer, i.e. if count < (_bufferSize - _writePos). - #endregion Write algorithm comment - - Debug.Assert(_writePos < _bufferSize); - - int totalUserbytes; - bool useBuffer; - checked - { // We do not expect buffer sizes big enough for an overflow, but if it happens, lets fail early: - totalUserbytes = _writePos + count; - useBuffer = (totalUserbytes + count < (_bufferSize + _bufferSize)); - } - - if (useBuffer) - { - WriteToBuffer(buffer, ref offset, ref count); - - if (_writePos < _bufferSize) - { - Debug.Assert(count == 0); - return; - } - - Debug.Assert(count >= 0); - Debug.Assert(_writePos == _bufferSize); - Debug.Assert(_buffer != null); - - _strategy.Write(_buffer, 0, _writePos); - _writePos = 0; - - WriteToBuffer(buffer, ref offset, ref count); - - Debug.Assert(count == 0); - Debug.Assert(_writePos < _bufferSize); - } - else - { // if (!useBuffer) - // Write out the buffer if necessary. - if (_writePos > 0) - { - Debug.Assert(_buffer != null); - Debug.Assert(totalUserbytes >= _bufferSize); - - // Try avoiding extra write to underlying stream by combining previously buffered data with current user data: - if (totalUserbytes <= (_bufferSize + _bufferSize) && totalUserbytes <= MaxShadowBufferSize) - { - EnsureShadowBufferAllocated(); - Buffer.BlockCopy(buffer, offset, _buffer, _writePos, count); - _strategy.Write(_buffer, 0, totalUserbytes); - _writePos = 0; - return; - } - - _strategy.Write(_buffer, 0, _writePos); - _writePos = 0; - } - - // Write out user data. - _strategy.Write(buffer, offset, count); - } + WriteSpan(new ReadOnlySpan(buffer, offset, count), new ArraySegment(buffer, offset, count)); } public override void Write(ReadOnlySpan buffer) { EnsureNotClosed(); - EnsureCanWrite(); + WriteSpan(buffer, default); + } + + private void WriteSpan(ReadOnlySpan source, ArraySegment arraySegment) + { if (_writePos == 0) { + EnsureCanWrite(); ClearReadBufferBeforeWrite(); } - Debug.Assert(_writePos < _bufferSize, $"Expected {_writePos} < {_bufferSize}"); - int totalUserbytes; - bool useBuffer; - checked - { - // We do not expect buffer sizes big enough for an overflow, but if it happens, lets fail early: - totalUserbytes = _writePos + buffer.Length; - useBuffer = (totalUserbytes + buffer.Length < (_bufferSize + _bufferSize)); - } - - if (useBuffer) + // If our buffer has data in it, copy data from the user's array into + // the buffer, and if we can fit it all there, return. Otherwise, write + // the buffer to disk and copy any remaining data into our buffer. + // The assumption here is memcpy is cheaper than disk (or net) IO. + // (10 milliseconds to disk vs. ~20-30 microseconds for a 4K memcpy) + // So the extra copying will reduce the total number of writes, in + // non-pathological cases (i.e. write 1 byte, then write for the buffer + // size repeatedly) + if (_writePos > 0) { - // Copy as much data to the buffer as will fit. If there's still room in the buffer, - // everything must have fit. - int bytesWritten = WriteToBuffer(buffer); - if (_writePos < _bufferSize) + int numBytes = _bufferSize - _writePos; // space left in buffer + if (numBytes > 0) { - Debug.Assert(bytesWritten == buffer.Length); - return; + if (numBytes >= source.Length) + { + source.CopyTo(_buffer!.AsSpan(_writePos)); + _writePos += source.Length; + return; + } + else + { + source.Slice(0, numBytes).CopyTo(_buffer!.AsSpan(_writePos)); + _writePos += numBytes; + source = source.Slice(numBytes); + } } - buffer = buffer.Slice(bytesWritten); - Debug.Assert(_writePos == _bufferSize); - Debug.Assert(_buffer != null); - - // Output the buffer to the underlying strategy. - _strategy.Write(_buffer, 0, _writePos); - _writePos = 0; - - // Now write the remainder. It must fit, as we're only on this path if that's true. - bytesWritten = WriteToBuffer(buffer); - Debug.Assert(bytesWritten == buffer.Length); - - Debug.Assert(_writePos < _bufferSize); + FlushWrite(); + Debug.Assert(_writePos == 0, "FlushWrite must set _writePos to 0"); } - else // skip the buffer - { - // Flush anything existing in the buffer. - if (_writePos > 0) - { - Debug.Assert(_buffer != null); - Debug.Assert(totalUserbytes >= _bufferSize); - // Try avoiding extra write to underlying stream by combining previously buffered data with current user data: - if (totalUserbytes <= (_bufferSize + _bufferSize) && totalUserbytes <= MaxShadowBufferSize) - { - EnsureShadowBufferAllocated(); - buffer.CopyTo(new Span(_buffer, _writePos, buffer.Length)); - _strategy.Write(_buffer, 0, totalUserbytes); - _writePos = 0; - return; - } + // If the buffer would slow _bufferSize down, avoid buffer completely. + if (source.Length >= _bufferSize) + { + Debug.Assert(_writePos == 0, "FileStream cannot have buffered data to write here! Your stream will be corrupted."); - _strategy.Write(_buffer, 0, _writePos); - _writePos = 0; + // For async file stream strategies the call to Write(Span) is translated to Stream.Write(Span), + // which rents an array from the pool, copies the data, and then calls Write(Array). This is expensive! + // To avoid that (and code duplication), the Write(Array) method passes ArraySegment to this method + // which allows for calling Strategy.Write(Array) instead of Strategy.Write(Span). + if (arraySegment.Array != null) + { + _strategy.Write(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + } + else + { + _strategy.Write(source); } - // Write out user data. - _strategy.Write(buffer); + return; } + else if (source.Length == 0) + { + return; // Don't allocate a buffer then call memcpy for 0 bytes. + } + + // Copy remaining bytes into buffer, to write at a later date. + EnsureBufferAllocated(); + source.CopyTo(_buffer!.AsSpan(_writePos)); + _writePos = source.Length; } public override void WriteByte(byte value) @@ -683,21 +611,19 @@ public override void WriteByte(byte value) private void WriteByteSlow(byte value) { - EnsureNotClosed(); - if (_writePos == 0) { + EnsureNotClosed(); EnsureCanWrite(); ClearReadBufferBeforeWrite(); EnsureBufferAllocated(); } - - if (_writePos >= _bufferSize - 1) + else if (_writePos >= _bufferSize - 1) + { FlushWrite(); + } _buffer![_writePos++] = value; - - Debug.Assert(_writePos < _bufferSize); } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) @@ -714,143 +640,147 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo return ValueTask.FromCanceled(cancellationToken); } - EnsureNotClosed(); EnsureCanWrite(); - // Try to satisfy the request from the buffer synchronously. - SemaphoreSlim sem = EnsureAsyncActiveSemaphoreInitialized(); - Task semaphoreLockTask = sem.WaitAsync(cancellationToken); - if (semaphoreLockTask.IsCompletedSuccessfully) + Debug.Assert(!_strategy.IsClosed, "FileStream ensures that strategy is not closed"); + Debug.Assert((_readPos == 0 && _readLen == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLen), + "We're either reading or writing, but not both."); + Debug.Assert(!_strategy.IsPipe || (_readPos == 0 && _readLen == 0), + "Win32FileStream must not have buffered data here! Pipes should be unidirectional."); + + SemaphoreSlim semaphore = EnsureAsyncActiveSemaphoreInitialized(); + Task semaphoreLockTask = semaphore.WaitAsync(cancellationToken); + + if (_strategy.IsPipe) { - bool completeSynchronously = true; - try - { - if (_writePos == 0) - { - ClearReadBufferBeforeWrite(); - } + // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadAsync) + return WriteAsyncPipe(semaphoreLockTask, buffer, cancellationToken); + } - Debug.Assert(_writePos < _bufferSize); + if (!semaphoreLockTask.IsCompletedSuccessfully) + { + // we could not acquire the lock, so we fall to slow path that is going to wait for the lock to be released + return WriteAsyncNotPipe(semaphoreLockTask, buffer, cancellationToken); + } - // hot path #1 If the write completely fits into the buffer, we can complete synchronously: - completeSynchronously = buffer.Length < _bufferSize - _writePos; - if (completeSynchronously) - { - int bytesWritten = WriteToBuffer(buffer.Span); - Debug.Assert(bytesWritten == buffer.Length); - return default; - } - } - finally + int spaceLeft = _bufferSize - _writePos; + if (!(spaceLeft >= buffer.Length || (_writePos == 0 && buffer.Length >= _bufferSize))) + { + // there is not enough space in the buffer which has already been partially filled + return WriteAsyncNotPipe(semaphoreLockTask, buffer, cancellationToken); + } + + try + { + if (_writePos == 0) { - if (completeSynchronously) // if this is FALSE, we will be entering WriteToUnderlyingStreamAsync and releasing there. - { - sem.Release(); - } + ClearReadBufferBeforeWrite(); } - // hot path #2: there is nothing to Flush and buffering would not be beneficial - if (_writePos == 0 && buffer.Length >= _bufferSize) + // hot path #1 if the write completely fits into the buffer, we can complete synchronously: + if (spaceLeft >= buffer.Length) { - try - { - return _strategy.WriteAsync(buffer, cancellationToken); - } - finally - { - sem.Release(); - } + EnsureBufferAllocated(); + buffer.Span.CopyTo(_buffer.AsSpan(_writePos)); + _writePos += buffer.Length; + + return default; } - } - // Delegate to the async implementation. - return WriteToUnderlyingStreamAsync(buffer, cancellationToken, semaphoreLockTask); + // hot path #2: nothing to flush and buffering is not beneficial + Debug.Assert(_writePos == 0 && buffer.Length >= _bufferSize, "Bug introduced in the conditions above"); + return _strategy.WriteAsync(buffer, cancellationToken); + } + finally + { + semaphore.Release(); + } } - /// BufferedStream should be as thin a wrapper as possible. We want WriteAsync to delegate to - /// WriteAsync of the underlying _stream rather than calling the base Stream which implements the one - /// in terms of the other. This allows BufferedStream to affect the semantics of the stream it wraps as - /// little as possible. - /// - private async ValueTask WriteToUnderlyingStreamAsync( - ReadOnlyMemory buffer, CancellationToken cancellationToken, Task semaphoreLockTask) + private async ValueTask WriteAsyncPipe(Task semaphoreLockTask, ReadOnlyMemory source, CancellationToken cancellationToken) { - Debug.Assert(_strategy.CanWrite); - Debug.Assert(_bufferSize > 0); Debug.Assert(_asyncActiveSemaphore != null); - Debug.Assert(semaphoreLockTask != null); + Debug.Assert(!_strategy.IsPipe); - // See the LARGE COMMENT in Write(..) for the explanation of the write buffer algorithm. + if (!semaphoreLockTask.IsCompletedSuccessfully) + { + await semaphoreLockTask.ConfigureAwait(false); + } - await semaphoreLockTask.ConfigureAwait(false); try { - // The buffer might have been changed by another async task while we were waiting on the semaphore. - // However, note that if we recalculate the sync completion condition to TRUE, then useBuffer will also be TRUE. + await _strategy.WriteAsync(source, cancellationToken).ConfigureAwait(false); + } + finally + { + _asyncActiveSemaphore.Release(); + } + } + + private async ValueTask WriteAsyncNotPipe(Task semaphoreLockTask, ReadOnlyMemory source, CancellationToken cancellationToken) + { + Debug.Assert(_asyncActiveSemaphore != null); + Debug.Assert(!_strategy.IsPipe); + + if (!semaphoreLockTask.IsCompletedSuccessfully) + { + await semaphoreLockTask.ConfigureAwait(false); + } + try + { if (_writePos == 0) { ClearReadBufferBeforeWrite(); } - int totalUserBytes; - bool useBuffer; - checked - { - // We do not expect buffer sizes big enough for an overflow, but if it happens, lets fail early: - totalUserBytes = _writePos + buffer.Length; - useBuffer = (totalUserBytes + buffer.Length < (_bufferSize + _bufferSize)); - } - - if (useBuffer) + // If our buffer has data in it, copy data from the user's array into + // the buffer, and if we can fit it all there, return. Otherwise, write + // the buffer to disk and copy any remaining data into our buffer. + // The assumption here is memcpy is cheaper than disk (or net) IO. + // (10 milliseconds to disk vs. ~20-30 microseconds for a 4K memcpy) + // So the extra copying will reduce the total number of writes, in + // non-pathological cases (i.e. write 1 byte, then write for the buffer + // size repeatedly) + if (_writePos > 0) { - buffer = buffer.Slice(WriteToBuffer(buffer.Span)); - - if (_writePos < _bufferSize) + int spaceLeft = _bufferSize - _writePos; + if (spaceLeft > 0) { - Debug.Assert(buffer.Length == 0); - return; + if (spaceLeft >= source.Length) + { + source.Span.CopyTo(_buffer!.AsSpan(_writePos)); + _writePos += source.Length; + return; + } + else + { + source.Span.Slice(0, spaceLeft).CopyTo(_buffer!.AsSpan(_writePos)); + _writePos += spaceLeft; + source = source.Slice(spaceLeft); + } } - Debug.Assert(buffer.Length >= 0); - Debug.Assert(_writePos == _bufferSize); - Debug.Assert(_buffer != null); - await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); _writePos = 0; + } - int bytesWritten = WriteToBuffer(buffer.Span); - Debug.Assert(bytesWritten == buffer.Length); - - Debug.Assert(_writePos < _bufferSize); - + // If the buffer would slow _bufferSize down, avoid buffer completely. + if (source.Length >= _bufferSize) + { + Debug.Assert(_writePos == 0, "FileStream cannot have buffered data to write here! Your stream will be corrupted."); + await _strategy.WriteAsync(source, cancellationToken).ConfigureAwait(false); + return; } - else // !useBuffer + else if (source.Length == 0) { - // Write out the buffer if necessary. - if (_writePos > 0) - { - Debug.Assert(_buffer != null); - Debug.Assert(totalUserBytes >= _bufferSize); - - // Try avoiding extra write to underlying stream by combining previously buffered data with current user data: - if (totalUserBytes <= (_bufferSize + _bufferSize) && totalUserBytes <= MaxShadowBufferSize) - { - EnsureShadowBufferAllocated(); - buffer.Span.CopyTo(new Span(_buffer, _writePos, buffer.Length)); - - await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, totalUserBytes), cancellationToken).ConfigureAwait(false); - _writePos = 0; - return; - } - - await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); - _writePos = 0; - } - - // Write out user data. - await _strategy.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + return; // Don't allocate a buffer then call memcpy for 0 bytes. } + + // Copy remaining bytes into buffer, to write at a later date. + EnsureBufferAllocated(); + source.Span.CopyTo(_buffer!.AsSpan(_writePos)); + _writePos = source.Length; } finally { @@ -864,6 +794,13 @@ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, As public override void EndWrite(IAsyncResult asyncResult) => TaskToApm.End(asyncResult); + public override void SetLength(long value) + { + Flush(); + + _strategy.SetLength(value); + } + public override void Flush() => Flush(flushToDisk: false); internal override void Flush(bool flushToDisk) @@ -906,7 +843,6 @@ internal override void Flush(bool flushToDisk) _writePos = _readPos = _readLen = 0; } - public override Task FlushAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) @@ -926,7 +862,8 @@ private async Task FlushAsyncInternal(CancellationToken cancellationToken) { if (_writePos > 0) { - await FlushWriteAsync(cancellationToken).ConfigureAwait(false); + await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); + _writePos = 0; Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); return; } @@ -985,7 +922,8 @@ private async Task CopyToAsyncCore(Stream destination, int bufferSize, Cancellat else if (_writePos > 0) { // If there's write data in the buffer, flush it back to the underlying stream, as does ReadAsync. - await FlushWriteAsync(cancellationToken).ConfigureAwait(false); + await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); + _writePos = 0; } // Our buffer is now clear. Copy data directly from the source stream to the destination stream. @@ -1098,78 +1036,6 @@ private void FlushWrite() _writePos = 0; } - private async ValueTask FlushWriteAsync(CancellationToken cancellationToken) - { - Debug.Assert(_readPos == 0 && _readLen == 0, "Read buffer must be empty in FlushWriteAsync!"); - Debug.Assert(_buffer != null && _bufferSize >= _writePos, "Write buffer must be allocated and write position must be in the bounds of the buffer in FlushWriteAsync!"); - - // TODO: we might get rid of the await - await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); - _writePos = 0; - } - - private int ReadFromBuffer(byte[] buffer, int offset, int count) - { - int readbytes = _readLen - _readPos; - Debug.Assert(readbytes >= 0); - - if (readbytes == 0) - { - return 0; - } - - if (readbytes > count) - { - readbytes = count; - } - - Buffer.BlockCopy(_buffer!, _readPos, buffer, offset, readbytes); - _readPos += readbytes; - - return readbytes; - } - - private int ReadFromBuffer(Span destination) - { - int readbytes = Math.Min(_readLen - _readPos, destination.Length); - Debug.Assert(readbytes >= 0); - if (readbytes > 0) - { - new ReadOnlySpan(_buffer, _readPos, readbytes).CopyTo(destination); - _readPos += readbytes; - } - return readbytes; - } - - private void WriteToBuffer(byte[] buffer, ref int offset, ref int count) - { - int bytesToWrite = Math.Min(_bufferSize - _writePos, count); - - if (bytesToWrite <= 0) - { - return; - } - - EnsureBufferAllocated(); - Buffer.BlockCopy(buffer, offset, _buffer!, _writePos, bytesToWrite); - - _writePos += bytesToWrite; - count -= bytesToWrite; - offset += bytesToWrite; - } - - private int WriteToBuffer(ReadOnlySpan buffer) - { - int bytesToWrite = Math.Min(_bufferSize - _writePos, buffer.Length); - if (bytesToWrite > 0) - { - EnsureBufferAllocated(); - buffer.Slice(0, bytesToWrite).CopyTo(new Span(_buffer, _writePos, bytesToWrite)); - _writePos += bytesToWrite; - } - return bytesToWrite; - } - /// /// Called by Write methods to clear the Read Buffer /// @@ -1186,17 +1052,7 @@ private void ClearReadBufferBeforeWrite() // Must have read data. Debug.Assert(_readPos < _readLen); - - // If the underlying stream cannot seek, FlushRead would end up throwing NotSupported. - // However, since the user did not call a method that is intuitively expected to seek, a better message is in order. - // Ideally, we would throw an InvalidOperation here, but for backward compat we have to stick with NotSupported. - if (!_strategy.CanSeek) - ThrowNotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed(); - FlushRead(); - - static void ThrowNotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed() - => throw new NotSupportedException(SR.NotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed); } private void EnsureNotClosed() @@ -1247,26 +1103,11 @@ void AllocateBuffer() // logic kept in a separate method to get EnsureBufferAllo } } - private void EnsureShadowBufferAllocated() - { - Debug.Assert(_buffer != null); - Debug.Assert(_bufferSize > 0); - - // Already have a shadow buffer? - // Or is the user-specified buffer size already so large that we don't want to create one? - if (_buffer.Length != _bufferSize || _bufferSize >= MaxShadowBufferSize) - return; - - byte[] shadowBuffer = new byte[Math.Min(_bufferSize + _bufferSize, MaxShadowBufferSize)]; - Buffer.BlockCopy(_buffer, 0, shadowBuffer, 0, _writePos); - _buffer = shadowBuffer; - } - [Conditional("DEBUG")] private void AssertBufferArguments(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); // FileStream is supposed to call this - Debug.Assert(!_strategy.IsClosed, "Strategy.IsClosed was supposed to be validated by FileStream itself"); + Debug.Assert(!_strategy.IsClosed, "FileStream ensures that strategy is not closed"); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs index 982dd40f89d47..416e48a01eac0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs @@ -17,6 +17,8 @@ internal abstract class FileStreamStrategy : Stream internal abstract bool IsClosed { get; } + internal virtual bool IsPipe => false; + internal abstract void Lock(long position, long length); internal abstract void Unlock(long position, long length); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs index 2dfabe1025a47..45917d0bc6d2c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs @@ -95,6 +95,8 @@ public override long Position internal sealed override bool IsClosed => _fileHandle.IsClosed; + internal sealed override bool IsPipe => _isPipe; + internal sealed override SafeFileHandle SafeFileHandle { get From e54edc5380232451ea23b6088a8da18775924083 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 11 Mar 2021 18:48:46 +0100 Subject: [PATCH 26/28] set `_writePos = 0` in explicit way in Dispose so WriteByte hot path can rely on simple check --- .../src/System/IO/BufferedFileStreamStrategy.cs | 4 ++-- .../src/System/IO/BufferedStream.cs | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs index 3be2408f6181f..bcf09ed6a7340 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs @@ -118,7 +118,7 @@ public override async ValueTask DisposeAsync() // when users have a race condition in their code (i.e. they call // FileStream.Close when calling another method on FileStream like Read). - Debug.Assert(_writePos == 0, "Everything must have been already flushed"); + _writePos = 0; // WriteByte hot path relies on this } } @@ -167,7 +167,7 @@ protected override void Dispose(bool disposing) // Call base.Dispose(bool) to cleanup async IO resources base.Dispose(disposing); - Debug.Assert(_writePos == 0, "Everything must have been already flushed"); + _writePos = 0; } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs index 8c3b907fd0bdc..f6da2fd101d1c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs @@ -237,11 +237,10 @@ protected override void Dispose(bool disposing) { _stream = null; _buffer = null; + _writePos = 0; // WriteByte hot path relies on this // Call base.Dispose(bool) to cleanup async IO resources base.Dispose(disposing); - - Debug.Assert(_writePos == 0, "Everything must have been already flushed"); } } @@ -265,8 +264,7 @@ public override async ValueTask DisposeAsync() { _stream = null; _buffer = null; - - Debug.Assert(_writePos == 0, "Everything must have been already flushed"); + _writePos = 0; } } @@ -407,12 +405,9 @@ private void ClearReadBufferBeforeWrite() // However, since the user did not call a method that is intuitively expected to seek, a better message is in order. // Ideally, we would throw an InvalidOperation here, but for backward compat we have to stick with NotSupported. if (!_stream.CanSeek) - ThrowNotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed(); + throw new NotSupportedException(SR.NotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed); FlushRead(); - - static void ThrowNotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed() - => throw new NotSupportedException(SR.NotSupported_CannotWriteToBufferedStreamIfReadBufferCannotBeFlushed); } private void FlushWrite() From 0294191521c7d7c2739bb6da38bab6e2d5073f17 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 12 Mar 2021 10:39:17 +0100 Subject: [PATCH 27/28] fix ReadAsync and WriteAsync bugs that were discovered by the checked config test runs --- .../System/IO/BufferedFileStreamStrategy.cs | 197 ++++++++---------- 1 file changed, 84 insertions(+), 113 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs index bcf09ed6a7340..cdf56409f5994 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs @@ -310,29 +310,24 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel { AssertBufferArguments(buffer, offset, count); - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - ValueTask readResult = ReadAsync(new Memory(buffer, offset, count), cancellationToken); return readResult.IsCompletedSuccessfully ? LastSyncCompletedReadTask(readResult.Result) : readResult.AsTask(); - } - private Task LastSyncCompletedReadTask(int val) - { - Task? t = _lastSyncCompletedReadTask; - Debug.Assert(t == null || t.IsCompletedSuccessfully); + Task LastSyncCompletedReadTask(int val) + { + Task? t = _lastSyncCompletedReadTask; + Debug.Assert(t == null || t.IsCompletedSuccessfully); - if (t != null && t.Result == val) - return t; + if (t != null && t.Result == val) + return t; - t = Task.FromResult(val); - _lastSyncCompletedReadTask = t; - return t; + t = Task.FromResult(val); + _lastSyncCompletedReadTask = t; + return t; + } } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) @@ -348,61 +343,54 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken Debug.Assert((_readPos == 0 && _readLen == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLen), "We're either reading or writing, but not both."); - SemaphoreSlim semaphore = EnsureAsyncActiveSemaphoreInitialized(); - Task semaphoreLockTask = semaphore.WaitAsync(cancellationToken); - - if (_strategy.IsPipe) + if (_strategy.IsPipe) // pipes have a very limited support for buffering { - // pipes have a very limited support for buffering - return ReadAsyncPipe(semaphoreLockTask, buffer, cancellationToken); + return ReadFromPipeAsync(buffer, cancellationToken); } - if (!semaphoreLockTask.IsCompletedSuccessfully) - { - // we could not acquire the lock, so we fall to slow path that is going to wait for the lock to be released - return ReadAsyncNotPipe(semaphoreLockTask, buffer, cancellationToken); - } + SemaphoreSlim semaphore = EnsureAsyncActiveSemaphoreInitialized(); + Task semaphoreLockTask = semaphore.WaitAsync(cancellationToken); - int availableToRead = _readLen - _readPos; - if (!(availableToRead >= buffer.Length || (_writePos == 0 && buffer.Length >= _bufferSize))) + if (semaphoreLockTask.IsCompletedSuccessfully // lock has been acquired + && _writePos == 0) // there is nothing to flush { - // there is not enough bytes in the buffer which has already been partially filled - return ReadAsyncNotPipe(semaphoreLockTask, buffer, cancellationToken); - } + bool releaseTheLock = true; + try + { + if (_readLen - _readPos >= buffer.Length) + { + // hot path #1: there is enough data in the buffer + _buffer.AsSpan(_readPos, buffer.Length).CopyTo(buffer.Span); + _readPos += buffer.Length; + return new ValueTask(buffer.Length); + } + else if (_readLen == _readPos && buffer.Length >= _bufferSize) + { + // hot path #2: the read buffer is empty and buffering would not be beneficial + return _strategy.ReadAsync(buffer, cancellationToken); + } - try - { - // hot path #1: there is enough data in the buffer - if (availableToRead >= buffer.Length) + releaseTheLock = false; + } + finally { - _buffer.AsSpan(_readPos, buffer.Length).CopyTo(buffer.Span); - _readPos += buffer.Length; - return new ValueTask(buffer.Length); + if (releaseTheLock) + { + semaphore.Release(); + } + // the code is going to call ReadAsyncSlowPath which is going to release the lock } - - // hot path #2: there is nothing to Flush and buffering would not be beneficial - Debug.Assert(_writePos == 0 && buffer.Length >= _bufferSize, "Bug introduced in the conditions above"); - Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); - _readPos = _readLen = 0; - return _strategy.ReadAsync(buffer, cancellationToken); - } - finally - { - semaphore.Release(); } + + return ReadAsyncSlowPath(semaphoreLockTask, buffer, cancellationToken); } - private async ValueTask ReadAsyncPipe(Task semaphoreLockTask, Memory destination, CancellationToken cancellationToken) + private async ValueTask ReadFromPipeAsync(Memory destination, CancellationToken cancellationToken) { Debug.Assert(_strategy.IsPipe); - Debug.Assert(_asyncActiveSemaphore != null); - - if (!semaphoreLockTask.IsCompletedSuccessfully) - { - // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. - await semaphoreLockTask.ConfigureAwait(false); - } + // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. + await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); try { // Pipes are tricky, at least when you have 2 different pipes @@ -437,17 +425,13 @@ private async ValueTask ReadAsyncPipe(Task semaphoreLockTask, Memory } } - private async ValueTask ReadAsyncNotPipe(Task semaphoreLockTask, Memory buffer, CancellationToken cancellationToken) + private async ValueTask ReadAsyncSlowPath(Task semaphoreLockTask, Memory buffer, CancellationToken cancellationToken) { Debug.Assert(_asyncActiveSemaphore != null); Debug.Assert(!_strategy.IsPipe); - if (!semaphoreLockTask.IsCompletedSuccessfully) - { - // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. - await semaphoreLockTask.ConfigureAwait(false); - } - + // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. + await semaphoreLockTask.ConfigureAwait(false); try { int bytesFromBuffer = 0; @@ -462,6 +446,7 @@ private async ValueTask ReadAsyncNotPipe(Task semaphoreLockTask, Memory 0) // don't try to copy 0 bytes { _buffer.AsSpan(_readPos, bytesFromBuffer).CopyTo(buffer.Span); + _readPos += bytesFromBuffer; } if (bytesFromBuffer == buffer.Length) @@ -648,65 +633,55 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo Debug.Assert(!_strategy.IsPipe || (_readPos == 0 && _readLen == 0), "Win32FileStream must not have buffered data here! Pipes should be unidirectional."); - SemaphoreSlim semaphore = EnsureAsyncActiveSemaphoreInitialized(); - Task semaphoreLockTask = semaphore.WaitAsync(cancellationToken); - if (_strategy.IsPipe) { - // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadAsync) - return WriteAsyncPipe(semaphoreLockTask, buffer, cancellationToken); + // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadFromPipeAsync) + return WriteToPipeAsync(buffer, cancellationToken); } - if (!semaphoreLockTask.IsCompletedSuccessfully) - { - // we could not acquire the lock, so we fall to slow path that is going to wait for the lock to be released - return WriteAsyncNotPipe(semaphoreLockTask, buffer, cancellationToken); - } - - int spaceLeft = _bufferSize - _writePos; - if (!(spaceLeft >= buffer.Length || (_writePos == 0 && buffer.Length >= _bufferSize))) - { - // there is not enough space in the buffer which has already been partially filled - return WriteAsyncNotPipe(semaphoreLockTask, buffer, cancellationToken); - } + SemaphoreSlim semaphore = EnsureAsyncActiveSemaphoreInitialized(); + Task semaphoreLockTask = semaphore.WaitAsync(cancellationToken); - try + if (semaphoreLockTask.IsCompletedSuccessfully // lock has been acquired + && _readPos == _readLen) // there is nothing to flush { - if (_writePos == 0) + bool releaseTheLock = true; + try { - ClearReadBufferBeforeWrite(); - } + // hot path #1 if the write completely fits into the buffer, we can complete synchronously: + if (_bufferSize - _writePos >= buffer.Length) + { + EnsureBufferAllocated(); + buffer.Span.CopyTo(_buffer.AsSpan(_writePos)); + _writePos += buffer.Length; + return default; + } + else if (_writePos == 0 && buffer.Length >= _bufferSize) + { + // hot path #2: the write buffer is empty and buffering would not be beneficial + return _strategy.WriteAsync(buffer, cancellationToken); + } - // hot path #1 if the write completely fits into the buffer, we can complete synchronously: - if (spaceLeft >= buffer.Length) + releaseTheLock = false; + } + finally { - EnsureBufferAllocated(); - buffer.Span.CopyTo(_buffer.AsSpan(_writePos)); - _writePos += buffer.Length; - - return default; + if (releaseTheLock) + { + semaphore.Release(); + } + // the code is going to call ReadAsyncSlowPath which is going to release the lock } - - // hot path #2: nothing to flush and buffering is not beneficial - Debug.Assert(_writePos == 0 && buffer.Length >= _bufferSize, "Bug introduced in the conditions above"); - return _strategy.WriteAsync(buffer, cancellationToken); - } - finally - { - semaphore.Release(); } + + return WriteAsyncSlowPath(semaphoreLockTask, buffer, cancellationToken); } - private async ValueTask WriteAsyncPipe(Task semaphoreLockTask, ReadOnlyMemory source, CancellationToken cancellationToken) + private async ValueTask WriteToPipeAsync(ReadOnlyMemory source, CancellationToken cancellationToken) { - Debug.Assert(_asyncActiveSemaphore != null); - Debug.Assert(!_strategy.IsPipe); - - if (!semaphoreLockTask.IsCompletedSuccessfully) - { - await semaphoreLockTask.ConfigureAwait(false); - } + Debug.Assert(_strategy.IsPipe); + await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); try { await _strategy.WriteAsync(source, cancellationToken).ConfigureAwait(false); @@ -717,16 +692,12 @@ private async ValueTask WriteAsyncPipe(Task semaphoreLockTask, ReadOnlyMemory source, CancellationToken cancellationToken) + private async ValueTask WriteAsyncSlowPath(Task semaphoreLockTask, ReadOnlyMemory source, CancellationToken cancellationToken) { Debug.Assert(_asyncActiveSemaphore != null); Debug.Assert(!_strategy.IsPipe); - if (!semaphoreLockTask.IsCompletedSuccessfully) - { - await semaphoreLockTask.ConfigureAwait(false); - } - + await semaphoreLockTask.ConfigureAwait(false); try { if (_writePos == 0) From c73558a42a6e22d8bb0eaacf6780ec5149f3fcc6 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 12 Mar 2021 11:36:26 +0100 Subject: [PATCH 28/28] move the finalization logic to the finalizer --- .../System/IO/BufferedFileStreamStrategy.cs | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs index cdf56409f5994..eb802eb646efb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs @@ -29,7 +29,20 @@ internal BufferedFileStreamStrategy(FileStreamStrategy strategy, int bufferSize) _bufferSize = bufferSize; } - ~BufferedFileStreamStrategy() => DisposeInternal(false); + ~BufferedFileStreamStrategy() + { + try + { + // the finalizer must at least try to flush the write buffer + // so we enforce it by passing always true + Dispose(true); + } + catch (Exception e) when (FileStream.IsIoRelatedException(e)) + { + // On finalization, ignore failures from trying to flush the write buffer, + // e.g. if this stream is wrapping a pipe and the pipe is now broken. + } + } public override bool CanRead => _strategy.CanRead; @@ -122,25 +135,7 @@ public override async ValueTask DisposeAsync() } } - internal override void DisposeInternal(bool disposing) - { - try - { - // the finalizer must at least try to flush the write buffer - // so we enforce it by passing always true - Dispose(true); - } - catch (Exception e) when (!disposing && FileStream.IsIoRelatedException(e)) - { - // On finalization, ignore failures from trying to flush the write buffer, - // e.g. if this stream is wrapping a pipe and the pipe is now broken. - } - - if (disposing) - { - GC.SuppressFinalize(this); - } - } + internal override void DisposeInternal(bool disposing) => Dispose(disposing); protected override void Dispose(bool disposing) {