diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs index 7f7fd8ad240bab..99f51624cc1aa7 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs @@ -179,11 +179,12 @@ public static IEnumerable MemberData_FileStreamAsyncWriting() } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] // Browser PNSE: Cannot wait on monitors + [PlatformSpecific(TestPlatforms.Windows)] public Task ManyConcurrentWriteAsyncs() { // For inner loop, just test one case return ManyConcurrentWriteAsyncs_OuterLoop( - useAsync: OperatingSystem.IsWindows(), + useAsync: true, presize: false, exposeHandle: false, cancelable: true, @@ -193,6 +194,7 @@ public Task ManyConcurrentWriteAsyncs() } [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [PlatformSpecific(TestPlatforms.Windows)] // testing undocumented feature that's legacy in the Windows implementation [MemberData(nameof(MemberData_FileStreamAsyncWriting))] [OuterLoop] // many combinations: we test just one in inner loop and the rest outer public async Task ManyConcurrentWriteAsyncs_OuterLoop( diff --git a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj index e9ab0a7365e303..5c4ca4d01d7c61 100644 --- a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj @@ -1,9 +1,8 @@ - + true true - - $(NetCoreAppCurrent)-windows + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix --working-dir=/test-dir diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.ThreadPoolValueTaskSource.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.ThreadPoolValueTaskSource.cs index e9f06359326d24..49638af80f34fe 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.ThreadPoolValueTaskSource.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.ThreadPoolValueTaskSource.cs @@ -21,17 +21,12 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid internal ThreadPoolValueTaskSource GetThreadPoolValueTaskSource() => Interlocked.Exchange(ref _reusableThreadPoolValueTaskSource, null) ?? new ThreadPoolValueTaskSource(this); - private void TryToReuse(ThreadPoolValueTaskSource source) - { - Interlocked.CompareExchange(ref _reusableThreadPoolValueTaskSource, source, null); - } - /// /// A reusable implementation that /// queues asynchronous operations to /// be completed synchronously on the thread pool. /// - internal sealed class ThreadPoolValueTaskSource : IThreadPoolWorkItem, IValueTaskSource, IValueTaskSource + internal sealed class ThreadPoolValueTaskSource : IThreadPoolWorkItem, IValueTaskSource, IValueTaskSource, IValueTaskSource { private readonly SafeFileHandle _fileHandle; private ManualResetValueTaskSourceCore _source; @@ -68,7 +63,7 @@ private long GetResultAndRelease(short token) finally { _source.Reset(); - _fileHandle.TryToReuse(this); + Volatile.Write(ref _fileHandle._reusableThreadPoolValueTaskSource, this); } } @@ -77,6 +72,7 @@ public void OnCompleted(Action continuation, object? state, short token _source.OnCompleted(continuation, state, token, flags); int IValueTaskSource.GetResult(short token) => (int) GetResultAndRelease(token); long IValueTaskSource.GetResult(short token) => GetResultAndRelease(token); + void IValueTaskSource.GetResult(short token) => GetResultAndRelease(token); private void ExecuteInternal() { diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index 1ce14b82abe99c..93c2907fb45ddf 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.IO; using System.IO.Strategies; +using System.Threading; namespace Microsoft.Win32.SafeHandles { @@ -28,6 +29,10 @@ private SafeFileHandle(bool ownsHandle) internal bool CanSeek => !IsClosed && GetCanSeek(); + internal ThreadPoolBoundHandle? ThreadPoolBinding => null; + + internal void EnsureThreadPoolBindingInitialized() { /* nop */ } + /// Opens the specified file with the requested flags and mode. /// The path to the file. /// The flags with which to open the file. diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index 651b49be0226d4..a7c4751a094786 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -24,8 +24,6 @@ public SafeFileHandle() : base(true) internal bool CanSeek => !IsClosed && GetFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; - internal bool IsPipe => GetFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE; - internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; } internal static unsafe SafeFileHandle Open(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) 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 f344b005f933aa..834ed8b32295ca 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 @@ -459,6 +459,7 @@ + @@ -1813,7 +1814,6 @@ - @@ -2084,8 +2084,7 @@ - - + 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 9653bd057bac3a..1805e6cbb3da43 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -479,10 +479,17 @@ public override long Position public override ValueTask DisposeAsync() => _strategy.DisposeAsync(); - public override void CopyTo(Stream destination, int bufferSize) => _strategy.CopyTo(destination, bufferSize); + public override void CopyTo(Stream destination, int bufferSize) + { + ValidateCopyToArguments(destination, bufferSize); + _strategy.CopyTo(destination, bufferSize); + } public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - => _strategy.CopyToAsync(destination, bufferSize, cancellationToken); + { + ValidateCopyToArguments(destination, bufferSize); + return _strategy.CopyToAsync(destination, bufferSize, cancellationToken); + } public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs index bc3a04f2f5d971..41de56fcbea7ed 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs @@ -28,7 +28,12 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer { fixed (byte* bufPtr = &MemoryMarshal.GetReference(buffer)) { - int result = Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset); + // The Windows implementation uses ReadFile, which ignores the offset if the handle + // isn't seekable. We do the same manually with PRead vs Read, in order to enable + // the function to be used by FileStream for all the same situations. + int result = handle.CanSeek ? + Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset) : + Interop.Sys.Read(handle, bufPtr, buffer.Length); FileStreamHelpers.CheckFileCall(result, handle.Path); return result; } @@ -67,7 +72,7 @@ internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnly return FileStreamHelpers.CheckFileCall(result, handle.Path); } - private static ValueTask ReadAtOffsetAsync(SafeFileHandle handle, Memory buffer, long fileOffset, CancellationToken cancellationToken) + internal static ValueTask ReadAtOffsetAsync(SafeFileHandle handle, Memory buffer, long fileOffset, CancellationToken cancellationToken) => ScheduleSyncReadAtOffsetAsync(handle, buffer, fileOffset, cancellationToken); private static ValueTask ReadScatterAtOffsetAsync(SafeFileHandle handle, IReadOnlyList> buffers, @@ -78,9 +83,14 @@ internal static unsafe int WriteAtOffset(SafeFileHandle handle, ReadOnlySpan WriteAtOffsetAsync(SafeFileHandle handle, ReadOnlyMemory buffer, long fileOffset, CancellationToken cancellationToken) + internal static ValueTask WriteAtOffsetAsync(SafeFileHandle handle, ReadOnlyMemory buffer, long fileOffset, CancellationToken cancellationToken) => ScheduleSyncWriteAtOffsetAsync(handle, buffer, fileOffset, cancellationToken); private static ValueTask WriteGatherAtOffsetAsync(SafeFileHandle handle, IReadOnlyList> buffers, diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs index 9329c0fa60b13c..c83b58921c4e64 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs @@ -8,7 +8,7 @@ namespace System.IO.Strategies { - internal sealed partial class AsyncWindowsFileStreamStrategy : WindowsFileStreamStrategy + internal sealed partial class AsyncWindowsFileStreamStrategy : OSFileStreamStrategy { internal AsyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access, FileShare share) : base(handle, access, share) @@ -98,12 +98,8 @@ private Exception HandleIOError(long positionBefore, int errorCode) return SafeFileHandle.OverlappedValueTaskSource.GetIOError(errorCode, _fileHandle.Path); } - public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; // no buffering = nothing to flush - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { - ValidateCopyToArguments(destination, bufferSize); - // Fail if the file was closed if (_fileHandle.IsClosed) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs index 22fb3e0c14022a..11d21121beea66 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs @@ -247,7 +247,7 @@ private int ReadSpan(Span destination, ArraySegment arraySegment) // 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 (_strategy.CanSeek) { // 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 @@ -340,9 +340,9 @@ 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."); - if (_strategy.IsPipe) // pipes have a very limited support for buffering + if (!_strategy.CanSeek) { - return ReadFromPipeAsync(buffer, cancellationToken); + return ReadFromNonSeekableAsync(buffer, cancellationToken); } SemaphoreSlim semaphore = EnsureAsyncActiveSemaphoreInitialized(); @@ -383,9 +383,9 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return ReadAsyncSlowPath(semaphoreLockTask, buffer, cancellationToken); } - private async ValueTask ReadFromPipeAsync(Memory destination, CancellationToken cancellationToken) + private async ValueTask ReadFromNonSeekableAsync(Memory destination, CancellationToken cancellationToken) { - Debug.Assert(_strategy.IsPipe); + Debug.Assert(!_strategy.CanSeek); // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); @@ -426,7 +426,7 @@ private async ValueTask ReadFromPipeAsync(Memory destination, Cancell private async ValueTask ReadAsyncSlowPath(Task semaphoreLockTask, Memory buffer, CancellationToken cancellationToken) { Debug.Assert(_asyncActiveSemaphore != null); - Debug.Assert(!_strategy.IsPipe); + Debug.Assert(_strategy.CanSeek); // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. await semaphoreLockTask.ConfigureAwait(false); @@ -633,13 +633,13 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo 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), + Debug.Assert(_strategy.CanSeek || (_readPos == 0 && _readLen == 0), "Win32FileStream must not have buffered data here! Pipes should be unidirectional."); - if (_strategy.IsPipe) + if (!_strategy.CanSeek) { // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadFromPipeAsync) - return WriteToPipeAsync(buffer, cancellationToken); + return WriteToNonSeekableAsync(buffer, cancellationToken); } SemaphoreSlim semaphore = EnsureAsyncActiveSemaphoreInitialized(); @@ -690,9 +690,9 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo return WriteAsyncSlowPath(semaphoreLockTask, buffer, cancellationToken); } - private async ValueTask WriteToPipeAsync(ReadOnlyMemory source, CancellationToken cancellationToken) + private async ValueTask WriteToNonSeekableAsync(ReadOnlyMemory source, CancellationToken cancellationToken) { - Debug.Assert(_strategy.IsPipe); + Debug.Assert(!_strategy.CanSeek); await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); try @@ -708,7 +708,7 @@ private async ValueTask WriteToPipeAsync(ReadOnlyMemory source, Cancellati private async ValueTask WriteAsyncSlowPath(Task semaphoreLockTask, ReadOnlyMemory source, CancellationToken cancellationToken) { Debug.Assert(_asyncActiveSemaphore != null); - Debug.Assert(!_strategy.IsPipe); + Debug.Assert(_strategy.CanSeek); await semaphoreLockTask.ConfigureAwait(false); try @@ -878,7 +878,6 @@ private async Task FlushAsyncInternal(CancellationToken cancellationToken) public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { - ValidateCopyToArguments(destination, bufferSize); EnsureNotClosed(); EnsureCanRead(); @@ -921,7 +920,6 @@ private async Task CopyToAsyncCore(Stream destination, int bufferSize, Cancellat public override void CopyTo(Stream destination, int bufferSize) { - ValidateCopyToArguments(destination, bufferSize); EnsureNotClosed(); EnsureCanRead(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs index 868ccb84fd7f21..1d508870956788 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs @@ -8,12 +8,11 @@ namespace System.IO.Strategies // this type defines a set of stateless FileStream/FileStreamStrategy helper methods internal static partial class FileStreamHelpers { - // in the future we are most probably going to introduce more strategies (io_uring etc) - private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) - => new Net5CompatFileStreamStrategy(handle, access, bufferSize == 0 ? 1 : bufferSize, isAsync); + private static OSFileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, bool isAsync) => + new UnixFileStreamStrategy(handle, access, share); - private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) - => new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize == 0 ? 1 : bufferSize, options, preallocationSize); + private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) => + new UnixFileStreamStrategy(path, mode, access, share, options, preallocationSize); internal static long CheckFileCall(long result, string? path, bool ignoreNotSupported = false) { @@ -28,5 +27,56 @@ internal static long CheckFileCall(long result, string? path, bool ignoreNotSupp return result; } + + internal static void ValidateFileTypeForNonExtendedPaths(SafeFileHandle handle, string originalPath) { /* nop */ } + + internal static long Seek(SafeFileHandle handle, long offset, SeekOrigin origin, bool closeInvalidHandle = false) => + CheckFileCall(Interop.Sys.LSeek(handle, offset, (Interop.Sys.SeekWhence)(int)origin), handle.Path); // SeekOrigin values are the same as Interop.libc.SeekWhence values + + internal static void ThrowInvalidArgument(SafeFileHandle handle) => + throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(Interop.Error.EINVAL), handle.Path); + + internal static unsafe void SetFileLength(SafeFileHandle handle, long length) => + CheckFileCall(Interop.Sys.FTruncate(handle, length), handle.Path); + + /// Flushes the file's OS buffer. + internal static void FlushToDisk(SafeFileHandle handle) + { + if (Interop.Sys.FSync(handle) < 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + switch (errorInfo.Error) + { + case Interop.Error.EROFS: + case Interop.Error.EINVAL: + case Interop.Error.ENOTSUP: + // Ignore failures for special files that don't support synchronization. + // In such cases there's nothing to flush. + break; + default: + throw Interop.GetExceptionForIoErrno(errorInfo, handle.Path, isDirectory: false); + } + } + } + + internal static void Lock(SafeFileHandle handle, bool canWrite, long position, long length) + { + if (OperatingSystem.IsOSXLike()) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_OSXFileLocking); + } + + CheckFileCall(Interop.Sys.LockFileRegion(handle, position, length, canWrite ? Interop.Sys.LockType.F_WRLCK : Interop.Sys.LockType.F_RDLCK), handle.Path); + } + + internal static void Unlock(SafeFileHandle handle, long position, long length) + { + if (OperatingSystem.IsOSXLike()) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_OSXFileLocking); + } + + CheckFileCall(Interop.Sys.LockFileRegion(handle, position, length, Interop.Sys.LockType.F_UNLCK), handle.Path); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs index 17c03b7f931863..3f29ed92b431f6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs @@ -27,38 +27,15 @@ internal static class TaskSourceCodes internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32; } - private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) - { - if (UseNet5CompatStrategy) - { - // The .NET 5 Compat strategy does not support bufferSize == 0. - // To minimize the risk of introducing bugs to it, we just pass 1 to disable the buffering. - return new Net5CompatFileStreamStrategy(handle, access, bufferSize == 0 ? 1 : bufferSize, isAsync); - } - - WindowsFileStreamStrategy strategy = isAsync - ? new AsyncWindowsFileStreamStrategy(handle, access, share) - : new SyncWindowsFileStreamStrategy(handle, access, share); - - return EnableBufferingIfNeeded(strategy, bufferSize); - } - - private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) - { - if (UseNet5CompatStrategy) - { - return new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize == 0 ? 1 : bufferSize, options, preallocationSize); - } + private static OSFileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, bool isAsync) => + isAsync ? + new AsyncWindowsFileStreamStrategy(handle, access, share) : + new SyncWindowsFileStreamStrategy(handle, access, share); - WindowsFileStreamStrategy strategy = (options & FileOptions.Asynchronous) != 0 - ? new AsyncWindowsFileStreamStrategy(path, mode, access, share, options, preallocationSize) - : new SyncWindowsFileStreamStrategy(path, mode, access, share, options, preallocationSize); - - return EnableBufferingIfNeeded(strategy, bufferSize); - } - - internal static FileStreamStrategy EnableBufferingIfNeeded(WindowsFileStreamStrategy strategy, int bufferSize) - => bufferSize > 1 ? new BufferedFileStreamStrategy(strategy, bufferSize) : strategy; + private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) => + (options & FileOptions.Asynchronous) != 0 ? + new AsyncWindowsFileStreamStrategy(path, mode, access, share, options, preallocationSize) : + new SyncWindowsFileStreamStrategy(path, mode, access, share, options, preallocationSize); internal static void FlushToDisk(SafeFileHandle handle) { @@ -87,6 +64,9 @@ internal static long Seek(SafeFileHandle handle, long offset, SeekOrigin origin, return ret; } + internal static void ThrowInvalidArgument(SafeFileHandle handle) => + throw Win32Marshal.GetExceptionForWin32Error(Interop.Errors.ERROR_INVALID_PARAMETER, handle.Path); + internal static int GetLastWin32ErrorAndDisposeHandleIfInvalid(SafeFileHandle handle) { int errorCode = Marshal.GetLastPInvokeError(); @@ -116,7 +96,7 @@ internal static int GetLastWin32ErrorAndDisposeHandleIfInvalid(SafeFileHandle ha return errorCode; } - internal static void Lock(SafeFileHandle handle, long position, long length) + internal static void Lock(SafeFileHandle handle, bool canWrite, long position, long length) { int positionLow = unchecked((int)(position)); int positionHigh = unchecked((int)(position >> 32)); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs index 965e9c09f04e15..2c05acc1b0a1af 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs @@ -14,10 +14,28 @@ internal static partial class FileStreamHelpers internal static bool UseNet5CompatStrategy { get; } = AppContextConfigHelper.GetBooleanConfig("System.IO.UseNet5CompatFileStream", "DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM"); internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) - => WrapIfDerivedType(fileStream, ChooseStrategyCore(handle, access, share, bufferSize, isAsync)); + { + // The .NET 5 Compat strategy does not support bufferSize == 0. + // To minimize the risk of introducing bugs to it, we just pass 1 to disable the buffering. + + FileStreamStrategy strategy = UseNet5CompatStrategy ? + new Net5CompatFileStreamStrategy(handle, access, bufferSize == 0 ? 1 : bufferSize, isAsync) : + EnableBufferingIfNeeded(ChooseStrategyCore(handle, access, share, isAsync), bufferSize); + + return WrapIfDerivedType(fileStream, strategy); + } internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) - => WrapIfDerivedType(fileStream, ChooseStrategyCore(path, mode, access, share, bufferSize, options, preallocationSize)); + { + FileStreamStrategy strategy = UseNet5CompatStrategy ? + new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize == 0 ? 1 : bufferSize, options, preallocationSize) : + EnableBufferingIfNeeded(ChooseStrategyCore(path, mode, access, share, options, preallocationSize), bufferSize); + + return WrapIfDerivedType(fileStream, strategy); + } + + private static FileStreamStrategy EnableBufferingIfNeeded(FileStreamStrategy strategy, int bufferSize) + => bufferSize > 1 ? new BufferedFileStreamStrategy(strategy, bufferSize) : strategy; private static FileStreamStrategy WrapIfDerivedType(FileStream fileStream, FileStreamStrategy strategy) => fileStream.GetType() == typeof(FileStream) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamStrategy.cs index c94892a9010ec3..6cec66e7ac89b0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamStrategy.cs @@ -17,8 +17,6 @@ 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/Strategies/Net5CompatFileStreamStrategy.Lock.OSX.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Lock.OSX.cs deleted file mode 100644 index d3ac0b7cf7f167..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Lock.OSX.cs +++ /dev/null @@ -1,18 +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.Strategies -{ - internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy - { - internal override void Lock(long position, long length) - { - throw new PlatformNotSupportedException(SR.PlatformNotSupported_OSXFileLocking); - } - - internal override void Unlock(long position, long length) - { - throw new PlatformNotSupportedException(SR.PlatformNotSupported_OSXFileLocking); - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Lock.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Lock.Unix.cs deleted file mode 100644 index 20e065d6317fd5..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Lock.Unix.cs +++ /dev/null @@ -1,24 +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.Strategies -{ - internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy - { - /// Prevents other processes from reading from or writing to the FileStream. - /// The beginning of the range to lock. - /// The range to be locked. - internal override void Lock(long position, long length) - { - CheckFileCall(Interop.Sys.LockFileRegion(_fileHandle, position, length, CanWrite ? Interop.Sys.LockType.F_WRLCK : Interop.Sys.LockType.F_RDLCK)); - } - - /// Allows access by other processes to all or part of a file that was previously locked. - /// The beginning of the range to unlock. - /// The range to be unlocked. - internal override void Unlock(long position, long length) - { - CheckFileCall(Interop.Sys.LockFileRegion(_fileHandle, position, length, Interop.Sys.LockType.F_UNLCK)); - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs index 6c225186a3b4b0..2feffb5cb6be54 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs @@ -79,6 +79,18 @@ public override long Length } } + /// Prevents other processes from reading from or writing to the FileStream. + /// The beginning of the range to lock. + /// The range to be locked. + internal override void Lock(long position, long length) => + FileStreamHelpers.Lock(_fileHandle, CanWrite, position, length); + + /// Allows access by other processes to all or part of a file that was previously locked. + /// The beginning of the range to unlock. + /// The range to be unlocked. + internal override void Unlock(long position, long length) => + FileStreamHelpers.Unlock(_fileHandle, position, length); + /// Releases the unmanaged resources used by the stream. /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) @@ -141,26 +153,6 @@ public override ValueTask DisposeAsync() return base.DisposeAsync(); } - /// Flushes the OS buffer. This does not flush the internal read/write buffer. - private void FlushOSBuffer() - { - if (Interop.Sys.FSync(_fileHandle) < 0) - { - Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); - switch (errorInfo.Error) - { - case Interop.Error.EROFS: - case Interop.Error.EINVAL: - case Interop.Error.ENOTSUP: - // Ignore failures due to the FileStream being bound to a special file that - // doesn't support synchronization. In such cases there's nothing to flush. - break; - default: - throw Interop.GetExceptionForIoErrno(errorInfo, _fileHandle.Path, isDirectory: false); - } - } - } - private void FlushWriteBufferForWriteByte() { #pragma warning disable CA1416 // Validate platform compatibility, issue: https://github.com/dotnet/runtime/issues/44542 @@ -589,26 +581,6 @@ public override long Seek(long offset, SeekOrigin origin) return pos; } - /// Sets the current position of this stream to the given value. - /// The file handle on which to seek. - /// The point relative to origin from which to begin seeking. - /// - /// Specifies the beginning, the end, or the current position as a reference - /// point for offset, using a value of type SeekOrigin. - /// - /// not used in Unix implementation - /// The new position in the stream. - private long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin, bool closeInvalidHandle = false) - { - Debug.Assert(!fileHandle.IsInvalid); - Debug.Assert(fileHandle.CanSeek); - Debug.Assert(origin >= SeekOrigin.Begin && origin <= SeekOrigin.End); - - long pos = FileStreamHelpers.CheckFileCall(Interop.Sys.LSeek(fileHandle, offset, (Interop.Sys.SeekWhence)(int)origin), fileHandle.Path); // SeekOrigin values are the same as Interop.libc.SeekWhence values - _filePosition = pos; - return pos; - } - private int CheckFileCall(int result, bool ignoreNotSupported = false) { FileStreamHelpers.CheckFileCall(result, _fileHandle?.Path, ignoreNotSupported); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Windows.cs index 3218baa33e7738..e9e4e640a0a33d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Windows.cs @@ -179,8 +179,6 @@ public override async ValueTask DisposeAsync() } } - private void FlushOSBuffer() => FileStreamHelpers.FlushToDisk(_fileHandle); - // Returns a task that flushes the internal write buffer private Task FlushWriteAsync(CancellationToken cancellationToken) { @@ -318,7 +316,7 @@ private int ReadSpan(Span destination) // 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 (!_fileHandle.IsPipe) + if (_fileHandle.CanSeek) { // 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 @@ -466,16 +464,6 @@ public override long Seek(long offset, SeekOrigin origin) 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 - private long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin, bool closeInvalidHandle = false) - { - Debug.Assert(fileHandle.CanSeek, "fileHandle.CanSeek"); - - return _filePosition = FileStreamHelpers.Seek(fileHandle, offset, origin, closeInvalidHandle); - } - partial void OnBufferAllocated() { Debug.Assert(_buffer != null); @@ -593,7 +581,7 @@ private unsafe void WriteCore(ReadOnlySpan source) Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); - if (_fileHandle.IsPipe) + if (!_fileHandle.CanSeek) { // Pipes are tricky, at least when you have 2 different pipes // that you want to use simultaneously. When redirecting stdout @@ -624,7 +612,7 @@ private unsafe void WriteCore(ReadOnlySpan source) } } - Debug.Assert(!_fileHandle.IsPipe, "Should not be a pipe."); + Debug.Assert(_fileHandle.CanSeek, "Should be seekable"); // Handle buffering. if (_writePos > 0) FlushWriteBuffer(); @@ -803,12 +791,12 @@ private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationTo { Debug.Assert(_useAsyncIO); Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); - Debug.Assert(!_fileHandle.IsPipe || (_readPos == 0 && _readLength == 0), "Win32FileStream must not have buffered data here! Pipes should be unidirectional."); + Debug.Assert(_fileHandle.CanSeek || (_readPos == 0 && _readLength == 0), "Win32FileStream must not have buffered data here! Pipes should be unidirectional."); if (!CanWrite) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); bool writeDataStoredInBuffer = false; - if (!_fileHandle.IsPipe) // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadInternalAsyncCore) + if (_fileHandle.CanSeek) // avoid async buffering with non-seekable files (e.g. pipes), as doing so can lead to deadlocks (see comments in ReadInternalAsyncCore) { // Ensure the buffer is clear for writing if (_writePos == 0) @@ -1044,8 +1032,6 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio return base.CopyToAsync(destination, bufferSize, cancellationToken); } - ValidateCopyToArguments(destination, bufferSize); - // Fail if the file was closed if (_fileHandle.IsClosed) { @@ -1113,7 +1099,7 @@ await FileStreamHelpers } } - internal override void Lock(long position, long length) => FileStreamHelpers.Lock(_fileHandle, position, length); + internal override void Lock(long position, long length) => FileStreamHelpers.Lock(_fileHandle, CanWrite, position, length); internal override void Unlock(long position, long length) => FileStreamHelpers.Unlock(_fileHandle, position, length); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs index f01791ba9639e9..797f6f5e9c0ce8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs @@ -159,7 +159,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel // 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)base.BeginReadInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); + return BeginReadInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); } return ReadAsyncTask(buffer, offset, count, cancellationToken); @@ -174,7 +174,7 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken // 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)base.BeginReadInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : + new ValueTask(BeginReadInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : base.ReadAsync(buffer, cancellationToken); } @@ -241,7 +241,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati // 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)base.BeginWriteInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); + return BeginWriteInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); } return WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); @@ -256,7 +256,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo // 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)base.BeginWriteInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : + new ValueTask(BeginWriteInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : base.WriteAsync(buffer, cancellationToken); } @@ -271,7 +271,7 @@ internal override void Flush(bool flushToDisk) if (flushToDisk && CanWrite) { - FlushOSBuffer(); + FileStreamHelpers.FlushToDisk(_fileHandle); } } @@ -371,6 +371,16 @@ public override long Position } } + // 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 + private long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin, bool closeInvalidHandle = false) + { + Debug.Assert(fileHandle.CanSeek, "fileHandle.CanSeek"); + + return _filePosition = FileStreamHelpers.Seek(fileHandle, offset, origin, closeInvalidHandle); + } + internal override bool IsClosed => _fileHandle.IsClosed; /// diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/WindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs similarity index 76% rename from src/libraries/System.Private.CoreLib/src/System/IO/Strategies/WindowsFileStreamStrategy.cs rename to src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs index f03c07fd4d0a09..2be66fffde521e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/WindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs @@ -2,13 +2,14 @@ // 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.Strategies { - // this type serves some basic functionality that is common for Async and Sync Windows File Stream Strategies - internal abstract class WindowsFileStreamStrategy : FileStreamStrategy + // this type serves some basic functionality that is common for native OS File Stream Strategies + internal abstract class OSFileStreamStrategy : FileStreamStrategy { protected readonly SafeFileHandle _fileHandle; // only ever null if ctor throws private readonly FileAccess _access; // What file was opened for. @@ -16,10 +17,10 @@ internal abstract class WindowsFileStreamStrategy : FileStreamStrategy protected long _filePosition; private long _appendStart; // When appending, prevent overwriting file. - private long _length = -1; // When the file is locked for writes (_share <= FileShare.Read) cache file length in-memory, negative means that hasn't been fetched. + private long _length = -1; // When the file is locked for writes on Windows (_share <= FileShare.Read) cache file length in-memory, negative means that hasn't been fetched. private bool _exposedHandle; // created from handle, or SafeFileHandle was used and the handle got exposed - internal WindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access, FileShare share) + internal OSFileStreamStrategy(SafeFileHandle handle, FileAccess access, FileShare share) { _access = access; _share = share; @@ -41,7 +42,7 @@ internal WindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access, Fil _fileHandle = handle; } - internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) + internal OSFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) { string fullPath = Path.GetFullPath(path); @@ -52,7 +53,16 @@ internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access try { - Init(mode, path); + FileStreamHelpers.ValidateFileTypeForNonExtendedPaths(_fileHandle, path); + + if (mode == FileMode.Append && CanSeek) + { + _appendStart = _filePosition = Length; + } + else + { + _appendStart = -1; + } } catch { @@ -70,17 +80,18 @@ internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access public sealed override bool CanWrite => !_fileHandle.IsClosed && (_access & FileAccess.Write) != 0; - // When the file is locked for writes we can cache file length in memory - // and avoid subsequent native calls which are expensive. public unsafe sealed override long Length { get { - if (_share > FileShare.Read || _exposedHandle) + if (!LengthCachingSupported) { return RandomAccess.GetFileLength(_fileHandle); } + // On Windows, when the file is locked for writes we can cache file length + // in memory and avoid subsequent native calls which are expensive. + if (_length < 0) { _length = RandomAccess.GetFileLength(_fileHandle); @@ -94,7 +105,7 @@ protected void UpdateLengthOnChangePosition() { // Do not update the cached length if the file is not locked // or if the length hasn't been fetched. - if (_share > FileShare.Read || _length < 0 || _exposedHandle) + if (!LengthCachingSupported || _length < 0) { Debug.Assert(_length < 0); return; @@ -106,8 +117,10 @@ protected void UpdateLengthOnChangePosition() } } + private bool LengthCachingSupported => OperatingSystem.IsWindows() && _share <= FileShare.Read && !_exposedHandle; + /// Gets or sets the position within the current stream - public override long Position + public sealed override long Position { get => _filePosition; set => _filePosition = value; @@ -117,8 +130,6 @@ public override long Position internal sealed override bool IsClosed => _fileHandle.IsClosed; - internal sealed override bool IsPipe => _fileHandle.IsPipe; - // Flushing is the responsibility of BufferedFileStreamStrategy internal sealed override SafeFileHandle SafeFileHandle { @@ -138,17 +149,8 @@ internal sealed override SafeFileHandle SafeFileHandle } } - public override unsafe int ReadByte() - { - byte b; - return Read(new Span(&b, 1)) != 0 ? b : -1; - } - - public override unsafe void WriteByte(byte value) => - Write(new ReadOnlySpan(&value, 1)); - // this method just disposes everything (no buffer, no need to flush) - public override ValueTask DisposeAsync() + public sealed override ValueTask DisposeAsync() { if (_fileHandle != null && !_fileHandle.IsClosed) { @@ -162,16 +164,18 @@ public override ValueTask DisposeAsync() internal sealed override void DisposeInternal(bool disposing) => Dispose(disposing); // this method just disposes everything (no buffer, no need to flush) - protected override void Dispose(bool disposing) + protected sealed override void Dispose(bool disposing) { - if (_fileHandle != null && !_fileHandle.IsClosed) + if (disposing && _fileHandle != null && !_fileHandle.IsClosed) { _fileHandle.ThreadPoolBinding?.Dispose(); _fileHandle.Dispose(); } } - public sealed override void Flush() => Flush(flushToDisk: false); // we have nothing to flush as there is no buffer here + public sealed override void Flush() { } // no buffering = nothing to flush + + public sealed override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; // no buffering = nothing to flush internal sealed override void Flush(bool flushToDisk) { @@ -203,7 +207,7 @@ public sealed override long Seek(long offset, SeekOrigin origin) else { // keep throwing the same exception we did when seek was causing actual offset change - throw Win32Marshal.GetExceptionForWin32Error(Interop.Errors.ERROR_INVALID_PARAMETER); + FileStreamHelpers.ThrowInvalidArgument(_fileHandle); } // Prevent users from overwriting data in a file that was opened in append mode. @@ -216,25 +220,10 @@ public sealed override long Seek(long offset, SeekOrigin origin) return pos; } - internal sealed override void Lock(long position, long length) => FileStreamHelpers.Lock(_fileHandle, position, length); + internal sealed override void Lock(long position, long length) => FileStreamHelpers.Lock(_fileHandle, CanWrite, position, length); internal sealed override void Unlock(long position, long length) => FileStreamHelpers.Unlock(_fileHandle, position, length); - private void Init(FileMode mode, string originalPath) - { - FileStreamHelpers.ValidateFileTypeForNonExtendedPaths(_fileHandle, originalPath); - - // For Append mode... - if (mode == FileMode.Append) - { - _appendStart = _filePosition = Length; - } - else - { - _appendStart = -1; - } - } - public sealed override void SetLength(long value) { if (_appendStart != -1 && value < _appendStart) @@ -248,7 +237,10 @@ protected unsafe void SetLengthCore(long value) Debug.Assert(value >= 0, "value >= 0"); FileStreamHelpers.SetFileLength(_fileHandle, value); - _length = value; + if (LengthCachingSupported) + { + _length = value; + } if (_filePosition > value) { @@ -256,11 +248,16 @@ protected unsafe void SetLengthCore(long value) } } - public override int Read(byte[] buffer, int offset, int count) => ReadSpan(new Span(buffer, offset, count)); + public sealed override unsafe int ReadByte() + { + byte b; + return Read(new Span(&b, 1)) != 0 ? b : -1; + } - public override int Read(Span buffer) => ReadSpan(buffer); + public sealed override int Read(byte[] buffer, int offset, int count) => + Read(new Span(buffer, offset, count)); - private unsafe int ReadSpan(Span destination) + public sealed override int Read(Span buffer) { if (_fileHandle.IsClosed) { @@ -271,19 +268,20 @@ private unsafe int ReadSpan(Span destination) ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } - int r = RandomAccess.ReadAtOffset(_fileHandle, destination, _filePosition); + int r = RandomAccess.ReadAtOffset(_fileHandle, buffer, _filePosition); Debug.Assert(r >= 0, $"RandomAccess.ReadAtOffset returned {r}."); _filePosition += r; return r; } - public override void Write(byte[] buffer, int offset, int count) - => WriteSpan(new ReadOnlySpan(buffer, offset, count)); + public sealed override unsafe void WriteByte(byte value) => + Write(new ReadOnlySpan(&value, 1)); - public override void Write(ReadOnlySpan buffer) => WriteSpan(buffer); + public override void Write(byte[] buffer, int offset, int count) => + Write(new ReadOnlySpan(buffer, offset, count)); - private unsafe void WriteSpan(ReadOnlySpan source) + public sealed override void Write(ReadOnlySpan buffer) { if (_fileHandle.IsClosed) { @@ -294,7 +292,7 @@ private unsafe void WriteSpan(ReadOnlySpan source) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } - int r = RandomAccess.WriteAtOffset(_fileHandle, source, _filePosition); + int r = RandomAccess.WriteAtOffset(_fileHandle, buffer, _filePosition); Debug.Assert(r >= 0, $"RandomAccess.WriteAtOffset returned {r}."); _filePosition += r; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs index 4091b2db65c82d..50cd8d53649e44 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs @@ -8,7 +8,7 @@ namespace System.IO.Strategies { - internal sealed class SyncWindowsFileStreamStrategy : WindowsFileStreamStrategy + internal sealed class SyncWindowsFileStreamStrategy : OSFileStreamStrategy { internal SyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access, FileShare share) : base(handle, access, share) { @@ -27,7 +27,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel // 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); + return BeginReadInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) @@ -37,7 +37,7 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken // 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)) : + new ValueTask(BeginReadInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : base.ReadAsync(buffer, cancellationToken); } @@ -47,7 +47,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati // 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); + return BeginWriteInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) @@ -57,10 +57,8 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo // 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)) : + new ValueTask(BeginWriteInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : base.WriteAsync(buffer, cancellationToken); } - - public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; // no buffering = nothing to flush } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/UnixFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/UnixFileStreamStrategy.cs new file mode 100644 index 00000000000000..c88665bf0b8d8f --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/UnixFileStreamStrategy.cs @@ -0,0 +1,178 @@ +// 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 System.Threading.Tasks.Sources; +using Microsoft.Win32.SafeHandles; + +namespace System.IO.Strategies +{ + internal sealed partial class UnixFileStreamStrategy : OSFileStreamStrategy + { + private ReadAsyncTaskSource? _readAsyncTaskSource; + + internal UnixFileStreamStrategy(SafeFileHandle handle, FileAccess access, FileShare share) : + base(handle, access, share) + { + } + + internal UnixFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) : + base(path, mode, access, share, options, preallocationSize) + { + } + + internal override bool IsAsync => _fileHandle.IsAsync; + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => + TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state); + + public override int EndRead(IAsyncResult asyncResult) => + TaskToApm.End(asyncResult); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + + public override ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken) + { + if (!CanRead) + { + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); + } + + if (CanSeek) + { + // This implementation updates the file position after the operation completes, rather than before. + // Also, unlike the Net5CompatFileStreamStrategy implementation, this implementation doesn't serialize operations. + ReadAsyncTaskSource rats = Interlocked.Exchange(ref _readAsyncTaskSource, null) ?? new ReadAsyncTaskSource(this); + return rats.QueueRead(destination, cancellationToken); + } + + return RandomAccess.ReadAtOffsetAsync(_fileHandle, destination, fileOffset: -1, cancellationToken); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => + TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state); + + public override void EndWrite(IAsyncResult asyncResult) => + TaskToApm.End(asyncResult); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + WriteAsyncCore(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + + public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken) => +#pragma warning disable CA2012 // The analyzer doesn't know the internal AsValueTask is safe. + WriteAsyncCore(source, cancellationToken).AsValueTask(); +#pragma warning restore CA2012 + + private ValueTask WriteAsyncCore(ReadOnlyMemory source, CancellationToken cancellationToken) + { + if (!CanWrite) + { + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); + } + + long filePositionBefore = -1; + if (CanSeek) + { + filePositionBefore = _filePosition; + _filePosition += source.Length; + } + + return RandomAccess.WriteAtOffsetAsync(_fileHandle, source, filePositionBefore, cancellationToken); + } + + /// Provides a reusable ValueTask-backing object for implementing ReadAsync. + private sealed class ReadAsyncTaskSource : IValueTaskSource, IThreadPoolWorkItem + { + private readonly UnixFileStreamStrategy _stream; + private ManualResetValueTaskSourceCore _source; + + private Memory _destination; + private ExecutionContext? _context; + private CancellationToken _cancellationToken; + + public ReadAsyncTaskSource(UnixFileStreamStrategy stream) => _stream = stream; + + public ValueTask QueueRead(Memory destination, CancellationToken cancellationToken) + { + _destination = destination; + _cancellationToken = cancellationToken; + _context = ExecutionContext.Capture(); + + ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: true); + return new ValueTask(this, _source.Version); + } + + void IThreadPoolWorkItem.Execute() + { + if (_context is null || _context.IsDefault) + { + Read(); + } + else + { + ExecutionContext.RunForThreadPoolUnsafe(_context, static x => x.Read(), this); + } + } + + private void Read() + { + Exception? error = null; + int result = 0; + + try + { + if (_cancellationToken.IsCancellationRequested) + { + error = new OperationCanceledException(_cancellationToken); + } + else + { + result = _stream.Read(_destination.Span); + } + } + catch (Exception e) + { + error = e; + } + finally + { + _destination = default; + _cancellationToken = default; + _context = null; + } + + if (error is not null) + { + _source.SetException(error); + } + else + { + _source.SetResult(result); + } + } + + int IValueTaskSource.GetResult(short token) + { + try + { + return _source.GetResult(token); + } + finally + { + _source.Reset(); +#pragma warning disable CS0197 + Volatile.Write(ref _stream._readAsyncTaskSource, this); +#pragma warning restore CS0197 + } + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => + _source.GetStatus(token); + + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => + _source.OnCompleted(continuation, state, token, flags); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/OperatingSystem.cs b/src/libraries/System.Private.CoreLib/src/System/OperatingSystem.cs index ff577894c606df..0b483517b97bf1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/OperatingSystem.cs +++ b/src/libraries/System.Private.CoreLib/src/System/OperatingSystem.cs @@ -197,6 +197,13 @@ public static bool IsMacOS() => false; #endif + internal static bool IsOSXLike() => +#if TARGET_OSX || TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS + true; +#else + false; +#endif + /// /// Check for the macOS version (returned by 'libobjc.get_operatingSystemVersion') with a >= version comparison. Used to guard APIs that were added in the given macOS release. /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs index 3118072c757232..1b74aa3a23674c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs @@ -571,6 +571,31 @@ public Task AsTask() /// Gets a that may be used at any point in the future. public ValueTask Preserve() => _obj == null ? this : new ValueTask(AsTask()); + /// Casts a to . + internal ValueTask AsValueTask() + { + object? obj = _obj; + Debug.Assert(obj == null || obj is Task || obj is IValueTaskSource); + + if (obj is null) + { + return default; + } + + if (obj is Task t) + { + return new ValueTask(t); + } + + if (obj is IValueTaskSource vts) + { + // This assumes the token used with IVTS is the same as used with IVTS. + return new ValueTask(vts, _token); + } + + return new ValueTask(GetTaskForValueTaskSource(Unsafe.As>(obj))); + } + /// Creates a to represent the . /// /// The is passed in rather than reading and casting