From 75ec935aa8e1897a8219e07d1de3c4163fb850c9 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Thu, 19 Aug 2021 18:04:40 +0300 Subject: [PATCH] Remove Net5CompatFileStreamStrategy. --- .../TestUtilities/System/PlatformDetection.cs | 6 - .../System.IO.FileSystem.sln | 2 - .../tests/FileStream/ReadAsync.cs | 2 +- .../tests/FileStream/SafeFileHandle.cs | 84 -- .../tests/FileStream/WriteAsync.cs | 12 - .../Net5CompatTests/Net5CompatSwitchTests.cs | 31 - ...stem.IO.FileSystem.Net5Compat.Tests.csproj | 54 - .../runtimeconfig.template.json | 5 - src/libraries/System.IO/System.IO.sln | 2 - .../System.IO.Net5Compat.Tests.csproj | 25 - .../runtimeconfig.template.json | 5 - .../System.Private.CoreLib.Shared.projitems | 6 +- .../Strategies/FileStreamHelpers.Windows.cs | 7 +- .../System/IO/Strategies/FileStreamHelpers.cs | 11 +- ...StreamStrategy.CompletionSource.Windows.cs | 250 ---- .../Net5CompatFileStreamStrategy.Unix.cs | 601 --------- .../Net5CompatFileStreamStrategy.Windows.cs | 1104 ----------------- .../Net5CompatFileStreamStrategy.cs | 536 -------- 18 files changed, 7 insertions(+), 2736 deletions(-) delete mode 100644 src/libraries/System.IO.FileSystem/tests/Net5CompatTests/Net5CompatSwitchTests.cs delete mode 100644 src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj delete mode 100644 src/libraries/System.IO.FileSystem/tests/Net5CompatTests/runtimeconfig.template.json delete mode 100644 src/libraries/System.IO/tests/Net5CompatTests/System.IO.Net5Compat.Tests.csproj delete mode 100644 src/libraries/System.IO/tests/Net5CompatTests/runtimeconfig.template.json delete mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.CompletionSource.Windows.cs delete mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs delete mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Windows.cs delete mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index efef067dbce645..bd164ee9e1b428 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -290,12 +290,6 @@ private static Version GetICUVersion() version & 0xFF); } - private static readonly Lazy _net5CompatFileStream = new Lazy(() => GetStaticNonPublicBooleanPropertyValue("System.IO.Strategies.FileStreamHelpers", "UseNet5CompatStrategy")); - - public static bool IsNet5CompatFileStreamEnabled => _net5CompatFileStream.Value; - - public static bool IsNet5CompatFileStreamDisabled => !IsNet5CompatFileStreamEnabled; - private static readonly Lazy s_fileLockingDisabled = new Lazy(() => GetStaticNonPublicBooleanPropertyValue("Microsoft.Win32.SafeHandles.SafeFileHandle", "DisableFileLocking")); public static bool IsFileLockingEnabled => IsWindows || !s_fileLockingDisabled.Value; diff --git a/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln b/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln index f5ec7d34a57d02..aeb661f5b8d95b 100644 --- a/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln +++ b/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln @@ -21,8 +21,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.FileSystem.Disabl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.FileSystem.Manual.Tests", "tests\ManualTests\System.IO.FileSystem.Manual.Tests.csproj", "{534152EB-14A8-4EF4-B181-342A555337F1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.FileSystem.Net5Compat.Tests", "tests\Net5CompatTests\System.IO.FileSystem.Net5Compat.Tests.csproj", "{48E07F12-8597-40DE-8A37-CCBEB9D54012}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.FileSystem.Tests", "tests\System.IO.FileSystem.Tests.csproj", "{3A8E16D3-8A22-4076-BB48-2CD1FBFAF81B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\ref\System.Runtime.CompilerServices.Unsafe.csproj", "{79A74577-C550-4264-B352-51D304796B89}" diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs index 4e2fb6fd2046e7..af30be04c8673c 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs @@ -101,7 +101,7 @@ public async Task ReadAsyncCanceledFile(int bufferSize, bool isAsync) } } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported), nameof(PlatformDetection.IsNet5CompatFileStreamDisabled))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] [InlineData(FileShare.None, FileOptions.Asynchronous)] // FileShare.None: exclusive access [InlineData(FileShare.ReadWrite, FileOptions.Asynchronous)] // FileShare.ReadWrite: others can write to the file, the length can't be cached [InlineData(FileShare.None, FileOptions.None)] diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs index 0740eed02f0d85..49b31884761ab7 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs @@ -65,89 +65,5 @@ public void AccessFlushesFileClosesHandle() Assert.Equal(TestBuffer.Length, fsr.Length); } } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNet5CompatFileStreamEnabled))] - public async Task ThrowWhenHandlePositionIsChanged_sync() - { - await ThrowWhenHandlePositionIsChanged(useAsync: false); - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported), nameof(PlatformDetection.IsNet5CompatFileStreamEnabled))] - public async Task ThrowWhenHandlePositionIsChanged_async() - { - await ThrowWhenHandlePositionIsChanged(useAsync: true); - } - - private async Task ThrowWhenHandlePositionIsChanged(bool useAsync) - { - string fileName = GetTestFilePath(); - - using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite, 0x100, useAsync)) - { - // write some data to move the position, flush to ensure OS position is updated - fs.Write(TestBuffer, 0, TestBuffer.Length); - fs.Flush(); - - if (fs.SafeFileHandle.IsInvalid) - { - // nothing to test - return; - } - - using (FileStream fsr = new FileStream(fs.SafeFileHandle, FileAccess.Read, TestBuffer.Length, useAsync)) - { - Assert.Equal(TestBuffer.Length, fs.Position); - Assert.Equal(TestBuffer.Length, fsr.Position); - - // Operations on original filestream will fail if data is in buffer and position changes. - - // Put data in FS write buffer and update position from FSR - fs.WriteByte(0); - fsr.Position = 0; - - 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.IsNet5CompatFileStreamEnabled) - { - 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++; - Assert.Throws(() => fs.Read(new byte[1], 0, 1)); - - fs.WriteByte(0); - fsr.Position++; - await Assert.ThrowsAsync(() => fs.ReadAsync(new byte[1], 0, 1)); - - fs.WriteByte(0); - fsr.Position++; - Assert.Throws(() => fs.ReadByte()); - - fs.WriteByte(0); - fsr.Position++; - Assert.Throws(() => fs.Seek(0, SeekOrigin.End)); - - fs.WriteByte(0); - fsr.Position++; - Assert.Throws(() => fs.SetLength(2)); - - fs.WriteByte(0); - fsr.Position++; - Assert.Throws(() => fs.Flush()); - - fs.WriteByte(0); - fsr.Position++; - Assert.Throws(() => fs.Dispose()); - } - } - } } } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs index 06a9422589ee84..86eef052df1b40 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs @@ -229,18 +229,6 @@ public async Task ManyConcurrentWriteAsyncs_OuterLoop( { writes[i] = WriteAsync(fs, expectedData, i * writeSize, writeSize, cancellationToken); Assert.Null(writes[i].Exception); - if (useAsync) - { - // 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.IsNet5CompatFileStreamEnabled) - { - Assert.Equal((i + 1) * writeSize, fs.Position); - } - } } await Task.WhenAll(writes); diff --git a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/Net5CompatSwitchTests.cs b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/Net5CompatSwitchTests.cs deleted file mode 100644 index fb70c4d3c47d01..00000000000000 --- a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/Net5CompatSwitchTests.cs +++ /dev/null @@ -1,31 +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.Reflection; -using Xunit; - -namespace System.IO.Tests -{ - public class Net5CompatSwitchTests - { - [Fact] - public static void LegacySwitchIsHonored() - { - Assert.True(PlatformDetection.IsNet5CompatFileStreamEnabled); - - string filePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); - - using (FileStream fileStream = File.Create(filePath)) - { - object strategy = fileStream - .GetType() - .GetField("_strategy", BindingFlags.NonPublic | BindingFlags.Instance) - .GetValue(fileStream); - - Assert.Contains("Net5Compat", strategy.GetType().FullName); - } - - File.Delete(filePath); - } - } -} 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 deleted file mode 100644 index 745ff60368b2ef..00000000000000 --- a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj +++ /dev/null @@ -1,54 +0,0 @@ - - - true - true - $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix - - --working-dir=/test-dir - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/runtimeconfig.template.json b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/runtimeconfig.template.json deleted file mode 100644 index 6e843517fe47af..00000000000000 --- a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/runtimeconfig.template.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "configProperties": { - "System.IO.UseNet5CompatFileStream": true - } -} diff --git a/src/libraries/System.IO/System.IO.sln b/src/libraries/System.IO/System.IO.sln index 46f1a75add73c4..a8da427cc73a65 100644 --- a/src/libraries/System.IO/System.IO.sln +++ b/src/libraries/System.IO/System.IO.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO", "ref\System.IO. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO", "src\System.IO.csproj", "{0769544B-1A5D-4D74-94FD-899DF6C39D62}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.Net5Compat.Tests", "tests\Net5CompatTests\System.IO.Net5Compat.Tests.csproj", "{0217540D-FA86-41B3-9754-7BB5096ABA3E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.Tests", "tests\System.IO.Tests.csproj", "{72923407-7B7B-44A8-BCA6-2DB562835A8F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\ref\System.Runtime.CompilerServices.Unsafe.csproj", "{602525FF-EE47-4938-8979-32DC962109E4}" diff --git a/src/libraries/System.IO/tests/Net5CompatTests/System.IO.Net5Compat.Tests.csproj b/src/libraries/System.IO/tests/Net5CompatTests/System.IO.Net5Compat.Tests.csproj deleted file mode 100644 index 321e2f485088b8..00000000000000 --- a/src/libraries/System.IO/tests/Net5CompatTests/System.IO.Net5Compat.Tests.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - System.IO - true - true - true - - $(NetCoreAppCurrent)-windows - - - - - - - - - - - - - - - - - diff --git a/src/libraries/System.IO/tests/Net5CompatTests/runtimeconfig.template.json b/src/libraries/System.IO/tests/Net5CompatTests/runtimeconfig.template.json deleted file mode 100644 index 6e843517fe47af..00000000000000 --- a/src/libraries/System.IO/tests/Net5CompatTests/runtimeconfig.template.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "configProperties": { - "System.IO.UseNet5CompatFileStream": true - } -} 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 5a1593f032c69a..a8e8042e7e1664 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 @@ -461,7 +461,6 @@ - @@ -1833,8 +1832,6 @@ - - @@ -2118,7 +2115,6 @@ - @@ -2325,4 +2321,4 @@ - \ No newline at end of file + 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 779acce9373717..8722a06a8e612f 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 @@ -14,9 +14,8 @@ namespace System.IO.Strategies // this type defines a set of stateless FileStream/FileStreamStrategy helper methods internal static partial class FileStreamHelpers { - // Async completion/return codes shared by: - // - AsyncWindowsFileStreamStrategy.ValueTaskSource - // - Net5CompatFileStreamStrategy.CompletionSource + // Async completion/return codes used by + // SafeFileHandle.OverlappedValueTaskSource internal static class TaskSourceCodes { internal const long NoResult = 0; @@ -327,7 +326,7 @@ internal static async Task AsyncModeCopyToAsync(SafeFileHandle handle, bool canS } } - /// Used by AsyncWindowsFileStreamStrategy and Net5CompatFileStreamStrategy CopyToAsync to enable awaiting the result of an overlapped I/O operation with minimal overhead. + /// Used by AsyncWindowsFileStreamStrategy.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. 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 d86c70a621ca1a..764a54504df258 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 @@ -11,15 +11,9 @@ internal static partial class FileStreamHelpers /// Caches whether Serialization Guard has been disabled for file writes private static int s_cachedSerializationSwitch; - internal static bool UseNet5CompatStrategy { get; } = AppContextConfigHelper.GetBooleanConfig("System.IO.UseNet5CompatFileStream", "DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM"); - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, int bufferSize, bool 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) : + FileStreamStrategy strategy = EnableBufferingIfNeeded(ChooseStrategyCore(handle, access, isAsync), bufferSize); return WrapIfDerivedType(fileStream, strategy); @@ -27,8 +21,7 @@ internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFil internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { - FileStreamStrategy strategy = UseNet5CompatStrategy ? - new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize == 0 ? 1 : bufferSize, options, preallocationSize) : + FileStreamStrategy strategy = EnableBufferingIfNeeded(ChooseStrategyCore(path, mode, access, share, options, preallocationSize), bufferSize); return WrapIfDerivedType(fileStream, strategy); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.CompletionSource.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.CompletionSource.Windows.cs deleted file mode 100644 index 2c8a13feace0c3..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.CompletionSource.Windows.cs +++ /dev/null @@ -1,250 +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; -using TaskSourceCodes = System.IO.Strategies.FileStreamHelpers.TaskSourceCodes; - -namespace System.IO.Strategies -{ - internal sealed partial class Net5CompatFileStreamStrategy : 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 CompletionSource : TaskCompletionSource - { - internal static readonly unsafe IOCompletionCallback s_ioCallback = IOCallback; - - private static Action? s_cancelCallback; - - private readonly Net5CompatFileStreamStrategy _strategy; - 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) - internal CompletionSource(Net5CompatFileStreamStrategy strategy, PreAllocatedOverlapped? preallocatedOverlapped, - int numBufferedBytes, byte[]? bytes) : base(TaskCreationOptions.RunContinuationsAsynchronously) - { - _numBufferedBytes = numBufferedBytes; - _strategy = strategy; - _result = TaskSourceCodes.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, TaskSourceCodes.RegisteringCancellation, TaskSourceCodes.NoResult); - if (packedResult == TaskSourceCodes.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, TaskSourceCodes.NoResult); - } - else if (packedResult != TaskSourceCodes.CompletedCallback) - { - // Failed to set the result, IO is in the process of completing - // Attempt to take the packed result - packedResult = Interlocked.Exchange(ref _result, TaskSourceCodes.NoResult); - } - - // If we have a callback that needs to be completed - if ((packedResult != TaskSourceCodes.NoResult) && (packedResult != TaskSourceCodes.CompletedCallback) && (packedResult != TaskSourceCodes.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 Net5CompatFileStreamStrategy || state is CompletionSource); - CompletionSource completionSource = state switch - { - Net5CompatFileStreamStrategy strategy => strategy._currentOverlappedOwner!, // must be owned - _ => (CompletionSource)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 != Interop.Errors.ERROR_BROKEN_PIPE && errorCode != Interop.Errors.ERROR_NO_DATA) - { - packedResult = ((ulong)TaskSourceCodes.ResultError | errorCode); - } - else - { - packedResult = ((ulong)TaskSourceCodes.ResultSuccess | numBytes); - } - - // Stow the result so that other threads can observe it - // And, if no other thread is registering cancellation, continue - if (TaskSourceCodes.NoResult == Interlocked.Exchange(ref completionSource._result, (long)packedResult)) - { - // Successfully set the state, attempt to take back the callback - if (Interlocked.Exchange(ref completionSource._result, TaskSourceCodes.CompletedCallback) != TaskSourceCodes.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 & TaskSourceCodes.ResultMask); - if (result == TaskSourceCodes.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 == TaskSourceCodes.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 CompletionSource, "Unknown state passed to cancellation"); - CompletionSource completionSource = (CompletionSource)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.GetLastPInvokeError(); - - // 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 CompletionSource Create(Net5CompatFileStreamStrategy 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.IsUserObject(buffer.Array) // preallocatedOverlapped is allocated when BufferedStream|Net5CompatFileStreamStrategy allocates the buffer - ? new CompletionSource(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[]. - /// - private sealed class MemoryFileStreamCompletionSource : CompletionSource - { - private MemoryHandle _handle; // mutable struct; do not make this readonly - - internal MemoryFileStreamCompletionSource(Net5CompatFileStreamStrategy 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/Strategies/Net5CompatFileStreamStrategy.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs deleted file mode 100644 index 2feffb5cb6be54..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs +++ /dev/null @@ -1,601 +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 Microsoft.Win32.SafeHandles; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Threading; -using System.Threading.Tasks; - -namespace System.IO.Strategies -{ - /// Provides an implementation of a file stream for Unix files. - internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy - { - /// Advanced options requested when opening the file. - private FileOptions _options; - - /// If the file was opened with FileMode.Append, the length of the file when opened; otherwise, -1. - private long _appendStart = -1; - - /// - /// Extra state used by the file stream when _useAsyncIO is true. This includes - /// the semaphore used to serialize all operation, the buffer/offset/count provided by the - /// caller for ReadAsync/WriteAsync operations, and the last successful task returned - /// synchronously from ReadAsync which can be reused if the count matches the next request. - /// Only initialized when is true. - /// - private AsyncState? _asyncState; - - private void Init(FileMode mode, string originalPath, FileOptions options) - { - // FileStream performs most of the general argument validation. We can assume here that the arguments - // are all checked and consistent (e.g. non-null-or-empty path; valid enums in mode, access, share, and options; etc.) - // Store the arguments - _options = options; - - if (_useAsyncIO) - { - _asyncState = new AsyncState(); - } - - if (mode == FileMode.Append) - { - // Jump to the end of the file if opened as Append. - _appendStart = SeekCore(_fileHandle, 0, SeekOrigin.End); - } - - Debug.Assert(_fileHandle.IsAsync == _useAsyncIO); - } - - /// Initializes a stream from an already open file handle (file descriptor). - private void InitFromHandle(SafeFileHandle handle, FileAccess access, bool useAsyncIO) - { - if (useAsyncIO) - _asyncState = new AsyncState(); - - if (handle.CanSeek) - SeekCore(handle, 0, SeekOrigin.Current); - } - - public override bool CanSeek => _fileHandle.CanSeek; - - public override long Length - { - get - { - // Get the length of the file as reported by the OS - long length = RandomAccess.GetFileLength(_fileHandle); - - // But we may have buffered some data to be written that puts our length - // beyond what the OS is aware of. Update accordingly. - if (_writePos > 0 && _filePosition + _writePos > length) - { - length = _writePos + _filePosition; - } - - return 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) - { - try - { - if (_fileHandle != null && !_fileHandle.IsClosed) - { - // Flush any remaining data in the file - try - { - FlushWriteBuffer(); - } - catch (Exception e) when (!disposing && FileStreamHelpers.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. - } - - // Closing the file handle can fail, e.g. due to out of disk space - // Throw these errors as exceptions when disposing - if (_fileHandle != null && !_fileHandle.IsClosed && disposing) - { - SafeFileHandle.t_lastCloseErrorInfo = null; - - _fileHandle.Dispose(); - - if (SafeFileHandle.t_lastCloseErrorInfo != null) - { - throw Interop.GetExceptionForIoErrno(SafeFileHandle.t_lastCloseErrorInfo.GetValueOrDefault(), _fileHandle.Path, isDirectory: false); - } - } - } - } - finally - { - if (_fileHandle != null && !_fileHandle.IsClosed) - { - _fileHandle.Dispose(); - } - base.Dispose(disposing); - } - } - - public override ValueTask DisposeAsync() - { - // On Unix, we don't have any special support for async I/O, simply queueing writes - // rather than doing them synchronously. As such, if we're "using async I/O" and we - // have something to flush, queue the call to Dispose, so that we end up queueing whatever - // write work happens to flush the buffer. Otherwise, just delegate to the base implementation, - // which will synchronously invoke Dispose. We don't need to factor in the current type - // as we're using the virtual Dispose either way, and therefore factoring in whatever - // override may already exist on a derived type. - if (_useAsyncIO && _writePos > 0) - { - return new ValueTask(Task.Factory.StartNew(static s => ((Net5CompatFileStreamStrategy)s!).Dispose(), this, - CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default)); - } - - return base.DisposeAsync(); - } - - private void FlushWriteBufferForWriteByte() - { -#pragma warning disable CA1416 // Validate platform compatibility, issue: https://github.com/dotnet/runtime/issues/44542 - _asyncState?.Wait(); -#pragma warning restore CA1416 - try { FlushWriteBuffer(); } - finally { _asyncState?.Release(); } - } - - /// Writes any data in the write buffer to the underlying stream and resets the buffer. - private void FlushWriteBuffer(bool calledFromFinalizer = false) - { - AssertBufferInvariants(); - if (_writePos > 0) - { - WriteNative(new ReadOnlySpan(GetBuffer(), 0, _writePos)); - _writePos = 0; - } - } - - /// Sets the length of this stream to the given value. - /// The new length of the stream. - public override void SetLength(long value) - { - FlushInternalBuffer(); - - if (_appendStart != -1 && value < _appendStart) - { - throw new IOException(SR.IO_SetLengthAppendTruncate); - } - - VerifyOSHandlePosition(); - - CheckFileCall(Interop.Sys.FTruncate(_fileHandle, value)); - - // Set file pointer to end of file - if (_filePosition > value) - { - SeekCore(_fileHandle, 0, SeekOrigin.End); - } - } - - /// Reads a block of bytes from the stream and writes the data in a given buffer. - private int ReadSpan(Span destination) - { - PrepareForReading(); - - // Are there any bytes available in the read buffer? If yes, - // we can just return from the buffer. If the buffer is empty - // or has no more available data in it, we can either refill it - // (and then read from the buffer into the user's buffer) or - // we can just go directly into the user's buffer, if they asked - // for more data than we'd otherwise buffer. - int numBytesAvailable = _readLength - _readPos; - bool readFromOS = false; - if (numBytesAvailable == 0) - { - // If we're not able to seek, then we're not able to rewind the stream (i.e. flushing - // a read buffer), in which case we don't want to use a read buffer. Similarly, if - // the user has asked for more data than we can buffer, we also want to skip the buffer. - if (!CanSeek || (destination.Length >= _bufferLength)) - { - // Read directly into the user's buffer - _readPos = _readLength = 0; - return ReadNative(destination); - } - else - { - // Read into our buffer. - _readLength = numBytesAvailable = ReadNative(GetBuffer()); - _readPos = 0; - if (numBytesAvailable == 0) - { - return 0; - } - - // Note that we did an OS read as part of this Read, so that later - // we don't try to do one again if what's in the buffer doesn't - // meet the user's request. - readFromOS = true; - } - } - - // Now that we know there's data in the buffer, read from it into the user's buffer. - Debug.Assert(numBytesAvailable > 0, "Data must be in the buffer to be here"); - int bytesRead = Math.Min(numBytesAvailable, destination.Length); - new Span(GetBuffer(), _readPos, bytesRead).CopyTo(destination); - _readPos += bytesRead; - - // We may not have had enough data in the buffer to completely satisfy the user's request. - // While Read doesn't require that we return as much data as the user requested (any amount - // up to the requested count is fine), FileStream on Windows tries to do so by doing a - // subsequent read from the file if we tried to satisfy the request with what was in the - // buffer but the buffer contained less than the requested count. To be consistent with that - // behavior, we do the same thing here on Unix. Note that we may still get less the requested - // amount, as the OS may give us back fewer than we request, either due to reaching the end of - // file, or due to its own whims. - if (!readFromOS && bytesRead < destination.Length) - { - Debug.Assert(_readPos == _readLength, "bytesToRead should only be < destination.Length if numBytesAvailable < destination.Length"); - _readPos = _readLength = 0; // no data left in the read buffer - bytesRead += ReadNative(destination.Slice(bytesRead)); - } - - return bytesRead; - } - - /// Unbuffered, reads a block of bytes from the file handle into the given buffer. - /// The buffer into which data from the file is read. - /// - /// The total number of bytes read into the buffer. This might be less than the number of bytes requested - /// if that number of bytes are not currently available, or zero if the end of the stream is reached. - /// - private unsafe int ReadNative(Span buffer) - { - FlushWriteBuffer(); // we're about to read; dump the write buffer - - VerifyOSHandlePosition(); - - int bytesRead; - fixed (byte* bufPtr = &MemoryMarshal.GetReference(buffer)) - { - bytesRead = CheckFileCall(Interop.Sys.Read(_fileHandle, bufPtr, buffer.Length)); - Debug.Assert(bytesRead <= buffer.Length); - } - _filePosition += bytesRead; - return bytesRead; - } - - /// - /// Asynchronously reads a sequence of bytes from the current stream and advances - /// the position within the stream by the number of bytes read. - /// - /// The buffer to write the data into. - /// The token to monitor for cancellation requests. - /// If the operation completes synchronously, the number of bytes read. - /// A task that represents the asynchronous read operation. - private Task? ReadAsyncInternal(Memory destination, CancellationToken cancellationToken, out int synchronousResult) - { - Debug.Assert(_useAsyncIO); - Debug.Assert(_asyncState != null); - - if (!CanRead) // match Windows behavior; this gets thrown synchronously - { - ThrowHelper.ThrowNotSupportedException_UnreadableStream(); - } - - // Serialize operations using the semaphore. - Task waitTask = _asyncState.WaitAsync(); - - // If we got ownership immediately, and if there's enough data in our buffer - // to satisfy the full request of the caller, hand back the buffered data. - // While it would be a legal implementation of the Read contract, we don't - // hand back here less than the amount requested so as to match the behavior - // in ReadCore that will make a native call to try to fulfill the remainder - // of the request. - if (waitTask.Status == TaskStatus.RanToCompletion) - { - int numBytesAvailable = _readLength - _readPos; - if (numBytesAvailable >= destination.Length) - { - try - { - PrepareForReading(); - - new Span(GetBuffer(), _readPos, destination.Length).CopyTo(destination.Span); - _readPos += destination.Length; - - synchronousResult = destination.Length; - return null; - } - catch (Exception exc) - { - synchronousResult = 0; - return Task.FromException(exc); - } - finally - { - _asyncState.Release(); - } - } - } - - // Otherwise, issue the whole request asynchronously. - synchronousResult = 0; - _asyncState.Memory = destination; - return waitTask.ContinueWith(static (t, s) => - { - // The options available on Unix for writing asynchronously to an arbitrary file - // handle typically amount to just using another thread to do the synchronous write, - // which is exactly what this implementation does. This does mean there are subtle - // differences in certain FileStream behaviors between Windows and Unix when multiple - // asynchronous operations are issued against the stream to execute concurrently; on - // Unix the operations will be serialized due to the usage of a semaphore, but the - // position /length information won't be updated until after the write has completed, - // whereas on Windows it may happen before the write has completed. - - Debug.Assert(t.Status == TaskStatus.RanToCompletion); - var thisRef = (Net5CompatFileStreamStrategy)s!; - Debug.Assert(thisRef._asyncState != null); - try - { - Memory memory = thisRef._asyncState.Memory; - thisRef._asyncState.Memory = default; - return thisRef.ReadSpan(memory.Span); - } - finally { thisRef._asyncState.Release(); } - }, this, CancellationToken.None, TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default); - } - - /// Reads from the file handle into the buffer, overwriting anything in it. - private int FillReadBufferForReadByte() - { -#pragma warning disable CA1416 // Validate platform compatibility, issue: https://github.com/dotnet/runtime/issues/44542 - _asyncState?.Wait(); -#pragma warning restore CA1416 - try { return ReadNative(_buffer); } - finally { _asyncState?.Release(); } - } - - /// Writes a block of bytes to the file stream. - /// The buffer containing data to write to the stream. - private void WriteSpan(ReadOnlySpan source) - { - PrepareForWriting(); - - // If no data is being written, nothing more to do. - if (source.Length == 0) - { - return; - } - - // If there's already data in our write buffer, then we need to go through - // our buffer to ensure data isn't corrupted. - if (_writePos > 0) - { - // If there's space remaining in the buffer, then copy as much as - // we can from the user's buffer into ours. - int spaceRemaining = _bufferLength - _writePos; - if (spaceRemaining >= source.Length) - { - source.CopyTo(GetBuffer().AsSpan(_writePos)); - _writePos += source.Length; - return; - } - else if (spaceRemaining > 0) - { - source.Slice(0, spaceRemaining).CopyTo(GetBuffer().AsSpan(_writePos)); - _writePos += spaceRemaining; - source = source.Slice(spaceRemaining); - } - - // At this point, the buffer is full, so flush it out. - FlushWriteBuffer(); - } - - // Our buffer is now empty. If using the buffer would slow things down (because - // the user's looking to write more data than we can store in the buffer), - // skip the buffer. Otherwise, put the remaining data into the buffer. - Debug.Assert(_writePos == 0); - if (source.Length >= _bufferLength) - { - WriteNative(source); - } - else - { - source.CopyTo(new Span(GetBuffer())); - _writePos = source.Length; - } - } - - /// Unbuffered, writes a block of bytes to the file stream. - /// The buffer containing data to write to the stream. - private unsafe void WriteNative(ReadOnlySpan source) - { - VerifyOSHandlePosition(); - - fixed (byte* bufPtr = &MemoryMarshal.GetReference(source)) - { - int offset = 0; - int count = source.Length; - while (count > 0) - { - int bytesWritten = CheckFileCall(Interop.Sys.Write(_fileHandle, bufPtr + offset, count)); - _filePosition += bytesWritten; - offset += bytesWritten; - count -= bytesWritten; - } - } - } - - /// - /// Asynchronously writes a sequence of bytes to the current stream, advances - /// the current position within this stream by the number of bytes written, and - /// monitors cancellation requests. - /// - /// The buffer to write data from. - /// The token to monitor for cancellation requests. - /// A task that represents the asynchronous write operation. - private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationToken cancellationToken) - { - Debug.Assert(_useAsyncIO); - Debug.Assert(_asyncState != null); - - if (cancellationToken.IsCancellationRequested) - return ValueTask.FromCanceled(cancellationToken); - - if (_fileHandle.IsClosed) - ThrowHelper.ThrowObjectDisposedException_FileClosed(); - - if (!CanWrite) // match Windows behavior; this gets thrown synchronously - { - ThrowHelper.ThrowNotSupportedException_UnwritableStream(); - } - - // Serialize operations using the semaphore. - Task waitTask = _asyncState.WaitAsync(cancellationToken); - - // If we got ownership immediately, and if there's enough space in our buffer - // to buffer the entire write request, then do so and we're done. - if (waitTask.Status == TaskStatus.RanToCompletion) - { - int spaceRemaining = _bufferLength - _writePos; - if (spaceRemaining >= source.Length) - { - try - { - PrepareForWriting(); - - source.Span.CopyTo(new Span(GetBuffer(), _writePos, source.Length)); - _writePos += source.Length; - - return default; - } - catch (Exception exc) - { - return ValueTask.FromException(exc); - } - finally - { - _asyncState.Release(); - } - } - } - - // Otherwise, issue the whole request asynchronously. - _asyncState.ReadOnlyMemory = source; - return new ValueTask(waitTask.ContinueWith(static (t, s) => - { - // The options available on Unix for writing asynchronously to an arbitrary file - // handle typically amount to just using another thread to do the synchronous write, - // which is exactly what this implementation does. This does mean there are subtle - // differences in certain FileStream behaviors between Windows and Unix when multiple - // asynchronous operations are issued against the stream to execute concurrently; on - // Unix the operations will be serialized due to the usage of a semaphore, but the - // position/length information won't be updated until after the write has completed, - // whereas on Windows it may happen before the write has completed. - - Debug.Assert(t.Status == TaskStatus.RanToCompletion); - var thisRef = (Net5CompatFileStreamStrategy)s!; - Debug.Assert(thisRef._asyncState != null); - try - { - ReadOnlyMemory readOnlyMemory = thisRef._asyncState.ReadOnlyMemory; - thisRef._asyncState.ReadOnlyMemory = default; - thisRef.WriteSpan(readOnlyMemory.Span); - } - finally { thisRef._asyncState.Release(); } - }, this, CancellationToken.None, TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default)); - } - - /// Sets the current position of this stream to the given value. - /// 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. - /// - /// The new position in the stream. - 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) - { - ThrowHelper.ThrowObjectDisposedException_FileClosed(); - } - if (!CanSeek) - { - ThrowHelper.ThrowNotSupportedException_UnseekableStream(); - } - - VerifyOSHandlePosition(); - - // Flush our write/read buffer. FlushWrite will output any write buffer we have and reset _bufferWritePos. - // We don't call FlushRead, as that will do an unnecessary seek to rewind the read buffer, and since we're - // about to seek and update our position, we can simply update the offset as necessary and reset our read - // position and length to 0. (In the future, for some simple cases we could potentially add an optimization - // here to just move data around in the buffer for short jumps, to avoid re-reading the data from disk.) - FlushWriteBuffer(); - if (origin == SeekOrigin.Current) - { - offset -= (_readLength - _readPos); - } - _readPos = _readLength = 0; - - // Keep track of where we were, in case we're in append mode and need to verify - long oldPos = 0; - if (_appendStart >= 0) - { - oldPos = SeekCore(_fileHandle, 0, SeekOrigin.Current); - } - - // Jump to the new location - 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 the new position - return pos; - } - - private int CheckFileCall(int result, bool ignoreNotSupported = false) - { - FileStreamHelpers.CheckFileCall(result, _fileHandle?.Path, ignoreNotSupported); - - return result; - } - - /// State used when the stream is in async mode. - private sealed class AsyncState : SemaphoreSlim - { - internal ReadOnlyMemory ReadOnlyMemory; - internal Memory Memory; - - /// Initialize the AsyncState. - internal AsyncState() : base(initialCount: 1, maxCount: 1) { } - } - } -} 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 deleted file mode 100644 index a2e7666114524e..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Windows.cs +++ /dev/null @@ -1,1104 +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.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Win32.SafeHandles; - -/* - * Win32FileStream supports different modes of accessing the disk - async mode - * and sync mode. They are two completely different codepaths in the - * sync & async methods (i.e. Read/Write vs. ReadAsync/WriteAsync). File - * handles in NT can be opened in only sync or overlapped (async) mode, - * and we have to deal with this pain. Stream has implementations of - * the sync methods in terms of the async ones, so we'll - * call through to our base class to get those methods when necessary. - * - * Also buffering is added into Win32FileStream as well. Folded in the - * code from BufferedStream, so all the comments about it being mostly - * aggressive (and the possible perf improvement) apply to Win32FileStream as - * well. Also added some buffering to the async code paths. - * - * Class Invariants: - * The class has one buffer, shared for reading & writing. It can only be - * used for one or the other at any point in time - not both. The following - * should be true: - * 0 <= _readPos <= _readLen < _bufferSize - * 0 <= _writePos < _bufferSize - * _readPos == _readLen && _readPos > 0 implies the read buffer is valid, - * but we're at the end of the buffer. - * _readPos == _readLen == 0 means the read buffer contains garbage. - * Either _writePos can be greater than 0, or _readLen & _readPos can be - * greater than zero, but neither can be greater than zero at the same time. - * - */ - -namespace System.IO.Strategies -{ - internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy - { - private long _appendStart; // When appending, prevent overwriting file. - - 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 CompletionSource? _currentOverlappedOwner; // async op currently using the preallocated overlapped - - private void Init(FileMode mode, string originalPath, FileOptions options) - { - Debug.Assert(!_useAsyncIO || _fileHandle.ThreadPoolBinding != null); - - // For Append mode... - if (mode == FileMode.Append) - { - _appendStart = SeekCore(_fileHandle, 0, SeekOrigin.End); - } - else - { - _appendStart = -1; - } - } - - private void InitFromHandle(SafeFileHandle handle, FileAccess access, bool useAsyncIO) - { -#if DEBUG - bool hadBinding = handle.ThreadPoolBinding != null; - - try - { -#endif - InitFromHandleImpl(handle, useAsyncIO); -#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, bool useAsyncIO) - { - handle.EnsureThreadPoolBindingInitialized(); - - if (handle.CanSeek) - SeekCore(handle, 0, SeekOrigin.Current); - else - _filePosition = 0; - } - - private bool HasActiveBufferOperation => !_activeBufferOperation.IsCompleted; - - public override bool CanSeek => _fileHandle.CanSeek; - - public unsafe override long Length - { - get - { - long len = RandomAccess.GetFileLength(_fileHandle); - - // 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 && _filePosition + _writePos > len) - len = _writePos + _filePosition; - - return len; - } - } - - protected override void Dispose(bool disposing) - { - // Nothing will be done differently based on whether we are - // disposing vs. finalizing. This is taking advantage of the - // weak ordering between normal finalizable objects & critical - // finalizable objects, which I included in the SafeHandle - // design for Win32FileStream, which would often "just work" when - // finalized. - try - { - if (_fileHandle != null && !_fileHandle.IsClosed && _writePos > 0) - { - // Flush data to disk iff we were writing. After - // thinking about this, we also don't need to flush - // our read position, regardless of whether the handle - // was exposed to the user. They probably would NOT - // want us to do this. - try - { - FlushWriteBuffer(!disposing); - } - catch (Exception e) when (!disposing && FileStreamHelpers.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. - } - } - } - finally - { - if (_fileHandle != null && !_fileHandle.IsClosed) - { - _fileHandle.ThreadPoolBinding?.Dispose(); - _fileHandle.Dispose(); - } - - _preallocatedOverlapped?.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 override async ValueTask DisposeAsync() - { - // Same logic as in Dispose(), except with async counterparts. - // TODO: https://github.com/dotnet/runtime/issues/27643: FlushAsync does synchronous work. - try - { - if (_fileHandle != null && !_fileHandle.IsClosed && _writePos > 0) - { - await FlushAsync(default).ConfigureAwait(false); - } - } - finally - { - if (_fileHandle != null && !_fileHandle.IsClosed) - { - _fileHandle.ThreadPoolBinding?.Dispose(); - _fileHandle.Dispose(); - } - - _preallocatedOverlapped?.Dispose(); - GC.SuppressFinalize(this); // the handle is closed; nothing further for the finalizer to do - } - } - - // Returns a task that flushes the internal write buffer - private Task FlushWriteAsync(CancellationToken cancellationToken) - { - Debug.Assert(_useAsyncIO); - Debug.Assert(_readPos == 0 && _readLength == 0, "FileStream: Read buffer must be empty in FlushWriteAsync!"); - - // If the buffer is already flushed, don't spin up the OS write - if (_writePos == 0) return Task.CompletedTask; - - Task flushTask = WriteAsyncInternalCore(new ReadOnlyMemory(GetBuffer(), 0, _writePos), cancellationToken); - _writePos = 0; - - // Update the active buffer operation - _activeBufferOperation = HasActiveBufferOperation ? - Task.WhenAll(_activeBufferOperation, flushTask) : - flushTask; - - return flushTask; - } - - private void FlushWriteBufferForWriteByte() => FlushWriteBuffer(); - - // Writes are buffered. Anytime the buffer fills up - // (_writePos + delta > _bufferSize) or the buffer switches to reading - // and there is left over data (_writePos > 0), this function must be called. - private void FlushWriteBuffer(bool calledFromFinalizer = false) - { - if (_writePos == 0) return; - Debug.Assert(_readPos == 0 && _readLength == 0, "FileStream: Read buffer must be empty in FlushWrite!"); - - if (_useAsyncIO) - { - Task writeTask = FlushWriteAsync(CancellationToken.None); - // With our Whidbey async IO & overlapped support for AD unloads, - // we don't strictly need to block here to release resources - // since that support takes care of the pinning & freeing the - // overlapped struct. We need to do this when called from - // Close so that the handle is closed when Close returns, but - // we don't need to call EndWrite from the finalizer. - // Additionally, if we do call EndWrite, we block forever - // because AD unloads prevent us from running the managed - // callback from the IO completion port. Blocking here when - // called from the finalizer during AD unload is clearly wrong, - // but we can't use any sort of test for whether the AD is - // unloading because if we weren't unloading, an AD unload - // could happen on a separate thread before we call EndWrite. - if (!calledFromFinalizer) - { - writeTask.GetAwaiter().GetResult(); - } - } - else - { - WriteCore(new ReadOnlySpan(GetBuffer(), 0, _writePos)); - } - - _writePos = 0; - } - - public override void SetLength(long value) - { - // Handle buffering updates. - if (_writePos > 0) - { - FlushWriteBuffer(); - } - else if (_readPos < _readLength) - { - FlushReadBuffer(); - } - _readPos = 0; - _readLength = 0; - - 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. - private unsafe void SetLengthCore(long value) - { - Debug.Assert(value >= 0, "value >= 0"); - VerifyOSHandlePosition(); - - FileStreamHelpers.SetFileLength(_fileHandle, value); - - if (_filePosition > value) - { - SeekCore(_fileHandle, 0, SeekOrigin.End); - } - } - - private int ReadSpan(Span destination) - { - Debug.Assert(!_useAsyncIO, "Must only be used when in synchronous mode"); - Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), - "We're either reading or writing, but not both."); - - bool isBlocked = false; - int n = _readLength - _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) - { - if (!CanRead) ThrowHelper.ThrowNotSupportedException_UnreadableStream(); - if (_writePos > 0) FlushWriteBuffer(); - if (!CanSeek || (destination.Length >= _bufferLength)) - { - n = ReadNative(destination); - // Throw away read buffer. - _readPos = 0; - _readLength = 0; - return n; - } - n = ReadNative(GetBuffer()); - if (n == 0) return 0; - isBlocked = n < _bufferLength; - _readPos = 0; - _readLength = n; - } - // Now copy min of count or numBytesAvailable (i.e. near EOF) to array. - if (n > destination.Length) n = destination.Length; - new ReadOnlySpan(GetBuffer(), _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 (_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 - // 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 == _readLength, "Read buffer should be empty!"); - int moreBytesRead = ReadNative(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; - _readLength = 0; - } - } - - return n; - } - - [Conditional("DEBUG")] - private void AssertCanRead() - { - Debug.Assert(!_fileHandle.IsClosed, "!_fileHandle.IsClosed"); - Debug.Assert(CanRead, "CanRead"); - } - - /// Reads from the file handle into the buffer, overwriting anything in it. - private int FillReadBufferForReadByte() => - _useAsyncIO ? - ReadNativeAsync(new Memory(_buffer), 0, CancellationToken.None).GetAwaiter().GetResult() : - ReadNative(_buffer); - - private unsafe int ReadNative(Span buffer) - { - Debug.Assert(!_useAsyncIO, $"{nameof(ReadNative)} doesn't work on asynchronous file streams."); - AssertCanRead(); - - // Make sure we are reading from the right spot - VerifyOSHandlePosition(); - - int r = ReadFileNative(_fileHandle, buffer, null, out int errorCode); - - if (r == -1) - { - // For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe. - if (errorCode == Interop.Errors.ERROR_BROKEN_PIPE) - { - r = 0; - } - else - { - if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) - ThrowHelper.ThrowArgumentException_HandleNotSync(nameof(_fileHandle)); - - throw Win32Marshal.GetExceptionForWin32Error(errorCode, _fileHandle.Path); - } - } - Debug.Assert(r >= 0, "FileStream's ReadNative is likely broken."); - _filePosition += r; - - return r; - } - - 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) 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."); - - // If we've got bytes in our buffer to write, write them out. - // If we've read in and consumed some bytes, we'll have to adjust - // our seek positions ONLY IF we're seeking relative to the current - // position in the stream. This simulates doing a seek to the new - // position, then a read for the number of bytes we have in our buffer. - if (_writePos > 0) - { - FlushWriteBuffer(); - } - else if (origin == SeekOrigin.Current) - { - // Don't call FlushRead here, which would have caused an infinite - // loop. Simply adjust the seek origin. This isn't necessary - // if we're seeking relative to the beginning or end of the stream. - offset -= (_readLength - _readPos); - } - _readPos = _readLength = 0; - - // Verify that internal position is in sync with the handle - VerifyOSHandlePosition(); - - long oldPos = _filePosition + (_readPos - _readLength); - 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); - } - - // We now must update the read buffer. We can in some cases simply - // update _readPos within the buffer, copy around the buffer so our - // Position property is still correct, and avoid having to do more - // reads from the disk. Otherwise, discard the buffer's contents. - if (_readLength > 0) - { - // We can optimize the following condition: - // oldPos - _readPos <= pos < oldPos + _readLen - _readPos - if (oldPos == pos) - { - if (_readPos > 0) - { - Buffer.BlockCopy(GetBuffer(), _readPos, GetBuffer(), 0, _readLength - _readPos); - _readLength -= _readPos; - _readPos = 0; - } - // If we still have buffered data, we must update the stream's - // position so our Position property is correct. - if (_readLength > 0) - SeekCore(_fileHandle, _readLength, SeekOrigin.Current); - } - else if (oldPos - _readPos < pos && pos < oldPos + _readLength - _readPos) - { - int diff = (int)(pos - oldPos); - Buffer.BlockCopy(GetBuffer(), _readPos + diff, GetBuffer(), 0, _readLength - (_readPos + diff)); - _readLength -= (_readPos + diff); - _readPos = 0; - if (_readLength > 0) - SeekCore(_fileHandle, _readLength, SeekOrigin.Current); - } - else - { - // Lose the read buffer. - _readPos = 0; - _readLength = 0; - } - Debug.Assert(_readLength >= 0 && _readPos <= _readLength, "_readLen should be nonnegative, and _readPos should be less than or equal _readLen"); - Debug.Assert(pos == Position, "Seek optimization: pos != Position! Buffer math was mangled."); - } - return pos; - } - - partial void OnBufferAllocated() - { - Debug.Assert(_buffer != null); - Debug.Assert(_preallocatedOverlapped == null); - - if (_useAsyncIO) - _preallocatedOverlapped = PreAllocatedOverlapped.UnsafeCreate(CompletionSource.s_ioCallback, this, _buffer); - } - - private CompletionSource? CompareExchangeCurrentOverlappedOwner(CompletionSource? newSource, CompletionSource? existingSource) - => Interlocked.CompareExchange(ref _currentOverlappedOwner, newSource, existingSource); - - private void WriteSpan(ReadOnlySpan source) - { - Debug.Assert(!_useAsyncIO, "Must only be used when in synchronous mode"); - - if (_writePos == 0) - { - // Ensure we can write to the stream, and ready buffer for writing. - if (!CanWrite) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); - if (_readPos < _readLength) FlushReadBuffer(); - _readPos = 0; - _readLength = 0; - } - - // 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) - { - int numBytes = _bufferLength - _writePos; // space left in buffer - if (numBytes > 0) - { - if (numBytes >= source.Length) - { - source.CopyTo(GetBuffer().AsSpan(_writePos)); - _writePos += source.Length; - return; - } - else - { - source.Slice(0, numBytes).CopyTo(GetBuffer().AsSpan(_writePos)); - _writePos += numBytes; - source = source.Slice(numBytes); - } - } - // Reset our buffer. We essentially want to call FlushWrite - // without calling Flush on the underlying Stream. - - WriteCore(new ReadOnlySpan(GetBuffer(), 0, _writePos)); - _writePos = 0; - } - - // If the buffer would slow writes down, avoid buffer completely. - if (source.Length >= _bufferLength) - { - Debug.Assert(_writePos == 0, "FileStream cannot have buffered data to write here! Your stream will be corrupted."); - WriteCore(source); - 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. - source.CopyTo(GetBuffer().AsSpan(_writePos)); - _writePos = source.Length; - return; - } - - private unsafe void WriteCore(ReadOnlySpan source) - { - Debug.Assert(!_useAsyncIO); - Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); - Debug.Assert(CanWrite, "_parent.CanWrite"); - Debug.Assert(_readPos == _readLength, "_readPos == _readLen"); - - // Make sure we are writing to the position that we think we are - VerifyOSHandlePosition(); - - int r = 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 == Interop.Errors.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 == Interop.Errors.ERROR_INVALID_PARAMETER) - throw new IOException(SR.IO_FileTooLongOrHandleNotSync); - throw Win32Marshal.GetExceptionForWin32Error(errorCode, _fileHandle.Path); - } - } - Debug.Assert(r >= 0, "FileStream's WriteCore is likely broken."); - _filePosition += r; - return; - } - - private Task? ReadAsyncInternal(Memory destination, CancellationToken cancellationToken, out int synchronousResult) - { - Debug.Assert(_useAsyncIO); - 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."); - - if (!_fileHandle.CanSeek) - { - // 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 < _readLength) - { - int n = Math.Min(_readLength - _readPos, destination.Length); - new Span(GetBuffer(), _readPos, n).CopyTo(destination.Span); - _readPos += n; - synchronousResult = n; - return null; - } - else - { - Debug.Assert(_writePos == 0, "Win32FileStream must not have buffered write data here! Pipes should be unidirectional."); - synchronousResult = 0; - return ReadNativeAsync(destination, 0, cancellationToken); - } - } - - Debug.Assert(_fileHandle.CanSeek, "Should be seekable"); - - // Handle buffering. - if (_writePos > 0) FlushWriteBuffer(); - if (_readPos == _readLength) - { - // I can't see how to handle buffering of async requests when - // filling the buffer asynchronously, without a lot of complexity. - // The problems I see are issuing an async read, we do an async - // read to fill the buffer, then someone issues another read - // (either synchronously or asynchronously) before the first one - // returns. This would involve some sort of complex buffer locking - // that we probably don't want to get into, at least not in V1. - // If we did a sync read to fill the buffer, we could avoid the - // problem, and any async read less than 64K gets turned into a - // synchronous read by NT anyways... -- - - if (destination.Length < _bufferLength) - { - Task readTask = ReadNativeAsync(new Memory(GetBuffer()), 0, cancellationToken); - _readLength = readTask.GetAwaiter().GetResult(); - int n = Math.Min(_readLength, destination.Length); - new Span(GetBuffer(), 0, n).CopyTo(destination.Span); - _readPos = n; - - synchronousResult = n; - return null; - } - else - { - // Here we're making our position pointer inconsistent - // with our read buffer. Throw away the read buffer's contents. - _readPos = 0; - _readLength = 0; - synchronousResult = 0; - return ReadNativeAsync(destination, 0, cancellationToken); - } - } - else - { - int n = Math.Min(_readLength - _readPos, destination.Length); - new Span(GetBuffer(), _readPos, n).CopyTo(destination.Span); - _readPos += n; - - if (n == destination.Length) - { - // Return a completed task - synchronousResult = n; - return null; - } - else - { - // For streams with no clear EOF like serial ports or pipes - // we cannot read more data without causing an app to block - // incorrectly. Pipes don't go down this path - // though. This code needs to be fixed. - // Throw away read buffer. - _readPos = 0; - _readLength = 0; - synchronousResult = 0; - return ReadNativeAsync(destination.Slice(n), n, cancellationToken); - } - } - } - - private unsafe Task ReadNativeAsync(Memory destination, int numBufferedBytesRead, CancellationToken cancellationToken) - { - AssertCanRead(); - Debug.Assert(_useAsyncIO, "ReadNativeAsync doesn't work on synchronous file streams!"); - - // Create and store async stream class library specific data in the async result - CompletionSource completionSource = CompletionSource.Create(this, _preallocatedOverlapped, numBufferedBytesRead, 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 = 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 == Interop.Errors.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 != Interop.Errors.ERROR_IO_PENDING) - { - if (!_fileHandle.IsClosed && CanSeek) // Update Position - It could be anywhere. - { - SeekCore(_fileHandle, 0, SeekOrigin.Current); - } - - completionSource.ReleaseNativeResource(); - - if (errorCode == Interop.Errors.ERROR_HANDLE_EOF) - { - ThrowHelper.ThrowEndOfFileException(); - } - else - { - throw Win32Marshal.GetExceptionForWin32Error(errorCode, _fileHandle.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; - } - - private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationToken cancellationToken) - { - 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.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.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) - { - if (_readPos < _readLength) - { - FlushReadBuffer(); - } - _readPos = 0; - _readLength = 0; - } - - // Determine how much space remains in the buffer - int remainingBuffer = _bufferLength - _writePos; - Debug.Assert(remainingBuffer >= 0); - - // Simple/common case: - // - The write is smaller than our buffer, such that it's worth considering buffering it. - // - There's no active flush operation, such that we don't have to worry about the existing buffer being in use. - // - And the data we're trying to write fits in the buffer, meaning it wasn't already filled by previous writes. - // In that case, just store it in the buffer. - if (source.Length < _bufferLength && !HasActiveBufferOperation && source.Length <= remainingBuffer) - { - source.Span.CopyTo(new Span(GetBuffer(), _writePos, source.Length)); - _writePos += source.Length; - writeDataStoredInBuffer = true; - - // There is one special-but-common case, common because devs often use - // byte[] sizes that are powers of 2 and thus fit nicely into our buffer, which is - // also a power of 2. If after our write the buffer still has remaining space, - // then we're done and can return a completed task now. But if we filled the buffer - // completely, we want to do the asynchronous flush/write as part of this operation - // rather than waiting until the next write that fills the buffer. - if (source.Length != remainingBuffer) - return default; - - Debug.Assert(_writePos == _bufferLength); - } - } - - // At this point, at least one of the following is true: - // 1. There was an active flush operation (it could have completed by now, though). - // 2. The data doesn't fit in the remaining buffer (or it's a pipe and we chose not to try). - // 3. We wrote all of the data to the buffer, filling it. - // - // If there's an active operation, we can't touch the current buffer because it's in use. - // That gives us a choice: we can either allocate a new buffer, or we can skip the buffer - // entirely (even if the data would otherwise fit in it). For now, for simplicity, we do - // the latter; it could also have performance wins due to OS-level optimizations, and we could - // potentially add support for PreAllocatedOverlapped due to having a single buffer. (We can - // switch to allocating a new buffer, potentially experimenting with buffer pooling, should - // performance data suggest it's appropriate.) - // - // If the data doesn't fit in the remaining buffer, it could be because it's so large - // it's greater than the entire buffer size, in which case we'd always skip the buffer, - // or it could be because there's more data than just the space remaining. For the latter - // case, we need to issue an asynchronous write to flush that data, which then turns this into - // the first case above with an active operation. - // - // If we already stored the data, then we have nothing additional to write beyond what - // we need to flush. - // - // In any of these cases, we have the same outcome: - // - If there's data in the buffer, flush it by writing it out asynchronously. - // - Then, if there's any data to be written, issue a write for it concurrently. - // We return a Task that represents one or both. - - // Flush the buffer asynchronously if there's anything to flush - Task? flushTask = null; - if (_writePos > 0) - { - flushTask = FlushWriteAsync(cancellationToken); - - // If we already copied all of the data into the buffer, - // simply return the flush task here. Same goes for if the task has - // already completed and was unsuccessful. - if (writeDataStoredInBuffer || - flushTask.IsFaulted || - flushTask.IsCanceled) - { - return new ValueTask(flushTask); - } - } - - Debug.Assert(!writeDataStoredInBuffer); - Debug.Assert(_writePos == 0); - - // Finally, issue the write asynchronously, and return a Task that logically - // represents the write operation, including any flushing done. - Task writeTask = WriteAsyncInternalCore(source, cancellationToken); - return new ValueTask( - (flushTask == null || flushTask.Status == TaskStatus.RanToCompletion) ? writeTask : - (writeTask.Status == TaskStatus.RanToCompletion) ? flushTask : - Task.WhenAll(flushTask, writeTask)); - } - - private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, CancellationToken cancellationToken) - { - Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); - Debug.Assert(CanWrite, "_parent.CanWrite"); - Debug.Assert(_readPos == _readLength, "_readPos == _readLen"); - Debug.Assert(_useAsyncIO, "WriteInternalCoreAsync doesn't work on synchronous file streams!"); - - // Create and store async stream class library specific data in the async result - CompletionSource completionSource = CompletionSource.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 = 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 == Interop.Errors.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 != Interop.Errors.ERROR_IO_PENDING) - { - if (!_fileHandle.IsClosed && CanSeek) // Update Position - It could be anywhere. - { - SeekCore(_fileHandle, 0, SeekOrigin.Current); - } - - completionSource.ReleaseNativeResource(); - - if (errorCode == Interop.Errors.ERROR_HANDLE_EOF) - { - ThrowHelper.ThrowEndOfFileException(); - } - else - { - throw Win32Marshal.GetExceptionForWin32Error(errorCode, _fileHandle.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; - } - - // __ConsoleStream also uses this code. - private unsafe int ReadFileNative(SafeFileHandle handle, Span bytes, NativeOverlapped* overlapped, out int errorCode) - { - Debug.Assert((_useAsyncIO && overlapped != null) || (!_useAsyncIO && overlapped == null), "Async IO and overlapped parameters inconsistent in call to ReadFileNative."); - - return FileStreamHelpers.ReadFileNative(handle, bytes, overlapped, out errorCode); - } - - private unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan buffer, NativeOverlapped* overlapped, out int errorCode) - { - 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 = overlapped == null - ? Interop.Kernel32.WriteFile(handle, p, buffer.Length, out numBytesWritten, overlapped) - : Interop.Kernel32.WriteFile(handle, p, buffer.Length, IntPtr.Zero, overlapped); - } - - if (r == 0) - { - errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); - return -1; - } - else - { - errorCode = 0; - return numBytesWritten; - } - } - - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - // If we're in sync mode, just use the shared CopyToAsync implementation that does - // typical read/write looping. - if (!_useAsyncIO) - { - return base.CopyToAsync(destination, bufferSize, cancellationToken); - } - - // Fail if the file was closed - if (_fileHandle.IsClosed) - { - ThrowHelper.ThrowObjectDisposedException_FileClosed(); - } - if (!CanRead) - { - ThrowHelper.ThrowNotSupportedException_UnreadableStream(); - } - - // Bail early for cancellation if cancellation has been requested - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - - // Do the async copy, with differing implementations based on whether the FileStream was opened as async or sync - Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); - return AsyncModeCopyToAsync(destination, bufferSize, cancellationToken); - } - - private async Task AsyncModeCopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - Debug.Assert(_useAsyncIO, "This implementation is for async mode only"); - Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); - Debug.Assert(CanRead, "_parent.CanRead"); - - // Make sure any pending writes have been flushed before we do a read. - if (_writePos > 0) - { - await FlushWriteAsync(cancellationToken).ConfigureAwait(false); - } - - // Typically CopyToAsync would be invoked as the only "read" on the stream, but it's possible some reading is - // done and then the CopyToAsync is issued. For that case, see if we have any data available in the buffer. - if (GetBuffer() != null) - { - int bufferedBytes = _readLength - _readPos; - if (bufferedBytes > 0) - { - await destination.WriteAsync(new ReadOnlyMemory(GetBuffer(), _readPos, bufferedBytes), cancellationToken).ConfigureAwait(false); - _readPos = _readLength = 0; - } - } - - bool canSeek = CanSeek; - if (canSeek) - { - VerifyOSHandlePosition(); - } - - try - { - await FileStreamHelpers - .AsyncModeCopyToAsync(_fileHandle, canSeek, _filePosition, destination, bufferSize, cancellationToken) - .ConfigureAwait(false); - } - finally - { - // Make sure the stream's current position reflects where we ended up - if (!_fileHandle.IsClosed && CanSeek) - { - SeekCore(_fileHandle, 0, SeekOrigin.End); - } - } - } - - 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 deleted file mode 100644 index 797f6f5e9c0ce8..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs +++ /dev/null @@ -1,536 +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.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Win32.SafeHandles; - -namespace System.IO.Strategies -{ - // This type is partial so we can avoid code duplication between Windows and Unix Net5Compat implementations - internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy - { - private byte[]? _buffer; - private readonly int _bufferLength; - private readonly SafeFileHandle _fileHandle; // only ever null if ctor throws - - /// Whether the file is opened for reading, writing, or both. - private readonly FileAccess _access; - - /// The next available byte to be read from the _buffer. - private int _readPos; - - /// The number of valid bytes in _buffer. - private int _readLength; - - /// The next location in which a write should occur to the buffer. - private int _writePos; - - /// - /// Whether asynchronous read/write/flush operations should be performed using async I/O. - /// On Windows FileOptions.Asynchronous controls how the file handle is configured, - /// and then as a result how operations are issued against that file handle. On Unix, - /// there isn't any distinction around how file descriptors are created for async vs - /// sync, but we still differentiate how the operations are issued in order to provide - /// similar behavioral semantics and performance characteristics as on Windows. On - /// Windows, if non-async, async read/write requests just delegate to the base stream, - /// and no attempt is made to synchronize between sync and async operations on the stream; - /// if async, then async read/write requests are implemented specially, and sync read/write - /// requests are coordinated with async ones by implementing the sync ones over the async - /// ones. On Unix, we do something similar. If non-async, async read/write requests just - /// delegate to the base stream, and no attempt is made to synchronize. If async, we use - /// a semaphore to coordinate both sync and async operations. - /// - private readonly bool _useAsyncIO; - - /// cached task for read ops that complete synchronously - private Task? _lastSynchronouslyCompletedTask; - - /// - /// Currently cached position in the stream. This should always mirror the underlying file's actual position, - /// and should only ever be out of sync if another stream with access to this same file manipulates it, at which - /// point we attempt to error out. - /// - private long _filePosition; - - /// Whether the file stream's handle has been exposed. - private bool _exposedHandle; - - internal Net5CompatFileStreamStrategy(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) - { - _exposedHandle = true; - _bufferLength = bufferSize; - - InitFromHandle(handle, access, isAsync); - - // Note: It would be cleaner to set the following fields in ValidateHandle, - // but we can't as they're readonly. - _access = access; - _useAsyncIO = isAsync; - - // 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 Net5CompatFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) - { - string fullPath = Path.GetFullPath(path); - - _access = access; - _bufferLength = bufferSize; - - if ((options & FileOptions.Asynchronous) != 0) - _useAsyncIO = true; - - _fileHandle = SafeFileHandle.Open(fullPath, mode, access, share, options, preallocationSize); - - try - { - Init(mode, path, options); - } - catch - { - // If anything goes wrong while setting up the stream, make sure we deterministically dispose - // of the opened handle. - _fileHandle.Dispose(); - _fileHandle = null!; - throw; - } - } - - ~Net5CompatFileStreamStrategy() => Dispose(false); // mandatory to Flush the write buffer - - internal override void DisposeInternal(bool disposing) => Dispose(disposing); - - public override Task FlushAsync(CancellationToken cancellationToken) - { - // TODO: https://github.com/dotnet/runtime/issues/27643 (stop doing this synchronous work!!). - // The always synchronous data transfer between the OS and the internal buffer is intentional - // because this is needed to allow concurrent async IO requests. Concurrent data transfer - // between the OS and the internal buffer will result in race conditions. Since FlushWrite and - // FlushRead modify internal state of the stream and transfer data between the OS and the - // internal buffer, they cannot be truly async. We will, however, flush the OS file buffers - // asynchronously because it doesn't modify any internal state of the stream and is potentially - // a long running process. - try - { - FlushInternalBuffer(); - } - catch (Exception e) - { - return Task.FromException(e); - } - - return Task.CompletedTask; - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _useAsyncIO ? - ReadAsyncTask(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult() : - ReadSpan(new Span(buffer, offset, count)); - } - - public override int Read(Span buffer) - { - if (!_useAsyncIO) - { - if (_fileHandle.IsClosed) - { - ThrowHelper.ThrowObjectDisposedException_FileClosed(); - } - - return ReadSpan(buffer); - } - - // If the stream is in async mode, we can't call the synchronous ReadSpan, so we similarly call the base Read, - // which will turn delegate to Read(byte[],int,int), which will do the right thing if we're in async mode. - return base.Read(buffer); - } - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (!_useAsyncIO) - { - // 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 BeginReadInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); - } - - return ReadAsyncTask(buffer, offset, count, cancellationToken); - } - - public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - if (!_useAsyncIO) - { - // 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(BeginReadInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : - base.ReadAsync(buffer, cancellationToken); - } - - Task? t = ReadAsyncInternal(buffer, cancellationToken, out int synchronousResult); - return t != null ? - new ValueTask(t) : - new ValueTask(synchronousResult); - } - - private Task ReadAsyncTask(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - Task? t = ReadAsyncInternal(new Memory(buffer, offset, count), cancellationToken, out int synchronousResult); - - if (t == null) - { - t = _lastSynchronouslyCompletedTask; - Debug.Assert(t == null || t.IsCompletedSuccessfully, "Cached task should have completed successfully"); - - if (t == null || t.Result != synchronousResult) - { - _lastSynchronouslyCompletedTask = t = Task.FromResult(synchronousResult); - } - } - - return t; - } - - public override void Write(byte[] buffer, int offset, int count) - { - if (_useAsyncIO) - { - WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), CancellationToken.None).AsTask().GetAwaiter().GetResult(); - } - else - { - WriteSpan(new ReadOnlySpan(buffer, offset, count)); - } - } - - public override void Write(ReadOnlySpan buffer) - { - if (!_useAsyncIO) - { - if (_fileHandle.IsClosed) - { - ThrowHelper.ThrowObjectDisposedException_FileClosed(); - } - - WriteSpan(buffer); - } - else - { - // If the stream is in async mode, we can't call the synchronous WriteSpan, so we similarly call the base Write, - // which will turn delegate to Write(byte[],int,int), which will do the right thing if we're in async mode. - base.Write(buffer); - } - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (!_useAsyncIO) - { - // 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 BeginWriteInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); - } - - return WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); - } - - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - if (!_useAsyncIO) - { - // 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(BeginWriteInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : - base.WriteAsync(buffer, cancellationToken); - } - - return WriteAsyncInternal(buffer, cancellationToken); - } - - public override void Flush() => Flush(flushToDisk: false); - - internal override void Flush(bool flushToDisk) - { - FlushInternalBuffer(); - - if (flushToDisk && CanWrite) - { - FileStreamHelpers.FlushToDisk(_fileHandle); - } - } - - public override bool CanRead => !_fileHandle.IsClosed && (_access & FileAccess.Read) != 0; - - public override bool CanWrite => !_fileHandle.IsClosed && (_access & FileAccess.Write) != 0; - - internal override SafeFileHandle SafeFileHandle - { - get - { - Flush(); - _exposedHandle = true; - return _fileHandle; - } - } - - internal override string Name => _fileHandle.Path ?? SR.IO_UnknownFileName; - - internal override bool IsAsync => _useAsyncIO; - - /// - /// 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. - /// - private 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. - _readPos = _readLength = 0; - if (_writePos > 0) - { - _writePos = 0; - throw new IOException(SR.IO_FileStreamHandlePosition); - } - } - } - } - - /// Verifies that state relating to the read/write buffer is consistent. - [Conditional("DEBUG")] - private void AssertBufferInvariants() - { - // Read buffer values must be in range: 0 <= _bufferReadPos <= _bufferReadLength <= _bufferLength - Debug.Assert(0 <= _readPos && _readPos <= _readLength && _readLength <= _bufferLength); - - // Write buffer values must be in range: 0 <= _bufferWritePos <= _bufferLength - Debug.Assert(0 <= _writePos && _writePos <= _bufferLength); - - // Read buffering and write buffering can't both be active - Debug.Assert((_readPos == 0 && _readLength == 0) || _writePos == 0); - } - - /// Validates that we're ready to read from the stream. - private void PrepareForReading() - { - if (_fileHandle.IsClosed) - ThrowHelper.ThrowObjectDisposedException_FileClosed(); - if (_readLength == 0 && !CanRead) - ThrowHelper.ThrowNotSupportedException_UnreadableStream(); - - AssertBufferInvariants(); - } - - /// Gets or sets the position within the current stream - public override long Position - { - get - { - AssertBufferInvariants(); - VerifyOSHandlePosition(); - - // We may have read data into our buffer from the handle, such that the handle position - // is artificially further along than the consumer's view of the stream's position. - // Thus, when reading, our position is really starting from the handle position negatively - // offset by the number of bytes in the buffer and positively offset by the number of - // bytes into that buffer we've read. When writing, both the read length and position - // must be zero, and our position is just the handle position offset positive by how many - // bytes we've written into the buffer. - return (_filePosition - _readLength) + _readPos + _writePos; - } - set - { - Seek(value, SeekOrigin.Begin); - } - } - - // 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; - - /// - /// Gets the array used for buffering reading and writing. - /// If the array hasn't been allocated, this will lazily allocate it. - /// - /// The buffer. - private byte[] GetBuffer() - { - Debug.Assert(_buffer == null || _buffer.Length == _bufferLength); - if (_buffer == null) - { - _buffer = new byte[_bufferLength]; - OnBufferAllocated(); - } - - return _buffer; - } - - /// - /// Flushes the internal read/write buffer for this stream. If write data has been buffered, - /// that data is written out to the underlying file. Or if data has been buffered for - /// reading from the stream, the data is dumped and our position in the underlying file - /// is rewound as necessary. This does not flush the OS buffer. - /// - private void FlushInternalBuffer() - { - AssertBufferInvariants(); - if (_writePos > 0) - { - FlushWriteBuffer(); - } - else if (_readPos < _readLength && CanSeek) - { - FlushReadBuffer(); - } - } - - /// Dumps any read data in the buffer and rewinds our position in the stream, accordingly, as necessary. - private void FlushReadBuffer() - { - // Reading is done by blocks from the file, but someone could read - // 1 byte from the buffer then write. At that point, the OS's file - // pointer is out of sync with the stream's position. All write - // functions should call this function to preserve the position in the file. - - AssertBufferInvariants(); - Debug.Assert(_writePos == 0, "FileStream: Write buffer must be empty in FlushReadBuffer!"); - - int rewind = _readPos - _readLength; - if (rewind != 0) - { - Debug.Assert(CanSeek, "FileStream will lose buffered read data now."); - SeekCore(_fileHandle, rewind, SeekOrigin.Current); - } - _readPos = _readLength = 0; - } - - /// - /// Reads a byte from the file stream. Returns the byte cast to an int - /// or -1 if reading from the end of the stream. - /// - public override int ReadByte() - { - PrepareForReading(); - - byte[] buffer = GetBuffer(); - if (_readPos == _readLength) - { - FlushWriteBuffer(); - _readLength = FillReadBufferForReadByte(); - _readPos = 0; - if (_readLength == 0) - { - return -1; - } - } - - return buffer[_readPos++]; - } - - /// - /// Writes a byte to the current position in the stream and advances the position - /// within the stream by one byte. - /// - /// The byte to write to the stream. - public override void WriteByte(byte value) - { - PrepareForWriting(); - - // Flush the write buffer if it's full - if (_writePos == _bufferLength) - FlushWriteBufferForWriteByte(); - - // We now have space in the buffer. Store the byte. - GetBuffer()[_writePos++] = value; - } - - /// - /// Validates that we're ready to write to the stream, - /// including flushing a read buffer if necessary. - /// - private void PrepareForWriting() - { - if (_fileHandle.IsClosed) - 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) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); - FlushReadBuffer(); - Debug.Assert(_bufferLength > 0, "_bufferSize > 0"); - } - } - - partial void OnBufferAllocated(); - - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - { - if (!_useAsyncIO) - return base.BeginRead(buffer, offset, count, callback, state); - else - return TaskToApm.Begin(ReadAsyncTask(buffer, offset, count, CancellationToken.None), callback, state); - } - - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - { - if (!_useAsyncIO) - return base.BeginWrite(buffer, offset, count, callback, state); - else - return TaskToApm.Begin(WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), CancellationToken.None).AsTask(), callback, state); - } - - public override int EndRead(IAsyncResult asyncResult) - { - if (!_useAsyncIO) - return base.EndRead(asyncResult); - else - return TaskToApm.End(asyncResult); - } - - public override void EndWrite(IAsyncResult asyncResult) - { - if (!_useAsyncIO) - base.EndWrite(asyncResult); - else - TaskToApm.End(asyncResult); - } - } -}