diff --git a/src/coreclr/System.Private.CoreLib/src/System/Threading/ClrThreadPoolPreAllocatedOverlapped.cs b/src/coreclr/System.Private.CoreLib/src/System/Threading/ClrThreadPoolPreAllocatedOverlapped.cs index 06df433df4660..d856d1d9231b3 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Threading/ClrThreadPoolPreAllocatedOverlapped.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Threading/ClrThreadPoolPreAllocatedOverlapped.cs @@ -95,5 +95,7 @@ unsafe void IDeferredDisposable.OnFinalRelease(bool disposed) } } } + + internal bool IsUserObject(byte[]? buffer) => _overlapped.IsUserObject(buffer); } } diff --git a/src/coreclr/System.Private.CoreLib/src/System/Threading/Overlapped.cs b/src/coreclr/System.Private.CoreLib/src/System/Threading/Overlapped.cs index 728954377dff8..53abea5f0d16d 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Threading/Overlapped.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Threading/Overlapped.cs @@ -132,6 +132,8 @@ internal sealed unsafe class OverlappedData return AllocateNativeOverlapped(); } + internal bool IsUserObject(byte[]? buffer) => ReferenceEquals(_userObject, buffer); + [MethodImpl(MethodImplOptions.InternalCall)] private extern NativeOverlapped* AllocateNativeOverlapped(); @@ -258,6 +260,8 @@ public static unsafe void Free(NativeOverlapped* nativeOverlappedPtr) OverlappedData.GetOverlappedFromNative(nativeOverlappedPtr)._overlapped._overlappedData = null; OverlappedData.FreeNativeOverlapped(nativeOverlappedPtr); } + + internal bool IsUserObject(byte[]? buffer) => _overlappedData!.IsUserObject(buffer); } #endregion class Overlapped diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index c41a3df0cd5ac..32801918ebfe2 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -187,14 +187,14 @@ public static string GetDistroVersionString() } } - private static readonly Lazy m_isInvariant = new Lazy(GetIsInvariantGlobalization); + private static readonly Lazy m_isInvariant = new Lazy(() => GetStaticNonPublicBooleanPropertyValue("System.Globalization.GlobalizationMode", "Invariant")); - private static bool GetIsInvariantGlobalization() + private static bool GetStaticNonPublicBooleanPropertyValue(string typeName, string propertyName) { - Type globalizationMode = Type.GetType("System.Globalization.GlobalizationMode"); + Type globalizationMode = Type.GetType(typeName); if (globalizationMode != null) { - MethodInfo methodInfo = globalizationMode.GetProperty("Invariant", BindingFlags.NonPublic | BindingFlags.Static)?.GetMethod; + MethodInfo methodInfo = globalizationMode.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetMethod; if (methodInfo != null) { return (bool)methodInfo.Invoke(null, null); @@ -235,6 +235,10 @@ private static Version GetICUVersion() version & 0xFF); } + private static readonly Lazy _legacyFileStream = new Lazy(() => GetStaticNonPublicBooleanPropertyValue("System.IO.FileStreamHelpers", "UseLegacyStrategy")); + + public static bool IsLegacyFileStreamEnabled => _legacyFileStream.Value; + private static bool GetIsInContainer() { if (IsWindows) diff --git a/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln b/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln index 4c3dc58180ee8..91fcdfa5375e5 100644 --- a/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln +++ b/src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln @@ -25,6 +25,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{32A31E04-255 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D9FB1730-B750-4C0D-8D24-8C992DEB6034}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.FileSystem.Legacy.Tests", "tests\LegacyTests\System.IO.FileSystem.Legacy.Tests.csproj", "{48E07F12-8597-40DE-8A37-CCBEB9D54012}" +EndProject Global GlobalSection(NestedProjects) = preSolution {D350D6E7-52F1-40A4-B646-C178F6BBB689} = {1A727AF9-4F39-4109-BB8F-813286031DC9} @@ -37,6 +39,7 @@ Global {06DD5AA8-FDA6-495B-A8D1-8CE83C78DE6C} = {32A31E04-2554-4223-BED8-45757408B4F6} {877E39A8-51CB-463A-AF4C-6FAE4F438075} = {D9FB1730-B750-4C0D-8D24-8C992DEB6034} {D7DF8034-3AE5-4DEF-BCC4-6353239391BF} = {D9FB1730-B750-4C0D-8D24-8C992DEB6034} + {48E07F12-8597-40DE-8A37-CCBEB9D54012} = {1A727AF9-4F39-4109-BB8F-813286031DC9} EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +86,10 @@ Global {06DD5AA8-FDA6-495B-A8D1-8CE83C78DE6C}.Debug|Any CPU.Build.0 = Debug|Any CPU {06DD5AA8-FDA6-495B-A8D1-8CE83C78DE6C}.Release|Any CPU.ActiveCfg = Release|Any CPU {06DD5AA8-FDA6-495B-A8D1-8CE83C78DE6C}.Release|Any CPU.Build.0 = Release|Any CPU + {48E07F12-8597-40DE-8A37-CCBEB9D54012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48E07F12-8597-40DE-8A37-CCBEB9D54012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48E07F12-8597-40DE-8A37-CCBEB9D54012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48E07F12-8597-40DE-8A37-CCBEB9D54012}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs index 04773b5a8df72..57ce00dea3d21 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs @@ -37,6 +37,14 @@ public void DisposeClosesHandle() } } + [Fact] + public void DisposingBufferedFileStreamThatWasClosedViaSafeFileHandleCloseDoesNotThrow() + { + FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, bufferSize: 100); + fs.SafeFileHandle.Dispose(); + fs.Dispose(); // must not throw + } + [Fact] public void AccessFlushesFileClosesHandle() { @@ -96,15 +104,13 @@ private async Task ThrowWhenHandlePositionIsChanged(bool useAsync) // Put data in FS write buffer and update position from FSR fs.WriteByte(0); fsr.Position = 0; - Assert.Throws(() => fs.Position); - fs.WriteByte(0); - fsr.Position++; - Assert.Throws(() => fs.Read(new byte[1], 0, 1)); - - fs.WriteByte(0); - fsr.Position++; - if (useAsync && OperatingSystem.IsWindows()) // Async I/O behaviors differ due to kernel-based implementation on Windows + if (useAsync + // Async I/O behaviors differ due to kernel-based implementation on Windows + && OperatingSystem.IsWindows() + // ReadAsync which in this case (single byte written to buffer) calls FlushAsync is now 100% async + // so it does not complete synchronously anymore + && PlatformDetection.IsLegacyFileStreamEnabled) { Assert.Throws(() => FSAssert.CompletesSynchronously(fs.ReadAsync(new byte[1], 0, 1))); } @@ -113,6 +119,14 @@ private async Task ThrowWhenHandlePositionIsChanged(bool useAsync) 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()); diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs index 6ed4d1db2c4cc..fb4c02f0a064a 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs @@ -222,7 +222,15 @@ public async Task ManyConcurrentWriteAsyncs_OuterLoop( Assert.Null(writes[i].Exception); if (useAsync) { - Assert.Equal((i + 1) * writeSize, fs.Position); + // To ensure that the buffer of a FileStream opened for async IO is flushed + // by FlushAsync in asynchronous way, we aquire a lock for every buffered WriteAsync. + // The side effect of this is that the Position of FileStream is not updated until + // the lock is released by a previous operation. + // So now all WriteAsync calls should be awaited before starting another async file operation. + if (PlatformDetection.IsLegacyFileStreamEnabled) + { + Assert.Equal((i + 1) * writeSize, fs.Position); + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/LegacyTests/LegacySwitchTests.cs b/src/libraries/System.IO.FileSystem/tests/LegacyTests/LegacySwitchTests.cs new file mode 100644 index 0000000000000..c930cee893352 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/LegacyTests/LegacySwitchTests.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Xunit; + +namespace System.IO.Tests +{ + public class LegacySwitchTests + { + [Fact] + public static void LegacySwitchIsHonored() + { + string filePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + + using (FileStream fileStream = File.Create(filePath)) + { + object strategy = fileStream + .GetType() + .GetField("_strategy", BindingFlags.NonPublic | BindingFlags.Instance) + .GetValue(fileStream); + + Assert.DoesNotContain(strategy.GetType().FullName, "Legacy"); + } + + File.Delete(filePath); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj new file mode 100644 index 0000000000000..b1cf5ce99b0c9 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/LegacyTests/System.IO.FileSystem.Legacy.Tests.csproj @@ -0,0 +1,36 @@ + + + true + true + + $(NetCoreAppCurrent)-windows + + --working-dir=/test-dir + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.IO.FileSystem/tests/LegacyTests/runtimeconfig.template.json b/src/libraries/System.IO.FileSystem/tests/LegacyTests/runtimeconfig.template.json new file mode 100644 index 0000000000000..010891ed8fc6c --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/LegacyTests/runtimeconfig.template.json @@ -0,0 +1,5 @@ +{ + "configProperties": { + "System.IO.UseLegacyFileStream": true + } +} diff --git a/src/libraries/System.IO/System.IO.sln b/src/libraries/System.IO/System.IO.sln index fa248eb5af822..7d8ebc2c3da28 100644 --- a/src/libraries/System.IO/System.IO.sln +++ b/src/libraries/System.IO/System.IO.sln @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{9FDAA57A-696 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D9FD8082-D04C-4DA8-9F4C-261D1C65A6D3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.Legacy.Tests", "tests\LegacyTests\System.IO.Legacy.Tests.csproj", "{0217540D-FA86-41B3-9754-7BB5096ABA3E}" +EndProject Global GlobalSection(NestedProjects) = preSolution {D11D3624-1322-45D1-A604-7E68CDB85BE8} = {5AD2C433-C661-4AD1-BD9F-D164ADC43512} @@ -34,6 +36,7 @@ Global {D0D1CDAC-16F8-4382-A219-74A513CC1790} = {9FDAA57A-696B-4CB1-99AE-BCDF91848B75} {0769544B-1A5D-4D74-94FD-899DF6C39D62} = {D9FD8082-D04C-4DA8-9F4C-261D1C65A6D3} {AA5E80B2-A0AA-46F1-B319-5B528BAC382B} = {D9FD8082-D04C-4DA8-9F4C-261D1C65A6D3} + {0217540D-FA86-41B3-9754-7BB5096ABA3E} = {5AD2C433-C661-4AD1-BD9F-D164ADC43512} EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -76,6 +79,10 @@ Global {D0D1CDAC-16F8-4382-A219-74A513CC1790}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0D1CDAC-16F8-4382-A219-74A513CC1790}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0D1CDAC-16F8-4382-A219-74A513CC1790}.Release|Any CPU.Build.0 = Release|Any CPU + {0217540D-FA86-41B3-9754-7BB5096ABA3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0217540D-FA86-41B3-9754-7BB5096ABA3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0217540D-FA86-41B3-9754-7BB5096ABA3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0217540D-FA86-41B3-9754-7BB5096ABA3E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/libraries/System.IO/tests/LegacyTests/System.IO.Legacy.Tests.csproj b/src/libraries/System.IO/tests/LegacyTests/System.IO.Legacy.Tests.csproj new file mode 100644 index 0000000000000..bfc33e36807b6 --- /dev/null +++ b/src/libraries/System.IO/tests/LegacyTests/System.IO.Legacy.Tests.csproj @@ -0,0 +1,25 @@ + + + System.IO + true + true + true + + $(NetCoreAppCurrent)-windows + + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.IO/tests/LegacyTests/runtimeconfig.template.json b/src/libraries/System.IO/tests/LegacyTests/runtimeconfig.template.json new file mode 100644 index 0000000000000..010891ed8fc6c --- /dev/null +++ b/src/libraries/System.IO/tests/LegacyTests/runtimeconfig.template.json @@ -0,0 +1,5 @@ +{ + "configProperties": { + "System.IO.UseLegacyFileStream": true + } +} diff --git a/src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs b/src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs index 34bff3febe863..127d72daa2e36 100644 --- a/src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs @@ -64,7 +64,7 @@ public static byte[] ReadAllBytes(string path) { int n = fs.Read(bytes, index, count); if (n == 0) - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); index += n; count -= n; } diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index f750e78ee7dbe..7aed1af79c2b2 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 @@ -392,11 +392,11 @@ + - @@ -1635,14 +1635,17 @@ + + - + + @@ -1895,6 +1898,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs b/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs index 9175a3e8ba912..331ef281bb32a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs @@ -10,6 +10,20 @@ internal static class AppContextConfigHelper internal static bool GetBooleanConfig(string configName, bool defaultValue) => AppContext.TryGetSwitch(configName, out bool value) ? value : defaultValue; + internal static bool GetBooleanConfig(string switchName, string envVariable) + { + if (!AppContext.TryGetSwitch(switchName, out bool ret)) + { + string? switchValue = Environment.GetEnvironmentVariable(envVariable); + if (switchValue != null) + { + ret = bool.IsTrueStringIgnoreCase(switchValue) || switchValue.Equals("1"); + } + } + + return ret; + } + internal static int GetInt32Config(string configName, int defaultValue, bool allowNegative = true) { try diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs index 1a0bf66c42e56..590aae9b3bdc7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs @@ -10,7 +10,7 @@ internal static partial class GlobalizationMode internal static bool Invariant { get; } = GetInvariantSwitchValue(); internal static bool UseNls { get; } = !Invariant && - (GetSwitchValue("System.Globalization.UseNls", "DOTNET_SYSTEM_GLOBALIZATION_USENLS") || + (AppContextConfigHelper.GetBooleanConfig("System.Globalization.UseNls", "DOTNET_SYSTEM_GLOBALIZATION_USENLS") || !LoadIcu()); private static bool LoadIcu() diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs index 7183786cf0d22..1d2172fb5510c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs @@ -9,27 +9,13 @@ namespace System.Globalization internal static partial class GlobalizationMode { private static bool GetInvariantSwitchValue() => - GetSwitchValue("System.Globalization.Invariant", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); + AppContextConfigHelper.GetBooleanConfig("System.Globalization.Invariant", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); private static bool TryGetAppLocalIcuSwitchValue([NotNullWhen(true)] out string? value) => TryGetStringValue("System.Globalization.AppLocalIcu", "DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU", out value); internal static bool PredefinedCulturesOnly { get; } = - GetSwitchValue("System.Globalization.PredefinedCulturesOnly", "DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY"); - - private static bool GetSwitchValue(string switchName, string envVariable) - { - if (!AppContext.TryGetSwitch(switchName, out bool ret)) - { - string? switchValue = Environment.GetEnvironmentVariable(envVariable); - if (switchValue != null) - { - ret = bool.IsTrueStringIgnoreCase(switchValue) || switchValue.Equals("1"); - } - } - - return ret; - } + AppContextConfigHelper.GetBooleanConfig("System.Globalization.PredefinedCulturesOnly", "DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY"); private static bool TryGetStringValue(string switchName, string envVariable, [NotNullWhen(true)] out string? value) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs new file mode 100644 index 0000000000000..918d9afc88337 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/AsyncWindowsFileStreamStrategy.cs @@ -0,0 +1,407 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + internal sealed partial class AsyncWindowsFileStreamStrategy : WindowsFileStreamStrategy, IFileStreamCompletionSourceStrategy + { + private PreAllocatedOverlapped? _preallocatedOverlapped; // optimization for async ops to avoid per-op allocations + private FileStreamCompletionSource? _currentOverlappedOwner; // async op currently using the preallocated overlapped + + internal AsyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access) + : base(handle, access) + { + } + + internal AsyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + : base(path, mode, access, share, options) + { + } + + internal override bool IsAsync => true; + + public override ValueTask DisposeAsync() + { + // the base class must dispose ThreadPoolBinding and FileHandle + // before _preallocatedOverlapped is disposed + ValueTask result = base.DisposeAsync(); + Debug.Assert(result.IsCompleted, "the method must be sync, as it performs no flushing"); + + _preallocatedOverlapped?.Dispose(); + + return result; + } + + protected override void Dispose(bool disposing) + { + // the base class must dispose ThreadPoolBinding and FileHandle + // before _preallocatedOverlapped is disposed + base.Dispose(disposing); + + _preallocatedOverlapped?.Dispose(); + } + + protected override void OnInitFromHandle(SafeFileHandle handle) + { + // This is necessary for async IO using IO Completion ports via our + // managed Threadpool API's. This calls the OS's + // BindIoCompletionCallback method, and passes in a stub for the + // LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped + // struct for this request and gets a delegate to a managed callback + // from there, which it then calls on a threadpool thread. (We allocate + // our native OVERLAPPED structs 2 pointers too large and store EE + // state & a handle to a delegate there.) + // + // If, however, we've already bound this file handle to our completion port, + // don't try to bind it again because it will fail. A handle can only be + // bound to a single completion port at a time. + if (handle.IsAsync != true) + { + try + { + handle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(handle); + } + catch (Exception ex) + { + // If you passed in a synchronous handle and told us to use + // it asynchronously, throw here. + throw new ArgumentException(SR.Arg_HandleNotAsync, nameof(handle), ex); + } + } + } + + protected override void OnInit() + { + // This is necessary for async IO using IO Completion ports via our + // managed Threadpool API's. This (theoretically) calls the OS's + // BindIoCompletionCallback method, and passes in a stub for the + // LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped + // struct for this request and gets a delegate to a managed callback + // from there, which it then calls on a threadpool thread. (We allocate + // our native OVERLAPPED structs 2 pointers too large and store EE state + // & GC handles there, one to an IAsyncResult, the other to a delegate.) + try + { + _fileHandle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(_fileHandle); + } + catch (ArgumentException ex) + { + throw new IOException(SR.IO_BindHandleFailed, ex); + } + finally + { + if (_fileHandle.ThreadPoolBinding == null) + { + // We should close the handle so that the handle is not open until SafeFileHandle GC + Debug.Assert(!_exposedHandle, "Are we closing handle that we exposed/not own, how?"); + _fileHandle.Dispose(); + } + } + } + + // called by BufferedFileStreamStrategy + internal override void OnBufferAllocated(byte[] buffer) + { + Debug.Assert(_preallocatedOverlapped == null); + + _preallocatedOverlapped = new PreAllocatedOverlapped(FileStreamCompletionSource.s_ioCallback, this, buffer); + } + + SafeFileHandle IFileStreamCompletionSourceStrategy.FileHandle => _fileHandle; + + FileStreamCompletionSource? IFileStreamCompletionSourceStrategy.CurrentOverlappedOwner => _currentOverlappedOwner; + + FileStreamCompletionSource? IFileStreamCompletionSourceStrategy.CompareExchangeCurrentOverlappedOwner(FileStreamCompletionSource? newSource, FileStreamCompletionSource? existingSource) + => Interlocked.CompareExchange(ref _currentOverlappedOwner, newSource, existingSource); + + public override int Read(byte[] buffer, int offset, int count) + => ReadAsyncInternal(new Memory(buffer, offset, count)).GetAwaiter().GetResult(); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => ReadAsyncInternal(new Memory(buffer, offset, count), cancellationToken); + + public override ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) + => new ValueTask(ReadAsyncInternal(destination, cancellationToken)); + + private unsafe Task ReadAsyncInternal(Memory destination, CancellationToken cancellationToken = default) + { + if (!CanRead) + { + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); + } + + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + + // Create and store async stream class library specific data in the async result + FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, _preallocatedOverlapped, 0, destination); + NativeOverlapped* intOverlapped = completionSource.Overlapped; + + // Calculate position in the file we should be at after the read is done + if (CanSeek) + { + long len = Length; + + // Make sure we are reading from the position that we think we are + VerifyOSHandlePosition(); + + if (destination.Length > len - _filePosition) + { + if (_filePosition <= len) + { + destination = destination.Slice(0, (int)(len - _filePosition)); + } + else + { + destination = default; + } + } + + // Now set the position to read from in the NativeOverlapped struct + // For pipes, we should leave the offset fields set to 0. + intOverlapped->OffsetLow = unchecked((int)_filePosition); + intOverlapped->OffsetHigh = (int)(_filePosition >> 32); + + // When using overlapped IO, the OS is not supposed to + // touch the file pointer location at all. We will adjust it + // ourselves. This isn't threadsafe. + + // WriteFile should not update the file pointer when writing + // in overlapped mode, according to MSDN. But it does update + // the file pointer when writing to a UNC path! + // So changed the code below to seek to an absolute + // location, not a relative one. ReadFile seems consistent though. + SeekCore(_fileHandle, destination.Length, SeekOrigin.Current); + } + + // queue an async ReadFile operation and pass in a packed overlapped + int r = FileStreamHelpers.ReadFileNative(_fileHandle, destination.Span, intOverlapped, out int errorCode); + + // ReadFile, the OS version, will return 0 on failure. But + // my ReadFileNative wrapper returns -1. My wrapper will return + // the following: + // On error, r==-1. + // On async requests that are still pending, r==-1 w/ errorCode==ERROR_IO_PENDING + // on async requests that completed sequentially, r==0 + // You will NEVER RELIABLY be able to get the number of bytes + // read back from this call when using overlapped structures! You must + // not pass in a non-null lpNumBytesRead to ReadFile when using + // overlapped structures! This is by design NT behavior. + if (r == -1) + { + // For pipes, when they hit EOF, they will come here. + if (errorCode == ERROR_BROKEN_PIPE) + { + // Not an error, but EOF. AsyncFSCallback will NOT be + // called. Call the user callback here. + + // We clear the overlapped status bit for this special case. + // Failure to do so looks like we are freeing a pending overlapped later. + intOverlapped->InternalLow = IntPtr.Zero; + completionSource.SetCompletedSynchronously(0); + } + else if (errorCode != ERROR_IO_PENDING) + { + if (!_fileHandle.IsClosed && CanSeek) // Update Position - It could be anywhere. + { + SeekCore(_fileHandle, 0, SeekOrigin.Current); + } + + completionSource.ReleaseNativeResource(); + + if (errorCode == ERROR_HANDLE_EOF) + { + ThrowHelper.ThrowEndOfFileException(); + } + else + { + throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); + } + } + else if (cancellationToken.CanBeCanceled) // ERROR_IO_PENDING + { + // Only once the IO is pending do we register for cancellation + completionSource.RegisterForCancellation(cancellationToken); + } + } + else + { + // Due to a workaround for a race condition in NT's ReadFile & + // WriteFile routines, we will always be returning 0 from ReadFileNative + // when we do async IO instead of the number of bytes read, + // irregardless of whether the operation completed + // synchronously or asynchronously. We absolutely must not + // set asyncResult._numBytes here, since will never have correct + // results. + } + + return completionSource.Task; + } + + public override void Write(byte[] buffer, int offset, int count) + => WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), CancellationToken.None).AsTask().GetAwaiter().GetResult(); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + => WriteAsyncInternal(buffer, cancellationToken); + + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; // no buffering = nothing to flush + + private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationToken cancellationToken) + => new ValueTask(WriteAsyncInternalCore(source, cancellationToken)); + + private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, CancellationToken cancellationToken) + { + if (!CanWrite) + { + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); + } + + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + + // Create and store async stream class library specific data in the async result + FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, _preallocatedOverlapped, 0, source); + NativeOverlapped* intOverlapped = completionSource.Overlapped; + + if (CanSeek) + { + // Make sure we set the length of the file appropriately. + long len = Length; + + // Make sure we are writing to the position that we think we are + VerifyOSHandlePosition(); + + if (_filePosition + source.Length > len) + { + SetLengthCore(_filePosition + source.Length); + } + + // Now set the position to read from in the NativeOverlapped struct + // For pipes, we should leave the offset fields set to 0. + intOverlapped->OffsetLow = (int)_filePosition; + intOverlapped->OffsetHigh = (int)(_filePosition >> 32); + + // When using overlapped IO, the OS is not supposed to + // touch the file pointer location at all. We will adjust it + // ourselves. This isn't threadsafe. + SeekCore(_fileHandle, source.Length, SeekOrigin.Current); + } + + // queue an async WriteFile operation and pass in a packed overlapped + int r = FileStreamHelpers.WriteFileNative(_fileHandle, source.Span, intOverlapped, out int errorCode); + + // WriteFile, the OS version, will return 0 on failure. But + // my WriteFileNative wrapper returns -1. My wrapper will return + // the following: + // On error, r==-1. + // On async requests that are still pending, r==-1 w/ errorCode==ERROR_IO_PENDING + // On async requests that completed sequentially, r==0 + // You will NEVER RELIABLY be able to get the number of bytes + // written back from this call when using overlapped IO! You must + // not pass in a non-null lpNumBytesWritten to WriteFile when using + // overlapped structures! This is ByDesign NT behavior. + if (r == -1) + { + // For pipes, when they are closed on the other side, they will come here. + if (errorCode == ERROR_NO_DATA) + { + // Not an error, but EOF. AsyncFSCallback will NOT be called. + // Completing TCS and return cached task allowing the GC to collect TCS. + completionSource.SetCompletedSynchronously(0); + return Task.CompletedTask; + } + else if (errorCode != ERROR_IO_PENDING) + { + if (!_fileHandle.IsClosed && CanSeek) // Update Position - It could be anywhere. + { + SeekCore(_fileHandle, 0, SeekOrigin.Current); + } + + completionSource.ReleaseNativeResource(); + + if (errorCode == ERROR_HANDLE_EOF) + { + ThrowHelper.ThrowEndOfFileException(); + } + else + { + throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); + } + } + else if (cancellationToken.CanBeCanceled) // ERROR_IO_PENDING + { + // Only once the IO is pending do we register for cancellation + completionSource.RegisterForCancellation(cancellationToken); + } + } + else + { + // Due to a workaround for a race condition in NT's ReadFile & + // WriteFile routines, we will always be returning 0 from WriteFileNative + // when we do async IO instead of the number of bytes written, + // irregardless of whether the operation completed + // synchronously or asynchronously. We absolutely must not + // set asyncResult._numBytes here, since will never have correct + // results. + } + + return completionSource.Task; + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + ValidateCopyToArguments(destination, bufferSize); + + // Fail if the file was closed + if (_fileHandle.IsClosed) + { + ThrowHelper.ThrowObjectDisposedException_FileClosed(); + } + if (!CanRead) + { + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); + } + + // Bail early for cancellation if cancellation has been requested + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return AsyncModeCopyToAsync(destination, bufferSize, cancellationToken); + } + + private async Task AsyncModeCopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + Debug.Assert(CanRead, "_parent.CanRead"); + + bool canSeek = CanSeek; + if (canSeek) + { + VerifyOSHandlePosition(); + } + + try + { + await FileStreamHelpers + .AsyncModeCopyToAsync(_fileHandle, _path, 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); + } + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs index 2f1b8ba21d509..39532de1f3e11 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs @@ -114,7 +114,7 @@ private void ThrowIfDisposed() { if (_disposed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } } @@ -214,7 +214,7 @@ private byte InternalReadByte() int b = _stream.ReadByte(); if (b == -1) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } return (byte)b; @@ -229,7 +229,7 @@ public virtual char ReadChar() int value = Read(); if (value == -1) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } return (char)value; } @@ -296,7 +296,7 @@ public virtual string ReadString() n = _stream.Read(_charBytes, 0, readLength); if (n == 0) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } charsRead = _decoder.GetChars(_charBytes, 0, n, _charBuffer, 0); @@ -536,7 +536,7 @@ private ReadOnlySpan InternalRead(int numBytes) int n = _stream.Read(_buffer, bytesRead, numBytes - bytesRead); if (n == 0) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } bytesRead += n; } while (bytesRead < numBytes); @@ -569,7 +569,7 @@ protected virtual void FillBuffer(int numBytes) n = _stream.ReadByte(); if (n == -1) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } _buffer[0] = (byte)n; @@ -581,7 +581,7 @@ protected virtual void FillBuffer(int numBytes) n = _stream.Read(_buffer, bytesRead, numBytes - bytesRead); if (n == 0) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } bytesRead += n; } while (bytesRead < numBytes); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs new file mode 100644 index 0000000000000..eb802eb646efb --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedFileStreamStrategy.cs @@ -0,0 +1,1079 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + // this type exists so we can avoid duplicating the buffering logic in every FileStreamStrategy implementation + internal sealed class BufferedFileStreamStrategy : FileStreamStrategy + { + private readonly FileStreamStrategy _strategy; + private readonly int _bufferSize; + + private byte[]? _buffer; + private int _writePos; + private int _readPos; + private int _readLen; + // The last successful Task returned from ReadAsync (perf optimization for successive reads of the same size) + private Task? _lastSyncCompletedReadTask; + + internal BufferedFileStreamStrategy(FileStreamStrategy strategy, int bufferSize) + { + Debug.Assert(bufferSize > 1); + + _strategy = strategy; + _bufferSize = bufferSize; + } + + ~BufferedFileStreamStrategy() + { + try + { + // the finalizer must at least try to flush the write buffer + // so we enforce it by passing always true + Dispose(true); + } + catch (Exception e) when (FileStream.IsIoRelatedException(e)) + { + // On finalization, ignore failures from trying to flush the write buffer, + // e.g. if this stream is wrapping a pipe and the pipe is now broken. + } + } + + public override bool CanRead => _strategy.CanRead; + + public override bool CanWrite => _strategy.CanWrite; + + public override bool CanSeek => _strategy.CanSeek; + + public override long Length + { + get + { + long len = _strategy.Length; + + // If we're writing near the end of the file, we must include our + // internal buffer in our Length calculation. Don't flush because + // we use the length of the file in AsyncWindowsFileStreamStrategy.WriteAsync + if (_writePos > 0 && _strategy.Position + _writePos > len) + { + len = _writePos + _strategy.Position; + } + + return len; + } + } + + public override long Position + { + get + { + Debug.Assert(!(_writePos > 0 && _readPos != _readLen), "Read and Write buffers cannot both have data in them at the same time."); + + return _strategy.Position + (_readPos - _readLen + _writePos); + } + set + { + if (_writePos > 0) + { + FlushWrite(); + } + + _readPos = 0; + _readLen = 0; + + _strategy.Position = value; + } + } + + internal override bool IsAsync => _strategy.IsAsync; + + internal override bool IsClosed => _strategy.IsClosed; + + internal override string Name => _strategy.Name; + + internal override SafeFileHandle SafeFileHandle + { + get + { + // BufferedFileStreamStrategy must flush before the handle is exposed + // so whoever uses SafeFileHandle to access disk data can see + // the changes that were buffered in memory so far + Flush(); + + return _strategy.SafeFileHandle; + } + } + + public override async ValueTask DisposeAsync() + { + try + { + if (!_strategy.IsClosed) + { + try + { + await FlushAsync().ConfigureAwait(false); + } + finally + { + await _strategy.DisposeAsync().ConfigureAwait(false); + } + } + } + finally + { + // Don't set the buffer to null, to avoid a NullReferenceException + // when users have a race condition in their code (i.e. they call + // FileStream.Close when calling another method on FileStream like Read). + + _writePos = 0; // WriteByte hot path relies on this + } + } + + internal override void DisposeInternal(bool disposing) => Dispose(disposing); + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && !_strategy.IsClosed) + { + try + { + Flush(); + } + finally + { + _strategy.Dispose(); + } + } + } + finally + { + // Don't set the buffer to null, to avoid a NullReferenceException + // when users have a race condition in their code (i.e. they call + // FileStream.Close when calling another method on FileStream like Read). + + // Call base.Dispose(bool) to cleanup async IO resources + base.Dispose(disposing); + + _writePos = 0; + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + AssertBufferArguments(buffer, offset, count); + + return ReadSpan(new Span(buffer, offset, count), new ArraySegment(buffer, offset, count)); + } + + public override int Read(Span destination) + { + EnsureNotClosed(); + + return ReadSpan(destination, default); + } + + private int ReadSpan(Span destination, ArraySegment arraySegment) + { + Debug.Assert((_readPos == 0 && _readLen == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLen), + "We're either reading or writing, but not both."); + + bool isBlocked = false; + int n = _readLen - _readPos; + // if the read buffer is empty, read into either user's array or our + // buffer, depending on number of bytes user asked for and buffer size. + if (n == 0) + { + EnsureCanRead(); + + if (_writePos > 0) + { + FlushWrite(); + } + + if (!_strategy.CanSeek || (destination.Length >= _bufferSize)) + { + // For async file stream strategies the call to Read(Span) is translated to Stream.Read(Span), + // which rents an array from the pool, copies the data, and then calls Read(Array). This is expensive! + // To avoid that (and code duplication), the Read(Array) method passes ArraySegment to this method + // which allows for calling Strategy.Read(Array) instead of Strategy.Read(Span). + n = arraySegment.Array != null + ? _strategy.Read(arraySegment.Array, arraySegment.Offset, arraySegment.Count) + : _strategy.Read(destination); + + // Throw away read buffer. + _readPos = 0; + _readLen = 0; + return n; + } + + EnsureBufferAllocated(); + n = _strategy.Read(_buffer!, 0, _bufferSize); + + if (n == 0) + { + return 0; + } + + isBlocked = n < _bufferSize; + _readPos = 0; + _readLen = n; + } + // Now copy min of count or numBytesAvailable (i.e. near EOF) to array. + if (n > destination.Length) + { + n = destination.Length; + } + new ReadOnlySpan(_buffer!, _readPos, n).CopyTo(destination); + _readPos += n; + + // We may have read less than the number of bytes the user asked + // for, but that is part of the Stream contract. Reading again for + // more data may cause us to block if we're using a device with + // no clear end of file, such as a serial port or pipe. If we + // blocked here & this code was used with redirected pipes for a + // process's standard output, this can lead to deadlocks involving + // two processes. But leave this here for files to avoid what would + // probably be a breaking change. -- + + // If we are reading from a device with no clear EOF like a + // serial port or a pipe, this will cause us to block incorrectly. + if (!_strategy.IsPipe) + { + // If we hit the end of the buffer and didn't have enough bytes, we must + // read some more from the underlying stream. However, if we got + // fewer bytes from the underlying stream than we asked for (i.e. we're + // probably blocked), don't ask for more bytes. + if (n < destination.Length && !isBlocked) + { + Debug.Assert(_readPos == _readLen, "Read buffer should be empty!"); + + int moreBytesRead = arraySegment.Array != null + ? _strategy.Read(arraySegment.Array, arraySegment.Offset + n, arraySegment.Count - n) + : _strategy.Read(destination.Slice(n)); + + n += moreBytesRead; + // We've just made our buffer inconsistent with our position + // pointer. We must throw away the read buffer. + _readPos = 0; + _readLen = 0; + } + } + + return n; + } + + public override int ReadByte() => _readPos != _readLen ? _buffer![_readPos++] : ReadByteSlow(); + + private int ReadByteSlow() + { + Debug.Assert(_readPos == _readLen); + + // We want to check for whether the underlying stream has been closed and whether + // it's readable, but we only need to do so if we don't have data in our buffer, + // as any data we have came from reading it from an open stream, and we don't + // care if the stream has been closed or become unreadable since. Further, if + // the stream is closed, its read buffer is flushed, so we'll take this slow path. + EnsureNotClosed(); + EnsureCanRead(); + + if (_writePos > 0) + { + FlushWrite(); + } + + EnsureBufferAllocated(); + _readLen = _strategy.Read(_buffer!, 0, _bufferSize); + _readPos = 0; + + if (_readLen == 0) + { + return -1; + } + + return _buffer![_readPos++]; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + AssertBufferArguments(buffer, offset, count); + + ValueTask readResult = ReadAsync(new Memory(buffer, offset, count), cancellationToken); + + return readResult.IsCompletedSuccessfully + ? LastSyncCompletedReadTask(readResult.Result) + : readResult.AsTask(); + + Task LastSyncCompletedReadTask(int val) + { + Task? t = _lastSyncCompletedReadTask; + Debug.Assert(t == null || t.IsCompletedSuccessfully); + + if (t != null && t.Result == val) + return t; + + t = Task.FromResult(val); + _lastSyncCompletedReadTask = t; + return t; + } + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + EnsureCanRead(); + + Debug.Assert(!_strategy.IsClosed, "FileStream ensures that strategy is not closed"); + Debug.Assert((_readPos == 0 && _readLen == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLen), + "We're either reading or writing, but not both."); + + if (_strategy.IsPipe) // pipes have a very limited support for buffering + { + return ReadFromPipeAsync(buffer, cancellationToken); + } + + SemaphoreSlim semaphore = EnsureAsyncActiveSemaphoreInitialized(); + Task semaphoreLockTask = semaphore.WaitAsync(cancellationToken); + + if (semaphoreLockTask.IsCompletedSuccessfully // lock has been acquired + && _writePos == 0) // there is nothing to flush + { + bool releaseTheLock = true; + try + { + if (_readLen - _readPos >= buffer.Length) + { + // hot path #1: there is enough data in the buffer + _buffer.AsSpan(_readPos, buffer.Length).CopyTo(buffer.Span); + _readPos += buffer.Length; + return new ValueTask(buffer.Length); + } + else if (_readLen == _readPos && buffer.Length >= _bufferSize) + { + // hot path #2: the read buffer is empty and buffering would not be beneficial + return _strategy.ReadAsync(buffer, cancellationToken); + } + + releaseTheLock = false; + } + finally + { + if (releaseTheLock) + { + semaphore.Release(); + } + // the code is going to call ReadAsyncSlowPath which is going to release the lock + } + } + + return ReadAsyncSlowPath(semaphoreLockTask, buffer, cancellationToken); + } + + private async ValueTask ReadFromPipeAsync(Memory destination, CancellationToken cancellationToken) + { + Debug.Assert(_strategy.IsPipe); + + // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. + await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + // Pipes are tricky, at least when you have 2 different pipes + // that you want to use simultaneously. When redirecting stdout + // & stderr with the Process class, it's easy to deadlock your + // parent & child processes when doing writes 4K at a time. The + // OS appears to use a 4K buffer internally. If you write to a + // pipe that is full, you will block until someone read from + // that pipe. If you try reading from an empty pipe and + // Win32FileStream's ReadAsync blocks waiting for data to fill it's + // internal buffer, you will be blocked. In a case where a child + // process writes to stdout & stderr while a parent process tries + // reading from both, you can easily get into a deadlock here. + // To avoid this deadlock, don't buffer when doing async IO on + // pipes. But don't completely ignore buffered data either. + if (_readPos < _readLen) + { + int n = Math.Min(_readLen - _readPos, destination.Length); + new Span(_buffer!, _readPos, n).CopyTo(destination.Span); + _readPos += n; + return n; + } + else + { + Debug.Assert(_writePos == 0, "Win32FileStream must not have buffered write data here! Pipes should be unidirectional."); + return await _strategy.ReadAsync(destination, cancellationToken).ConfigureAwait(false); + } + } + finally + { + _asyncActiveSemaphore.Release(); + } + } + + private async ValueTask ReadAsyncSlowPath(Task semaphoreLockTask, Memory buffer, CancellationToken cancellationToken) + { + Debug.Assert(_asyncActiveSemaphore != null); + Debug.Assert(!_strategy.IsPipe); + + // Employ async waiting based on the same synchronization used in BeginRead of the abstract Stream. + await semaphoreLockTask.ConfigureAwait(false); + try + { + int bytesFromBuffer = 0; + int bytesAlreadySatisfied = 0; + + if (_readLen - _readPos > 0) + { + // The buffer might have been changed by another async task while we were waiting on the semaphore. + // Check it now again. + bytesFromBuffer = Math.Min(buffer.Length, _readLen - _readPos); + + if (bytesFromBuffer > 0) // don't try to copy 0 bytes + { + _buffer.AsSpan(_readPos, bytesFromBuffer).CopyTo(buffer.Span); + _readPos += bytesFromBuffer; + } + + if (bytesFromBuffer == buffer.Length) + { + return bytesFromBuffer; + } + + if (bytesFromBuffer > 0) + { + buffer = buffer.Slice(bytesFromBuffer); + bytesAlreadySatisfied += bytesFromBuffer; + } + } + + Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); + _readPos = _readLen = 0; + + // If there was anything in the write buffer, clear it. + if (_writePos > 0) + { + await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); + _writePos = 0; + } + + // If the requested read is larger than buffer size, avoid the buffer and still use a single read: + if (buffer.Length >= _bufferSize) + { + return bytesAlreadySatisfied + await _strategy.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } + + // Ok. We can fill the buffer: + EnsureBufferAllocated(); + _readLen = await _strategy.ReadAsync(new Memory(_buffer, 0, _bufferSize), cancellationToken).ConfigureAwait(false); + + bytesFromBuffer = Math.Min(_readLen, buffer.Length); + _buffer.AsSpan(0, bytesFromBuffer).CopyTo(buffer.Span); + _readPos += bytesFromBuffer; + return bytesAlreadySatisfied + bytesFromBuffer; + } + finally + { + _asyncActiveSemaphore.Release(); + } + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => TaskToApm.Begin(ReadAsync(buffer, offset, count, CancellationToken.None), callback, state); + + public override int EndRead(IAsyncResult asyncResult) + => TaskToApm.End(asyncResult); + + public override void Write(byte[] buffer, int offset, int count) + { + AssertBufferArguments(buffer, offset, count); + + WriteSpan(new ReadOnlySpan(buffer, offset, count), new ArraySegment(buffer, offset, count)); + } + + public override void Write(ReadOnlySpan buffer) + { + EnsureNotClosed(); + + WriteSpan(buffer, default); + } + + private void WriteSpan(ReadOnlySpan source, ArraySegment arraySegment) + { + if (_writePos == 0) + { + EnsureCanWrite(); + ClearReadBufferBeforeWrite(); + } + + // 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 = _bufferSize - _writePos; // space left in buffer + if (numBytes > 0) + { + if (numBytes >= source.Length) + { + source.CopyTo(_buffer!.AsSpan(_writePos)); + _writePos += source.Length; + return; + } + else + { + source.Slice(0, numBytes).CopyTo(_buffer!.AsSpan(_writePos)); + _writePos += numBytes; + source = source.Slice(numBytes); + } + } + + FlushWrite(); + Debug.Assert(_writePos == 0, "FlushWrite must set _writePos to 0"); + } + + // If the buffer would slow _bufferSize down, avoid buffer completely. + if (source.Length >= _bufferSize) + { + Debug.Assert(_writePos == 0, "FileStream cannot have buffered data to write here! Your stream will be corrupted."); + + // For async file stream strategies the call to Write(Span) is translated to Stream.Write(Span), + // which rents an array from the pool, copies the data, and then calls Write(Array). This is expensive! + // To avoid that (and code duplication), the Write(Array) method passes ArraySegment to this method + // which allows for calling Strategy.Write(Array) instead of Strategy.Write(Span). + if (arraySegment.Array != null) + { + _strategy.Write(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + } + else + { + _strategy.Write(source); + } + + return; + } + else if (source.Length == 0) + { + return; // Don't allocate a buffer then call memcpy for 0 bytes. + } + + // Copy remaining bytes into buffer, to write at a later date. + EnsureBufferAllocated(); + source.CopyTo(_buffer!.AsSpan(_writePos)); + _writePos = source.Length; + } + + public override void WriteByte(byte value) + { + if (_writePos > 0 && _writePos < _bufferSize - 1) + { + _buffer![_writePos++] = value; + } + else + { + WriteByteSlow(value); + } + } + + private void WriteByteSlow(byte value) + { + if (_writePos == 0) + { + EnsureNotClosed(); + EnsureCanWrite(); + ClearReadBufferBeforeWrite(); + EnsureBufferAllocated(); + } + else if (_writePos >= _bufferSize - 1) + { + FlushWrite(); + } + + _buffer![_writePos++] = value; + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + AssertBufferArguments(buffer, offset, count); + + return WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + EnsureCanWrite(); + + Debug.Assert(!_strategy.IsClosed, "FileStream ensures that strategy is not closed"); + Debug.Assert((_readPos == 0 && _readLen == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLen), + "We're either reading or writing, but not both."); + Debug.Assert(!_strategy.IsPipe || (_readPos == 0 && _readLen == 0), + "Win32FileStream must not have buffered data here! Pipes should be unidirectional."); + + if (_strategy.IsPipe) + { + // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadFromPipeAsync) + return WriteToPipeAsync(buffer, cancellationToken); + } + + SemaphoreSlim semaphore = EnsureAsyncActiveSemaphoreInitialized(); + Task semaphoreLockTask = semaphore.WaitAsync(cancellationToken); + + if (semaphoreLockTask.IsCompletedSuccessfully // lock has been acquired + && _readPos == _readLen) // there is nothing to flush + { + bool releaseTheLock = true; + try + { + // hot path #1 if the write completely fits into the buffer, we can complete synchronously: + if (_bufferSize - _writePos >= buffer.Length) + { + EnsureBufferAllocated(); + buffer.Span.CopyTo(_buffer.AsSpan(_writePos)); + _writePos += buffer.Length; + return default; + } + else if (_writePos == 0 && buffer.Length >= _bufferSize) + { + // hot path #2: the write buffer is empty and buffering would not be beneficial + return _strategy.WriteAsync(buffer, cancellationToken); + } + + releaseTheLock = false; + } + finally + { + if (releaseTheLock) + { + semaphore.Release(); + } + // the code is going to call ReadAsyncSlowPath which is going to release the lock + } + } + + return WriteAsyncSlowPath(semaphoreLockTask, buffer, cancellationToken); + } + + private async ValueTask WriteToPipeAsync(ReadOnlyMemory source, CancellationToken cancellationToken) + { + Debug.Assert(_strategy.IsPipe); + + await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + await _strategy.WriteAsync(source, cancellationToken).ConfigureAwait(false); + } + finally + { + _asyncActiveSemaphore.Release(); + } + } + + private async ValueTask WriteAsyncSlowPath(Task semaphoreLockTask, ReadOnlyMemory source, CancellationToken cancellationToken) + { + Debug.Assert(_asyncActiveSemaphore != null); + Debug.Assert(!_strategy.IsPipe); + + await semaphoreLockTask.ConfigureAwait(false); + try + { + if (_writePos == 0) + { + ClearReadBufferBeforeWrite(); + } + + // 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 spaceLeft = _bufferSize - _writePos; + if (spaceLeft > 0) + { + if (spaceLeft >= source.Length) + { + source.Span.CopyTo(_buffer!.AsSpan(_writePos)); + _writePos += source.Length; + return; + } + else + { + source.Span.Slice(0, spaceLeft).CopyTo(_buffer!.AsSpan(_writePos)); + _writePos += spaceLeft; + source = source.Slice(spaceLeft); + } + } + + await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); + _writePos = 0; + } + + // If the buffer would slow _bufferSize down, avoid buffer completely. + if (source.Length >= _bufferSize) + { + Debug.Assert(_writePos == 0, "FileStream cannot have buffered data to write here! Your stream will be corrupted."); + await _strategy.WriteAsync(source, cancellationToken).ConfigureAwait(false); + return; + } + else if (source.Length == 0) + { + return; // Don't allocate a buffer then call memcpy for 0 bytes. + } + + // Copy remaining bytes into buffer, to write at a later date. + EnsureBufferAllocated(); + source.Span.CopyTo(_buffer!.AsSpan(_writePos)); + _writePos = source.Length; + } + finally + { + _asyncActiveSemaphore.Release(); + } + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => TaskToApm.Begin(WriteAsync(buffer, offset, count, CancellationToken.None), callback, state); + + public override void EndWrite(IAsyncResult asyncResult) + => TaskToApm.End(asyncResult); + + public override void SetLength(long value) + { + Flush(); + + _strategy.SetLength(value); + } + + public override void Flush() => Flush(flushToDisk: false); + + internal override void Flush(bool flushToDisk) + { + EnsureNotClosed(); + + // Has write data in the buffer: + if (_writePos > 0) + { + // EnsureNotClosed does not guarantee that the Stream has not been closed + // an example could be a call to fileStream.SafeFileHandle.Dispose() + // so to avoid getting exception here, we just ensure that we can Write before doing it + if (_strategy.CanWrite) + { + FlushWrite(); + Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); + return; + } + } + + // Has read data in the buffer: + if (_readPos < _readLen) + { + // If the underlying strategy is not seekable AND we have something in the read buffer, then FlushRead would throw. + // We can either throw away the buffer resulting in data loss (!) or ignore the Flush. + // (We cannot throw because it would be a breaking change.) We opt into ignoring the Flush in that situation. + if (_strategy.CanSeek) + { + FlushRead(); + } + + // If the Stream was seekable, then we should have called FlushRead which resets _readPos & _readLen. + Debug.Assert(_writePos == 0 && (!_strategy.CanSeek || (_readPos == 0 && _readLen == 0))); + return; + } + + // We had no data in the buffer, but we still need to tell the underlying strategy to flush. + _strategy.Flush(flushToDisk); + + _writePos = _readPos = _readLen = 0; + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + EnsureNotClosed(); + + return FlushAsyncInternal(cancellationToken); + } + + private async Task FlushAsyncInternal(CancellationToken cancellationToken) + { + await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (_writePos > 0) + { + await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); + _writePos = 0; + Debug.Assert(_writePos == 0 && _readPos == 0 && _readLen == 0); + return; + } + + if (_readPos < _readLen) + { + // If the underlying strategy is not seekable AND we have something in the read buffer, then FlushRead would throw. + // We can either throw away the buffer resulting in date loss (!) or ignore the Flush. (We cannot throw because it + // would be a breaking change.) We opt into ignoring the Flush in that situation. + if (_strategy.CanSeek) + { + FlushRead(); // not async; it uses Seek, but there's no SeekAsync + } + + // If the Strategy was seekable, then we should have called FlushRead which resets _readPos & _readLen. + Debug.Assert(_writePos == 0 && (!_strategy.CanSeek || (_readPos == 0 && _readLen == 0))); + return; + } + + // There was nothing in the buffer: + Debug.Assert(_writePos == 0 && _readPos == _readLen); + } + finally + { + _asyncActiveSemaphore.Release(); + } + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + EnsureCanRead(); + + return cancellationToken.IsCancellationRequested ? + Task.FromCanceled(cancellationToken) : + CopyToAsyncCore(destination, bufferSize, cancellationToken); + } + + private async Task CopyToAsyncCore(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + // Synchronize async operations as does Read/WriteAsync. + await EnsureAsyncActiveSemaphoreInitialized().WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + int readBytes = _readLen - _readPos; + Debug.Assert(readBytes >= 0, $"Expected a non-negative number of bytes in buffer, got {readBytes}"); + + if (readBytes > 0) + { + // If there's any read data in the buffer, write it all to the destination stream. + Debug.Assert(_writePos == 0, "Write buffer must be empty if there's data in the read buffer"); + await destination.WriteAsync(new ReadOnlyMemory(_buffer, _readPos, readBytes), cancellationToken).ConfigureAwait(false); + _readPos = _readLen = 0; + } + else if (_writePos > 0) + { + // If there's write data in the buffer, flush it back to the underlying stream, as does ReadAsync. + await _strategy.WriteAsync(new ReadOnlyMemory(_buffer, 0, _writePos), cancellationToken).ConfigureAwait(false); + _writePos = 0; + } + + // Our buffer is now clear. Copy data directly from the source stream to the destination stream. + await _strategy.CopyToAsync(destination, bufferSize, cancellationToken).ConfigureAwait(false); + } + finally + { + _asyncActiveSemaphore.Release(); + } + } + + public override void CopyTo(Stream destination, int bufferSize) + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + EnsureCanRead(); + + int readBytes = _readLen - _readPos; + Debug.Assert(readBytes >= 0, $"Expected a non-negative number of bytes in buffer, got {readBytes}"); + + if (readBytes > 0) + { + // If there's any read data in the buffer, write it all to the destination stream. + Debug.Assert(_writePos == 0, "Write buffer must be empty if there's data in the read buffer"); + destination.Write(_buffer!, _readPos, readBytes); + _readPos = _readLen = 0; + } + else if (_writePos > 0) + { + // If there's write data in the buffer, flush it back to the underlying stream, as does ReadAsync. + FlushWrite(); + } + + // Our buffer is now clear. Copy data directly from the source stream to the destination stream. + _strategy.CopyTo(destination, bufferSize); + } + + public override long Seek(long offset, SeekOrigin origin) + { + EnsureNotClosed(); + EnsureCanSeek(); + + // If we have bytes in the write buffer, flush them out, seek and be done. + if (_writePos > 0) + { + FlushWrite(); + return _strategy.Seek(offset, origin); + } + + // The buffer is either empty or we have a buffered read. + if (_readLen - _readPos > 0 && origin == SeekOrigin.Current) + { + // If we have bytes in the read buffer, adjust the seek offset to account for the resulting difference + // between this stream's position and the underlying stream's position. + offset -= (_readLen - _readPos); + } + + long oldPos = Position; + Debug.Assert(oldPos == _strategy.Position + (_readPos - _readLen)); + + long newPos = _strategy.Seek(offset, origin); + + // If the seek destination is still within the data currently in the buffer, we want to keep the buffer data and continue using it. + // Otherwise we will throw away the buffer. This can only happen on read, as we flushed write data above. + + // The offset of the new/updated seek pointer within _buffer: + _readPos = (int)(newPos - (oldPos - _readPos)); + + // If the offset of the updated seek pointer in the buffer is still legal, then we can keep using the buffer: + if (0 <= _readPos && _readPos < _readLen) + { + // Adjust the seek pointer of the underlying stream to reflect the amount of useful bytes in the read buffer: + _strategy.Seek(_readLen - _readPos, SeekOrigin.Current); + } + else + { // The offset of the updated seek pointer is not a legal offset. Loose the buffer. + _readPos = _readLen = 0; + } + + Debug.Assert(newPos == Position, "newPos (=" + newPos + ") == Position (=" + Position + ")"); + return newPos; + } + + internal override void Lock(long position, long length) => _strategy.Lock(position, length); + + internal override void Unlock(long position, long length) => _strategy.Unlock(position, length); + + // Reading is done in blocks, but someone could read 1 byte from the buffer then write. + // At that point, the underlying stream's pointer is out of sync with this stream's position. + // All write functions should call this function to ensure that the buffered data is not lost. + private void FlushRead() + { + Debug.Assert(_writePos == 0, "Write buffer must be empty in FlushRead!"); + + if (_readPos - _readLen != 0) + { + _strategy.Seek(_readPos - _readLen, SeekOrigin.Current); + } + + _readPos = 0; + _readLen = 0; + } + + private void FlushWrite() + { + Debug.Assert(_readPos == 0 && _readLen == 0, "Read buffer must be empty in FlushWrite!"); + Debug.Assert(_buffer != null && _bufferSize >= _writePos, "Write buffer must be allocated and write position must be in the bounds of the buffer in FlushWrite!"); + + _strategy.Write(_buffer, 0, _writePos); + _writePos = 0; + } + + /// + /// Called by Write methods to clear the Read Buffer + /// + private void ClearReadBufferBeforeWrite() + { + Debug.Assert(_readPos <= _readLen, "_readPos <= _readLen [" + _readPos + " <= " + _readLen + "]"); + + // No read data in the buffer: + if (_readPos == _readLen) + { + _readPos = _readLen = 0; + return; + } + + // Must have read data. + Debug.Assert(_readPos < _readLen); + FlushRead(); + } + + private void EnsureNotClosed() + { + if (_strategy.IsClosed) + { + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); + } + } + + private void EnsureCanSeek() + { + if (!_strategy.CanSeek) + { + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); + } + } + + private void EnsureCanRead() + { + if (!_strategy.CanRead) + { + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); + } + } + + private void EnsureCanWrite() + { + if (!_strategy.CanWrite) + { + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); + } + } + + private void EnsureBufferAllocated() + { + Debug.Assert(_bufferSize > 0); + + // BufferedFileStreamStrategy is not intended for multi-threaded use, so no worries about the get/set race on _buffer. + if (_buffer == null) + { + AllocateBuffer(); + } + + void AllocateBuffer() // logic kept in a separate method to get EnsureBufferAllocated() inlined + { + _strategy.OnBufferAllocated(_buffer = new byte[_bufferSize]); + } + } + + [Conditional("DEBUG")] + private void AssertBufferArguments(byte[] buffer, int offset, int count) + { + ValidateBufferArguments(buffer, offset, count); // FileStream is supposed to call this + Debug.Assert(!_strategy.IsClosed, "FileStream ensures that strategy is not closed"); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs index bc1ea76470377..f6da2fd101d1c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs @@ -67,7 +67,7 @@ public BufferedStream(Stream stream) public BufferedStream(Stream stream, int bufferSize) { if (stream == null) - throw new ArgumentNullException(nameof(stream)); + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stream); if (bufferSize <= 0) throw new ArgumentOutOfRangeException(nameof(bufferSize), SR.Format(SR.ArgumentOutOfRange_MustBePositive, nameof(bufferSize))); @@ -79,13 +79,13 @@ public BufferedStream(Stream stream, int bufferSize) // & writes are greater than or equal to buffer size. if (!_stream.CanRead && !_stream.CanWrite) - throw new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); } private void EnsureNotClosed() { if (_stream == null) - throw new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); } private void EnsureCanSeek() @@ -93,7 +93,7 @@ private void EnsureCanSeek() Debug.Assert(_stream != null); if (!_stream.CanSeek) - throw new NotSupportedException(SR.NotSupported_UnseekableStream); + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } private void EnsureCanRead() @@ -101,7 +101,7 @@ private void EnsureCanRead() Debug.Assert(_stream != null); if (!_stream.CanRead) - throw new NotSupportedException(SR.NotSupported_UnreadableStream); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } private void EnsureCanWrite() @@ -109,7 +109,7 @@ private void EnsureCanWrite() Debug.Assert(_stream != null); if (!_stream.CanWrite) - throw new NotSupportedException(SR.NotSupported_UnwritableStream); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } private void EnsureShadowBufferAllocated() @@ -203,7 +203,7 @@ public override long Position set { if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); EnsureNotClosed(); EnsureCanSeek(); @@ -237,6 +237,7 @@ protected override void Dispose(bool disposing) { _stream = null; _buffer = null; + _writePos = 0; // WriteByte hot path relies on this // Call base.Dispose(bool) to cleanup async IO resources base.Dispose(disposing); @@ -263,6 +264,7 @@ public override async ValueTask DisposeAsync() { _stream = null; _buffer = null; + _writePos = 0; } } @@ -503,8 +505,7 @@ public override int Read(byte[] buffer, int offset, int count) offset += bytesFromBuffer; } - // So the read buffer is empty. - Debug.Assert(_readLen == _readPos); + Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); _readPos = _readLen = 0; // If there was anything in the write buffer, clear it. @@ -554,8 +555,7 @@ public override int Read(Span destination) destination = destination.Slice(bytesFromBuffer); } - // The read buffer must now be empty. - Debug.Assert(_readLen == _readPos); + Debug.Assert(_readLen == _readPos, "The read buffer must now be empty"); _readPos = _readLen = 0; // If there was anything in the write buffer, clear it. @@ -1164,6 +1164,18 @@ public override void EndWrite(IAsyncResult asyncResult) => TaskToApm.End(asyncResult); public override void WriteByte(byte value) + { + if (_writePos > 0 && _writePos < _bufferSize - 1) + { + _buffer![_writePos++] = value; + } + else + { + WriteByteSlow(value); + } + } + + private void WriteByteSlow(byte value) { EnsureNotClosed(); @@ -1236,7 +1248,7 @@ public override long Seek(long offset, SeekOrigin origin) public override void SetLength(long value) { if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); EnsureNotClosed(); EnsureCanSeek(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/DerivedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/DerivedFileStreamStrategy.cs index 76d4441309f7b..e7dd824305f49 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/DerivedFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/DerivedFileStreamStrategy.cs @@ -15,8 +15,21 @@ namespace System.IO internal sealed class DerivedFileStreamStrategy : FileStreamStrategy { private readonly FileStreamStrategy _strategy; + private readonly FileStream _fileStream; - internal DerivedFileStreamStrategy(FileStream fileStream, FileStreamStrategy strategy) : base(fileStream) => _strategy = strategy; + internal DerivedFileStreamStrategy(FileStream fileStream, FileStreamStrategy strategy) + { + _fileStream = fileStream; + _strategy = strategy; + } + + ~DerivedFileStreamStrategy() + { + // Preserved for compatibility since FileStream has defined a + // finalizer in past releases and derived classes may depend + // on Dispose(false) call. + _fileStream.DisposeInternal(false); + } public override bool CanRead => _strategy.CanRead; @@ -36,7 +49,14 @@ public override long Position internal override string Name => _strategy.Name; - internal override SafeFileHandle SafeFileHandle => _strategy.SafeFileHandle; + internal override SafeFileHandle SafeFileHandle + { + get + { + _fileStream.Flush(false); + return _strategy.SafeFileHandle; + } + } internal override bool IsClosed => _strategy.IsClosed; @@ -136,6 +156,14 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio public override ValueTask DisposeAsync() => _fileStream.BaseDisposeAsync(); - internal override void DisposeInternal(bool disposing) => _strategy.DisposeInternal(disposing); + internal override void DisposeInternal(bool disposing) + { + _strategy.DisposeInternal(disposing); + + if (disposing) + { + GC.SuppressFinalize(this); + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Error.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Error.cs deleted file mode 100644 index d2e483433a872..0000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Error.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.IO -{ - /// - /// Provides centralized methods for creating exceptions for System.IO.FileSystem. - /// - internal static class Error - { - internal static Exception GetStreamIsClosed() - { - return new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); - } - - internal static Exception GetEndOfFile() - { - return new EndOfStreamException(SR.IO_EOF_ReadBeyondEOF); - } - - internal static Exception GetFileNotOpen() - { - return new ObjectDisposedException(null, SR.ObjectDisposed_FileClosed); - } - - internal static Exception GetReadNotSupported() - { - return new NotSupportedException(SR.NotSupported_UnreadableStream); - } - - internal static Exception GetSeekNotSupported() - { - return new NotSupportedException(SR.NotSupported_UnseekableStream); - } - - internal static Exception GetWriteNotSupported() - { - return new NotSupportedException(SR.NotSupported_UnwritableStream); - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs index 078b0bd0a4186..20e5f73bb0efd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -46,7 +46,7 @@ public FileStream(IntPtr handle, FileAccess access, bool ownsHandle, int bufferS { ValidateHandle(safeHandle, access, bufferSize, isAsync); - _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(this, safeHandle, access, bufferSize, isAsync)); + _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(safeHandle, access, bufferSize, isAsync)); } catch { @@ -73,7 +73,7 @@ private static void ValidateHandle(SafeFileHandle handle, FileAccess access, int throw new ArgumentOutOfRangeException(nameof(bufferSize), SR.ArgumentOutOfRange_NeedPosNum); if (handle.IsClosed) - throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); if (handle.IsAsync.HasValue && isAsync != handle.IsAsync.GetValueOrDefault()) throw new ArgumentException(SR.Arg_HandleNotAsync, nameof(handle)); } @@ -95,7 +95,7 @@ public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize, bool { ValidateHandle(handle, access, bufferSize, isAsync); - _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(this, handle, access, bufferSize, isAsync)); + _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(handle, access, bufferSize, isAsync)); } public FileStream(string path, FileMode mode) : @@ -164,7 +164,7 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share SerializationInfo.ThrowIfDeserializationInProgress("AllowFileWrites", ref s_cachedSerializationSwitch); } - _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(this, path, mode, access, share, bufferSize, options)); + _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(path, mode, access, share, bufferSize, options)); } [Obsolete("This property has been deprecated. Please use FileStream's SafeFileHandle property instead. https://go.microsoft.com/fwlink/?linkid=14202")] @@ -180,7 +180,7 @@ public virtual void Lock(long position, long length) if (_strategy.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } _strategy.Lock(position, length); @@ -196,7 +196,7 @@ public virtual void Unlock(long position, long length) if (_strategy.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } _strategy.Unlock(position, length); @@ -210,7 +210,7 @@ public override Task FlushAsync(CancellationToken cancellationToken) } if (_strategy.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } return _strategy.FlushAsync(cancellationToken); @@ -233,7 +233,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel return Task.FromCanceled(cancellationToken); if (_strategy.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); return _strategy.ReadAsync(buffer, offset, count, cancellationToken); } @@ -247,7 +247,7 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken if (_strategy.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } return _strategy.ReadAsync(buffer, cancellationToken); @@ -270,7 +270,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati return Task.FromCanceled(cancellationToken); if (_strategy.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); return _strategy.WriteAsync(buffer, offset, count, cancellationToken); } @@ -284,7 +284,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo if (_strategy.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } return _strategy.WriteAsync(buffer, cancellationToken); @@ -305,7 +305,7 @@ public override void Flush() /// public virtual void Flush(bool flushToDisk) { - if (_strategy.IsClosed) throw Error.GetFileNotOpen(); + if (_strategy.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); _strategy.Flush(flushToDisk); } @@ -324,7 +324,7 @@ private void ValidateReadWriteArgs(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); if (_strategy.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } /// Sets the length of this stream to the given value. @@ -332,13 +332,13 @@ private void ValidateReadWriteArgs(byte[] buffer, int offset, int count) public override void SetLength(long value) { if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); if (_strategy.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); if (!CanSeek) - throw Error.GetSeekNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); if (!CanWrite) - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); _strategy.SetLength(value); } @@ -356,8 +356,8 @@ public override long Length { get { - if (_strategy.IsClosed) throw Error.GetFileNotOpen(); - if (!CanSeek) throw Error.GetSeekNotSupported(); + if (_strategy.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); + if (!CanSeek) ThrowHelper.ThrowNotSupportedException_UnseekableStream(); return _strategy.Length; } } @@ -368,17 +368,17 @@ public override long Position get { if (_strategy.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); if (!CanSeek) - throw Error.GetSeekNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); return _strategy.Position; } set { if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); _strategy.Seek(value, SeekOrigin.Begin); } @@ -397,29 +397,22 @@ public override long Position /// The byte to write to the stream. public override void WriteByte(byte value) => _strategy.WriteByte(value); - ~FileStream() - { - // Preserved for compatibility since FileStream has defined a - // finalizer in past releases and derived classes may depend - // on Dispose(false) call. - Dispose(false); - } + protected override void Dispose(bool disposing) => _strategy?.DisposeInternal(disposing); - protected override void Dispose(bool disposing) - { - _strategy?.DisposeInternal(disposing); // null _strategy possible in finalizer - } + internal void DisposeInternal(bool disposing) => Dispose(disposing); public override ValueTask DisposeAsync() => _strategy.DisposeAsync(); + public override void CopyTo(Stream destination, int bufferSize) => _strategy.CopyTo(destination, bufferSize); + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => _strategy.CopyToAsync(destination, bufferSize, cancellationToken); public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { ValidateBufferArguments(buffer, offset, count); - if (_strategy.IsClosed) throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed); - if (!CanRead) throw new NotSupportedException(SR.NotSupported_UnreadableStream); + if (_strategy.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); + if (!CanRead) ThrowHelper.ThrowNotSupportedException_UnreadableStream(); return _strategy.BeginRead(buffer, offset, count, callback, state); } @@ -435,8 +428,8 @@ public override int EndRead(IAsyncResult asyncResult) public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { ValidateBufferArguments(buffer, offset, count); - if (_strategy.IsClosed) throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed); - if (!CanWrite) throw new NotSupportedException(SR.NotSupported_UnwritableStream); + if (_strategy.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); + if (!CanWrite) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); return _strategy.BeginWrite(buffer, offset, count, callback, state); } @@ -486,5 +479,21 @@ internal IAsyncResult BaseBeginWrite(byte[] buffer, int offset, int count, Async => base.BeginWrite(buffer, offset, count, callback, state); internal void BaseEndWrite(IAsyncResult asyncResult) => base.EndWrite(asyncResult); + + internal static bool IsIoRelatedException(Exception e) => + // These all derive from IOException + // DirectoryNotFoundException + // DriveNotFoundException + // EndOfStreamException + // FileLoadException + // FileNotFoundException + // PathTooLongException + // PipeException + e is IOException || + // Note that SecurityException is only thrown on runtimes that support CAS + // e is SecurityException || + e is UnauthorizedAccessException || + e is NotSupportedException || + (e is ArgumentException && !(e is ArgumentNullException)); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Win32.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Win32.cs index c7b56290ca7ee..edee6fce1315d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Win32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamCompletionSource.Win32.cs @@ -6,252 +6,262 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; namespace System.IO { - internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy + // to avoid code duplicaiton of FileStreamCompletionSource for LegacyFileStreamStrategy and AsyncWindowsFileStreamStrategy + // we have created the following interface that is a common contract for both of them + internal interface IFileStreamCompletionSourceStrategy { - // This is an internal object extending TaskCompletionSource with fields - // for all of the relevant data necessary to complete the IO operation. - // This is used by IOCallback and all of the async methods. - private unsafe class FileStreamCompletionSource : TaskCompletionSource - { - private const long NoResult = 0; - private const long ResultSuccess = (long)1 << 32; - private const long ResultError = (long)2 << 32; - private const long RegisteringCancellation = (long)4 << 32; - private const long CompletedCallback = (long)8 << 32; - private const ulong ResultMask = ((ulong)uint.MaxValue) << 32; - - private static Action? s_cancelCallback; - - private readonly LegacyFileStreamStrategy _stream; - private readonly int _numBufferedBytes; - private CancellationTokenRegistration _cancellationRegistration; + SafeFileHandle FileHandle { get; } + + FileStreamCompletionSource? CurrentOverlappedOwner { get; } + + FileStreamCompletionSource? CompareExchangeCurrentOverlappedOwner(FileStreamCompletionSource? newSource, FileStreamCompletionSource? existingSource); + } + + // This is an internal object extending TaskCompletionSource with fields + // for all of the relevant data necessary to complete the IO operation. + // This is used by IOCallback and all of the async methods. + internal unsafe class FileStreamCompletionSource : TaskCompletionSource + { + private const long NoResult = 0; + private const long ResultSuccess = (long)1 << 32; + private const long ResultError = (long)2 << 32; + private const long RegisteringCancellation = (long)4 << 32; + private const long CompletedCallback = (long)8 << 32; + private const ulong ResultMask = ((ulong)uint.MaxValue) << 32; + private const int ERROR_BROKEN_PIPE = 109; + private const int ERROR_NO_DATA = 232; + + internal static readonly unsafe IOCompletionCallback s_ioCallback = IOCallback; + + private static Action? s_cancelCallback; + + private readonly IFileStreamCompletionSourceStrategy _strategy; + private readonly int _numBufferedBytes; + private CancellationTokenRegistration _cancellationRegistration; #if DEBUG - private bool _cancellationHasBeenRegistered; + 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 + private NativeOverlapped* _overlapped; // Overlapped class responsible for operations in progress when an appdomain unload occurs + private long _result; // Using long since this needs to be used in Interlocked APIs - // Using RunContinuationsAsynchronously for compat reasons (old API used Task.Factory.StartNew for continuations) - protected FileStreamCompletionSource(LegacyFileStreamStrategy stream, int numBufferedBytes, byte[]? bytes) - : base(TaskCreationOptions.RunContinuationsAsynchronously) - { - _numBufferedBytes = numBufferedBytes; - _stream = stream; - _result = NoResult; - - // Create the native overlapped. We try to use the preallocated overlapped if possible: it's possible if the byte - // buffer is null (there's nothing to pin) or the same one that's associated with the preallocated overlapped (and - // thus is already pinned) and if no one else is currently using the preallocated overlapped. This is the fast-path - // for cases where the user-provided buffer is smaller than the FileStream's buffer (such that the FileStream's - // buffer is used) and where operations on the FileStream are not being performed concurrently. - Debug.Assert(bytes == null || ReferenceEquals(bytes, _stream._buffer)); - - // The _preallocatedOverlapped is null if the internal buffer was never created, so we check for - // a non-null bytes before using the stream's _preallocatedOverlapped - _overlapped = bytes != null && _stream.CompareExchangeCurrentOverlappedOwner(this, null) == null ? - _stream._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(_stream._preallocatedOverlapped!) : // allocated when buffer was created, and buffer is non-null - _stream._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(s_ioCallback, this, bytes); - Debug.Assert(_overlapped != null, "AllocateNativeOverlapped returned null"); - } + // Using RunContinuationsAsynchronously for compat reasons (old API used Task.Factory.StartNew for continuations) + internal FileStreamCompletionSource(IFileStreamCompletionSourceStrategy strategy, PreAllocatedOverlapped? preallocatedOverlapped, + int numBufferedBytes, byte[]? bytes) : base(TaskCreationOptions.RunContinuationsAsynchronously) + { + _numBufferedBytes = numBufferedBytes; + _strategy = strategy; + _result = NoResult; - internal NativeOverlapped* Overlapped => _overlapped; + // 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"); + } - public void SetCompletedSynchronously(int numBytes) - { - ReleaseNativeResource(); - TrySetResult(numBytes + _numBufferedBytes); - } + internal NativeOverlapped* Overlapped => _overlapped; - public void RegisterForCancellation(CancellationToken cancellationToken) - { + 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; + 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) + // Quick check to make sure the IO hasn't completed + if (_overlapped != null) + { + Action? cancelCallback = s_cancelCallback ??= Cancel; + + // Register the cancellation only if the IO hasn't completed + long packedResult = Interlocked.CompareExchange(ref _result, RegisteringCancellation, NoResult); + if (packedResult == NoResult) { - Action? cancelCallback = s_cancelCallback ??= Cancel; - - // Register the cancellation only if the IO hasn't completed - long packedResult = Interlocked.CompareExchange(ref _result, RegisteringCancellation, NoResult); - if (packedResult == NoResult) - { - _cancellationRegistration = cancellationToken.UnsafeRegister(cancelCallback, this); - - // Switch the result, just in case IO completed while we were setting the registration - packedResult = Interlocked.Exchange(ref _result, NoResult); - } - else if (packedResult != CompletedCallback) - { - // Failed to set the result, IO is in the process of completing - // Attempt to take the packed result - packedResult = Interlocked.Exchange(ref _result, NoResult); - } - - // If we have a callback that needs to be completed - if ((packedResult != NoResult) && (packedResult != CompletedCallback) && (packedResult != RegisteringCancellation)) - { - CompleteCallback((ulong)packedResult); - } - } - } + _cancellationRegistration = cancellationToken.UnsafeRegister(cancelCallback, this); - internal virtual void ReleaseNativeResource() - { - // Ensure that cancellation has been completed and cleaned up. - _cancellationRegistration.Dispose(); + // Switch the result, just in case IO completed while we were setting the registration + packedResult = Interlocked.Exchange(ref _result, NoResult); + } + else if (packedResult != CompletedCallback) + { + // Failed to set the result, IO is in the process of completing + // Attempt to take the packed result + packedResult = Interlocked.Exchange(ref _result, NoResult); + } - // 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) + // If we have a callback that needs to be completed + if ((packedResult != NoResult) && (packedResult != CompletedCallback) && (packedResult != RegisteringCancellation)) { - _stream._fileHandle.ThreadPoolBinding!.FreeNativeOverlapped(_overlapped); - _overlapped = null; + CompleteCallback((ulong)packedResult); } + } + } + + internal virtual void ReleaseNativeResource() + { + // Ensure that cancellation has been completed and cleaned up. + _cancellationRegistration.Dispose(); - // Ensure we're no longer set as the current completion source (we may not have been to begin with). - // Only one operation at a time is eligible to use the preallocated overlapped, - _stream.CompareExchangeCurrentOverlappedOwner(null, this); + // 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; } - // 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) + // Ensure we're no longer set as the current completion source (we may not have been to begin with). + // Only one operation at a time is eligible to use the preallocated overlapped, + _strategy.CompareExchangeCurrentOverlappedOwner(null, this); + } + + // When doing IO asynchronously (i.e. _isAsync==true), this callback is + // called by a free thread in the threadpool when the IO operation + // completes. + internal static void IOCallback(uint errorCode, uint numBytes, NativeOverlapped* pOverlapped) + { + // Extract the completion source from the overlapped. The state in the overlapped + // will either be a FileStreamStrategy (in the case where the preallocated overlapped was used), + // in which case the operation being completed is its _currentOverlappedOwner, or it'll + // be directly the FileStreamCompletionSource that's completing (in the case where the preallocated + // overlapped was already in use by another operation). + object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped); + Debug.Assert(state is IFileStreamCompletionSourceStrategy || state is FileStreamCompletionSource); + FileStreamCompletionSource completionSource = state switch + { + IFileStreamCompletionSourceStrategy strategy => strategy.CurrentOverlappedOwner!, // must be owned + _ => (FileStreamCompletionSource)state + }; + Debug.Assert(completionSource != null); + Debug.Assert(completionSource._overlapped == pOverlapped, "Overlaps don't match"); + + // Handle reading from & writing to closed pipes. While I'm not sure + // this is entirely necessary anymore, maybe it's possible for + // an async read on a pipe to be issued and then the pipe is closed, + // returning this error. This may very well be necessary. + ulong packedResult; + if (errorCode != 0 && errorCode != ERROR_BROKEN_PIPE && errorCode != ERROR_NO_DATA) { - // Extract the completion source from the overlapped. The state in the overlapped - // will either be a FileStream (in the case where the preallocated overlapped was used), - // in which case the operation being completed is its _currentOverlappedOwner, or it'll - // be directly the FileStreamCompletionSource that's completing (in the case where the preallocated - // overlapped was already in use by another operation). - object? state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped); - Debug.Assert(state is LegacyFileStreamStrategy || state is FileStreamCompletionSource); - FileStreamCompletionSource completionSource = state is LegacyFileStreamStrategy fs ? - fs._currentOverlappedOwner! : // must be owned - (FileStreamCompletionSource)state!; - Debug.Assert(completionSource != null); - Debug.Assert(completionSource._overlapped == pOverlapped, "Overlaps don't match"); - - // Handle reading from & writing to closed pipes. While I'm not sure - // this is entirely necessary anymore, maybe it's possible for - // an async read on a pipe to be issued and then the pipe is closed, - // returning this error. This may very well be necessary. - ulong packedResult; - if (errorCode != 0 && errorCode != ERROR_BROKEN_PIPE && errorCode != ERROR_NO_DATA) - { - packedResult = ((ulong)ResultError | errorCode); - } - else - { - packedResult = ((ulong)ResultSuccess | numBytes); - } + packedResult = ((ulong)ResultError | errorCode); + } + else + { + packedResult = ((ulong)ResultSuccess | numBytes); + } - // Stow the result so that other threads can observe it - // And, if no other thread is registering cancellation, continue - if (NoResult == Interlocked.Exchange(ref completionSource._result, (long)packedResult)) + // Stow the result so that other threads can observe it + // And, if no other thread is registering cancellation, continue + if (NoResult == Interlocked.Exchange(ref completionSource._result, (long)packedResult)) + { + // Successfully set the state, attempt to take back the callback + if (Interlocked.Exchange(ref completionSource._result, CompletedCallback) != NoResult) { - // Successfully set the state, attempt to take back the callback - if (Interlocked.Exchange(ref completionSource._result, CompletedCallback) != NoResult) - { - // Successfully got the callback, finish the callback - completionSource.CompleteCallback(packedResult); - } - // else: Some other thread stole the result, so now it is responsible to finish the callback + // Successfully got the callback, finish the callback + completionSource.CompleteCallback(packedResult); } - // else: Some other thread is registering a cancellation, so it *must* finish the callback + // 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(); + private void CompleteCallback(ulong packedResult) + { + // Free up the native resource and cancellation registration + CancellationToken cancellationToken = _cancellationRegistration.Token; // access before disposing registration + ReleaseNativeResource(); - // Unpack the result and send it to the user - long result = (long)(packedResult & ResultMask); - if (result == ResultError) + // Unpack the result and send it to the user + long result = (long)(packedResult & ResultMask); + if (result == ResultError) + { + int errorCode = unchecked((int)(packedResult & uint.MaxValue)); + if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED) { - 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); - } + TrySetCanceled(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(true)); } else { - Debug.Assert(result == ResultSuccess, "Unknown result"); - TrySetResult((int)(packedResult & uint.MaxValue) + _numBufferedBytes); + Exception e = Win32Marshal.GetExceptionForWin32Error(errorCode); + e.SetCurrentStackTrace(); + TrySetException(e); } } - - private static void Cancel(object? state) + else { - // WARNING: This may potentially be called under a lock (during cancellation registration) + Debug.Assert(result == ResultSuccess, "Unknown result"); + TrySetResult((int)(packedResult & uint.MaxValue) + _numBufferedBytes); + } + } - Debug.Assert(state is FileStreamCompletionSource, "Unknown state passed to cancellation"); - FileStreamCompletionSource completionSource = (FileStreamCompletionSource)state; - Debug.Assert(completionSource._overlapped != null && !completionSource.Task.IsCompleted, "IO should not have completed yet"); + private static void Cancel(object? state) + { + // WARNING: This may potentially be called under a lock (during cancellation registration) - // If the handle is still valid, attempt to cancel the IO - if (!completionSource._stream._fileHandle.IsInvalid && - !Interop.Kernel32.CancelIoEx(completionSource._stream._fileHandle, completionSource._overlapped)) - { - int errorCode = Marshal.GetLastWin32Error(); - - // ERROR_NOT_FOUND is returned if CancelIoEx cannot find the request to cancel. - // This probably means that the IO operation has completed. - if (errorCode != Interop.Errors.ERROR_NOT_FOUND) - { - throw Win32Marshal.GetExceptionForWin32Error(errorCode); - } - } - } + Debug.Assert(state is FileStreamCompletionSource, "Unknown state passed to cancellation"); + FileStreamCompletionSource completionSource = (FileStreamCompletionSource)state; + Debug.Assert(completionSource._overlapped != null && !completionSource.Task.IsCompleted, "IO should not have completed yet"); - public static FileStreamCompletionSource Create(LegacyFileStreamStrategy stream, int numBufferedBytesRead, ReadOnlyMemory memory) + // If the handle is still valid, attempt to cancel the IO + if (!completionSource._strategy.FileHandle.IsInvalid && + !Interop.Kernel32.CancelIoEx(completionSource._strategy.FileHandle, completionSource._overlapped)) { - // If the memory passed in is the stream's internal buffer, we can use the base FileStreamCompletionSource, - // which has a PreAllocatedOverlapped with the memory already pinned. Otherwise, we use the derived - // MemoryFileStreamCompletionSource, which Retains the memory, which will result in less pinning in the case - // where the underlying memory is backed by pre-pinned buffers. - return MemoryMarshal.TryGetArray(memory, out ArraySegment buffer) && ReferenceEquals(buffer.Array, stream._buffer) ? - new FileStreamCompletionSource(stream, numBufferedBytesRead, buffer.Array) : - new MemoryFileStreamCompletionSource(stream, numBufferedBytesRead, memory); + int errorCode = Marshal.GetLastWin32Error(); + + // ERROR_NOT_FOUND is returned if CancelIoEx cannot find the request to cancel. + // This probably means that the IO operation has completed. + if (errorCode != Interop.Errors.ERROR_NOT_FOUND) + { + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } } } - /// - /// Extends with to support disposing of a - /// when the operation has completed. This should only be used - /// when memory doesn't wrap a byte[]. - /// - private sealed class MemoryFileStreamCompletionSource : FileStreamCompletionSource + public static FileStreamCompletionSource Create(IFileStreamCompletionSourceStrategy strategy, PreAllocatedOverlapped? preallocatedOverlapped, + int numBufferedBytesRead, ReadOnlyMemory memory) { - private MemoryHandle _handle; // mutable struct; do not make this readonly + // 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|LegacyFileStreamStrategy allocates the buffer + ? new FileStreamCompletionSource(strategy, preallocatedOverlapped, numBufferedBytesRead, buffer.Array) + : new MemoryFileStreamCompletionSource(strategy, numBufferedBytesRead, memory); + } + } - internal MemoryFileStreamCompletionSource(LegacyFileStreamStrategy stream, int numBufferedBytes, ReadOnlyMemory memory) : - base(stream, numBufferedBytes, bytes: null) // this type handles the pinning, so null is passed for bytes - { - _handle = memory.Pin(); - } + /// + /// Extends with to support disposing of a + /// when the operation has completed. This should only be used + /// when memory doesn't wrap a byte[]. + /// + internal sealed class MemoryFileStreamCompletionSource : FileStreamCompletionSource + { + private MemoryHandle _handle; // mutable struct; do not make this readonly - internal override void ReleaseNativeResource() - { - _handle.Dispose(); - base.ReleaseNativeResource(); - } + internal MemoryFileStreamCompletionSource(IFileStreamCompletionSourceStrategy strategy, int numBufferedBytes, ReadOnlyMemory memory) + : base(strategy, null, numBufferedBytes, null) // this type handles the pinning, so null is passed for bytes + { + _handle = memory.Pin(); + } + + internal override void ReleaseNativeResource() + { + _handle.Dispose(); + base.ReleaseNativeResource(); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Unix.cs index 471005b833f74..25a09f1ab8940 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Unix.cs @@ -14,11 +14,11 @@ namespace System.IO internal static class FileStreamHelpers { // in the future we are most probably going to introduce more strategies (io_uring etc) - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) - => new LegacyFileStreamStrategy(fileStream, handle, access, bufferSize, isAsync); + internal static FileStreamStrategy ChooseStrategy(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) + => new LegacyFileStreamStrategy(handle, access, bufferSize, isAsync); - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - => new LegacyFileStreamStrategy(fileStream, path, mode, access, share, bufferSize, options); + internal static FileStreamStrategy ChooseStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + => new LegacyFileStreamStrategy(path, mode, access, share, bufferSize, options); internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs index 66dd3cb259e4f..840d62cb03b80 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs @@ -1,8 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; namespace System.IO @@ -10,11 +14,46 @@ namespace System.IO // this type defines a set of stateless FileStream/FileStreamStrategy helper methods internal static class FileStreamHelpers { - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) - => new LegacyFileStreamStrategy(fileStream, handle, access, bufferSize, isAsync); + internal const int ERROR_BROKEN_PIPE = 109; + internal const int ERROR_NO_DATA = 232; + private const int ERROR_HANDLE_EOF = 38; + private const int ERROR_IO_PENDING = 997; - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - => new LegacyFileStreamStrategy(fileStream, path, mode, access, share, bufferSize, options); + // It's enabled by default. We are going to change that (by removing !) once we fix #16354, #25905 and #24847. + internal static bool UseLegacyStrategy { get; } + = !AppContextConfigHelper.GetBooleanConfig("System.IO.UseLegacyFileStream", "DOTNET_SYSTEM_IO_USELEGACYFILESTREAM"); + + internal static FileStreamStrategy ChooseStrategy(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) + { + if (UseLegacyStrategy) + { + return new LegacyFileStreamStrategy(handle, access, bufferSize, isAsync); + } + + WindowsFileStreamStrategy strategy = isAsync + ? new AsyncWindowsFileStreamStrategy(handle, access) + : new SyncWindowsFileStreamStrategy(handle, access); + + return EnableBufferingIfNeeded(strategy, bufferSize); + } + + internal static FileStreamStrategy ChooseStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + { + if (UseLegacyStrategy) + { + return new LegacyFileStreamStrategy(path, mode, access, share, bufferSize, options); + } + + WindowsFileStreamStrategy strategy = (options & FileOptions.Asynchronous) != 0 + ? new AsyncWindowsFileStreamStrategy(path, mode, access, share, options) + : new SyncWindowsFileStreamStrategy(path, mode, access, share, options); + + return EnableBufferingIfNeeded(strategy, bufferSize); + } + + // TODO: we might want to consider strategy.IsPipe here and never enable buffering for async pipes + internal static FileStreamStrategy EnableBufferingIfNeeded(WindowsFileStreamStrategy strategy, int bufferSize) + => bufferSize == 1 ? strategy : new BufferedFileStreamStrategy(strategy, bufferSize); internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) => CreateFileOpenHandle(path, mode, access, share, options); @@ -58,7 +97,7 @@ internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsyn return handle.IsAsync ?? !IsHandleSynchronous(handle, ignoreInvalid: true) ?? defaultIsAsync; } - private static unsafe bool? IsHandleSynchronous(SafeFileHandle fileHandle, bool ignoreInvalid) + internal static unsafe bool? IsHandleSynchronous(SafeFileHandle fileHandle, bool ignoreInvalid) { if (fileHandle.IsInvalid) return null; @@ -144,5 +183,448 @@ private static SafeFileHandle ValidateFileHandle(SafeFileHandle fileHandle, stri fileHandle.IsAsync = useAsyncIO; return fileHandle; } + + internal static unsafe long GetFileLength(SafeFileHandle handle, string? path) + { + Interop.Kernel32.FILE_STANDARD_INFO info; + + if (!Interop.Kernel32.GetFileInformationByHandleEx(handle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))) + { + throw Win32Marshal.GetExceptionForLastWin32Error(path); + } + + return info.EndOfFile; + } + + internal static void FlushToDisk(SafeFileHandle handle, string? path) + { + if (!Interop.Kernel32.FlushFileBuffers(handle)) + { + throw Win32Marshal.GetExceptionForLastWin32Error(path); + } + } + + internal static long Seek(SafeFileHandle handle, string? path, long offset, SeekOrigin origin, bool closeInvalidHandle = false) + { + Debug.Assert(origin >= SeekOrigin.Begin && origin <= SeekOrigin.End, "origin >= SeekOrigin.Begin && origin <= SeekOrigin.End"); + + if (!Interop.Kernel32.SetFilePointerEx(handle, offset, out long ret, (uint)origin)) + { + if (closeInvalidHandle) + { + throw Win32Marshal.GetExceptionForWin32Error(GetLastWin32ErrorAndDisposeHandleIfInvalid(handle), path); + } + else + { + throw Win32Marshal.GetExceptionForLastWin32Error(path); + } + } + + return ret; + } + + private static int GetLastWin32ErrorAndDisposeHandleIfInvalid(SafeFileHandle handle) + { + int errorCode = Marshal.GetLastWin32Error(); + + // If ERROR_INVALID_HANDLE is returned, it doesn't suffice to set + // the handle as invalid; the handle must also be closed. + // + // Marking the handle as invalid but not closing the handle + // resulted in exceptions during finalization and locked column + // values (due to invalid but unclosed handle) in SQL Win32FileStream + // scenarios. + // + // A more mainstream scenario involves accessing a file on a + // network share. ERROR_INVALID_HANDLE may occur because the network + // connection was dropped and the server closed the handle. However, + // the client side handle is still open and even valid for certain + // operations. + // + // Note that _parent.Dispose doesn't throw so we don't need to special case. + // SetHandleAsInvalid only sets _closed field to true (without + // actually closing handle) so we don't need to call that as well. + if (errorCode == Interop.Errors.ERROR_INVALID_HANDLE) + { + handle.Dispose(); + } + + return errorCode; + } + + internal static void Lock(SafeFileHandle handle, string? path, long position, long length) + { + int positionLow = unchecked((int)(position)); + int positionHigh = unchecked((int)(position >> 32)); + int lengthLow = unchecked((int)(length)); + int lengthHigh = unchecked((int)(length >> 32)); + + if (!Interop.Kernel32.LockFile(handle, positionLow, positionHigh, lengthLow, lengthHigh)) + { + throw Win32Marshal.GetExceptionForLastWin32Error(path); + } + } + + internal static void Unlock(SafeFileHandle handle, string? path, long position, long length) + { + int positionLow = unchecked((int)(position)); + int positionHigh = unchecked((int)(position >> 32)); + int lengthLow = unchecked((int)(length)); + int lengthHigh = unchecked((int)(length >> 32)); + + if (!Interop.Kernel32.UnlockFile(handle, positionLow, positionHigh, lengthLow, lengthHigh)) + { + throw Win32Marshal.GetExceptionForLastWin32Error(path); + } + } + + internal static void ValidateFileTypeForNonExtendedPaths(SafeFileHandle handle, string originalPath) + { + if (!PathInternal.IsExtended(originalPath)) + { + // To help avoid stumbling into opening COM/LPT ports by accident, we will block on non file handles unless + // we were explicitly passed a path that has \\?\. GetFullPath() will turn paths like C:\foo\con.txt into + // \\.\CON, so we'll only allow the \\?\ syntax. + + int fileType = Interop.Kernel32.GetFileType(handle); + if (fileType != Interop.Kernel32.FileTypes.FILE_TYPE_DISK) + { + int errorCode = fileType == Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN + ? Marshal.GetLastWin32Error() + : Interop.Errors.ERROR_SUCCESS; + + handle.Dispose(); + + if (errorCode != Interop.Errors.ERROR_SUCCESS) + { + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + throw new NotSupportedException(SR.NotSupported_FileStreamOnNonFiles); + } + } + } + + internal static void GetFileTypeSpecificInformation(SafeFileHandle handle, out bool canSeek, out bool isPipe) + { + int handleType = Interop.Kernel32.GetFileType(handle); + Debug.Assert(handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK + || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE + || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_CHAR, + "FileStream was passed an unknown file type!"); + + canSeek = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; + isPipe = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE; + } + + internal static unsafe void SetLength(SafeFileHandle handle, string? path, long length) + { + var eofInfo = new Interop.Kernel32.FILE_END_OF_FILE_INFO + { + EndOfFile = length + }; + + if (!Interop.Kernel32.SetFileInformationByHandle( + handle, + Interop.Kernel32.FileEndOfFileInfo, + &eofInfo, + (uint)sizeof(Interop.Kernel32.FILE_END_OF_FILE_INFO))) + { + int errorCode = Marshal.GetLastWin32Error(); + if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) + throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_FileLengthTooBig); + throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); + } + } + + // __ConsoleStream also uses this code. + internal static unsafe int ReadFileNative(SafeFileHandle handle, Span bytes, NativeOverlapped* overlapped, out int errorCode) + { + Debug.Assert(handle != null, "handle != null"); + + int r; + int numBytesRead = 0; + + fixed (byte* p = &MemoryMarshal.GetReference(bytes)) + { + r = overlapped != null ? + Interop.Kernel32.ReadFile(handle, p, bytes.Length, IntPtr.Zero, overlapped) : + Interop.Kernel32.ReadFile(handle, p, bytes.Length, out numBytesRead, IntPtr.Zero); + } + + if (r == 0) + { + errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); + return -1; + } + else + { + errorCode = 0; + return numBytesRead; + } + } + + internal static unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan buffer, NativeOverlapped* overlapped, out int errorCode) + { + Debug.Assert(handle != null, "handle != null"); + + int numBytesWritten = 0; + int r; + + fixed (byte* p = &MemoryMarshal.GetReference(buffer)) + { + r = overlapped != null ? + Interop.Kernel32.WriteFile(handle, p, buffer.Length, IntPtr.Zero, overlapped) : + Interop.Kernel32.WriteFile(handle, p, buffer.Length, out numBytesWritten, IntPtr.Zero); + } + + if (r == 0) + { + errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); + return -1; + } + else + { + errorCode = 0; + return numBytesWritten; + } + } + + internal static async Task AsyncModeCopyToAsync(SafeFileHandle handle, string? path, bool canSeek, long filePosition, Stream destination, int bufferSize, CancellationToken cancellationToken) + { + // For efficiency, we avoid creating a new task and associated state for each asynchronous read. + // Instead, we create a single reusable awaitable object that will be triggered when an await completes + // and reset before going again. + var readAwaitable = new AsyncCopyToAwaitable(handle); + + // Make sure we are reading from the position that we think we are. + // Only set the position in the awaitable if we can seek (e.g. not for pipes). + if (canSeek) + { + readAwaitable._position = filePosition; + } + + // Get the buffer to use for the copy operation, as the base CopyToAsync does. We don't try to use + // _buffer here, even if it's not null, as concurrent operations are allowed, and another operation may + // actually be using the buffer already. Plus, it'll be rare for _buffer to be non-null, as typically + // CopyToAsync is used as the only operation performed on the stream, and the buffer is lazily initialized. + // Further, typically the CopyToAsync buffer size will be larger than that used by the FileStream, such that + // we'd likely be unable to use it anyway. Instead, we rent the buffer from a pool. + byte[] copyBuffer = ArrayPool.Shared.Rent(bufferSize); + + // Allocate an Overlapped we can use repeatedly for all operations + var awaitableOverlapped = new PreAllocatedOverlapped(AsyncCopyToAwaitable.s_callback, readAwaitable, copyBuffer); + var cancellationReg = default(CancellationTokenRegistration); + try + { + // Register for cancellation. We do this once for the whole copy operation, and just try to cancel + // whatever read operation may currently be in progress, if there is one. It's possible the cancellation + // request could come in between operations, in which case we flag that with explicit calls to ThrowIfCancellationRequested + // in the read/write copy loop. + if (cancellationToken.CanBeCanceled) + { + cancellationReg = cancellationToken.UnsafeRegister(static s => + { + Debug.Assert(s is AsyncCopyToAwaitable); + var innerAwaitable = (AsyncCopyToAwaitable)s; + unsafe + { + lock (innerAwaitable.CancellationLock) // synchronize with cleanup of the overlapped + { + if (innerAwaitable._nativeOverlapped != null) + { + // Try to cancel the I/O. We ignore the return value, as cancellation is opportunistic and we + // don't want to fail the operation because we couldn't cancel it. + Interop.Kernel32.CancelIoEx(innerAwaitable._fileHandle, innerAwaitable._nativeOverlapped); + } + } + } + }, readAwaitable); + } + + // Repeatedly read from this FileStream and write the results to the destination stream. + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + readAwaitable.ResetForNextOperation(); + + try + { + bool synchronousSuccess; + int errorCode; + unsafe + { + // Allocate a native overlapped for our reusable overlapped, and set position to read based on the next + // desired address stored in the awaitable. (This position may be 0, if either we're at the beginning or + // if the stream isn't seekable.) + readAwaitable._nativeOverlapped = handle.ThreadPoolBinding!.AllocateNativeOverlapped(awaitableOverlapped); + if (canSeek) + { + readAwaitable._nativeOverlapped->OffsetLow = unchecked((int)readAwaitable._position); + readAwaitable._nativeOverlapped->OffsetHigh = (int)(readAwaitable._position >> 32); + } + + // Kick off the read. + synchronousSuccess = ReadFileNative(handle, copyBuffer, readAwaitable._nativeOverlapped, out errorCode) >= 0; + } + + // If the operation did not synchronously succeed, it either failed or initiated the asynchronous operation. + if (!synchronousSuccess) + { + switch (errorCode) + { + case ERROR_IO_PENDING: + // Async operation in progress. + break; + case ERROR_BROKEN_PIPE: + case ERROR_HANDLE_EOF: + // We're at or past the end of the file, and the overlapped callback + // won't be raised in these cases. Mark it as completed so that the await + // below will see it as such. + readAwaitable.MarkCompleted(); + break; + default: + // Everything else is an error (and there won't be a callback). + throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); + } + } + + // Wait for the async operation (which may or may not have already completed), then throw if it failed. + await readAwaitable; + switch (readAwaitable._errorCode) + { + case 0: // success + break; + case ERROR_BROKEN_PIPE: // logically success with 0 bytes read (write end of pipe closed) + case ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file) + Debug.Assert(readAwaitable._numBytes == 0, $"Expected 0 bytes read, got {readAwaitable._numBytes}"); + break; + case Interop.Errors.ERROR_OPERATION_ABORTED: // canceled + throw new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(true)); + default: // error + throw Win32Marshal.GetExceptionForWin32Error((int)readAwaitable._errorCode, path); + } + + // Successful operation. If we got zero bytes, we're done: exit the read/write loop. + int numBytesRead = (int)readAwaitable._numBytes; + if (numBytesRead == 0) + { + break; + } + + // Otherwise, update the read position for next time accordingly. + if (canSeek) + { + readAwaitable._position += numBytesRead; + } + } + finally + { + // Free the resources for this read operation + unsafe + { + NativeOverlapped* overlapped; + lock (readAwaitable.CancellationLock) // just an Exchange, but we need this to be synchronized with cancellation, so using the same lock + { + overlapped = readAwaitable._nativeOverlapped; + readAwaitable._nativeOverlapped = null; + } + if (overlapped != null) + { + handle.ThreadPoolBinding!.FreeNativeOverlapped(overlapped); + } + } + } + + // Write out the read data. + await destination.WriteAsync(new ReadOnlyMemory(copyBuffer, 0, (int)readAwaitable._numBytes), cancellationToken).ConfigureAwait(false); + } + } + finally + { + // Cleanup from the whole copy operation + cancellationReg.Dispose(); + awaitableOverlapped.Dispose(); + + ArrayPool.Shared.Return(copyBuffer); + } + } + + /// Used by AsyncWindowsFileStreamStrategy and LegacyFileStreamStrategy CopyToAsync to enable awaiting the result of an overlapped I/O operation with minimal overhead. + private sealed unsafe class AsyncCopyToAwaitable : ICriticalNotifyCompletion + { + /// Sentinel object used to indicate that the I/O operation has completed before being awaited. + private static readonly Action s_sentinel = () => { }; + /// Cached delegate to IOCallback. + internal static readonly IOCompletionCallback s_callback = IOCallback; + + internal readonly SafeFileHandle _fileHandle; + + /// Tracked position representing the next location from which to read. + internal long _position; + /// The current native overlapped pointer. This changes for each operation. + internal NativeOverlapped* _nativeOverlapped; + /// + /// null if the operation is still in progress, + /// s_sentinel if the I/O operation completed before the await, + /// s_callback if it completed after the await yielded. + /// + internal Action? _continuation; + /// Last error code from completed operation. + internal uint _errorCode; + /// Last number of read bytes from completed operation. + internal uint _numBytes; + + /// Lock object used to protect cancellation-related access to _nativeOverlapped. + internal object CancellationLock => this; + + /// Initialize the awaitable. + internal AsyncCopyToAwaitable(SafeFileHandle fileHandle) => _fileHandle = fileHandle; + + /// Reset state to prepare for the next read operation. + internal void ResetForNextOperation() + { + Debug.Assert(_position >= 0, $"Expected non-negative position, got {_position}"); + _continuation = null; + _errorCode = 0; + _numBytes = 0; + } + + /// Overlapped callback: store the results, then invoke the continuation delegate. + internal static void IOCallback(uint errorCode, uint numBytes, NativeOverlapped* pOVERLAP) + { + var awaitable = (AsyncCopyToAwaitable?)ThreadPoolBoundHandle.GetNativeOverlappedState(pOVERLAP); + Debug.Assert(awaitable != null); + + Debug.Assert(!ReferenceEquals(awaitable._continuation, s_sentinel), "Sentinel must not have already been set as the continuation"); + awaitable._errorCode = errorCode; + awaitable._numBytes = numBytes; + + (awaitable._continuation ?? Interlocked.CompareExchange(ref awaitable._continuation, s_sentinel, null))?.Invoke(); + } + + /// + /// Called when it's known that the I/O callback for an operation will not be invoked but we'll + /// still be awaiting the awaitable. + /// + internal void MarkCompleted() + { + Debug.Assert(_continuation == null, "Expected null continuation"); + _continuation = s_sentinel; + } + + public AsyncCopyToAwaitable GetAwaiter() => this; + public bool IsCompleted => ReferenceEquals(_continuation, s_sentinel); + public void GetResult() { } + public void OnCompleted(Action continuation) => UnsafeOnCompleted(continuation); + public void UnsafeOnCompleted(Action continuation) + { + if (ReferenceEquals(_continuation, s_sentinel) || + Interlocked.CompareExchange(ref _continuation, continuation, null) != null) + { + Debug.Assert(ReferenceEquals(_continuation, s_sentinel), $"Expected continuation set to s_sentinel, got ${_continuation}"); + Task.Run(continuation); + } + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs index 97b5969355383..416e48a01eac0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs @@ -7,10 +7,6 @@ namespace System.IO { internal abstract class FileStreamStrategy : Stream { - protected readonly FileStream _fileStream; - - protected FileStreamStrategy(FileStream fileStream) => _fileStream = fileStream; - internal abstract bool IsAsync { get; } internal abstract string Name { get; } @@ -21,6 +17,8 @@ internal abstract class FileStreamStrategy : Stream internal abstract bool IsClosed { get; } + internal virtual bool IsPipe => false; + internal abstract void Lock(long position, long length); internal abstract void Unlock(long position, long length); @@ -28,5 +26,7 @@ internal abstract class FileStreamStrategy : Stream internal abstract void Flush(bool flushToDisk); internal abstract void DisposeInternal(bool disposing); + + internal virtual void OnBufferAllocated(byte[] buffer) { } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs index 550b2acab2505..26818014bbbcf 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs @@ -172,7 +172,7 @@ protected override void Dispose(bool disposing) { FlushWriteBuffer(); } - catch (Exception e) when (IsIoRelatedException(e) && !disposing) + catch (Exception e) when (!disposing && FileStream.IsIoRelatedException(e)) { // On finalization, ignore failures from trying to flush the write buffer, // e.g. if this stream is wrapping a pipe and the pipe is now broken. @@ -395,7 +395,7 @@ private unsafe int ReadNative(Span buffer) if (!CanRead) // match Windows behavior; this gets thrown synchronously { - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } // Serialize operations using the semaphore. @@ -559,11 +559,11 @@ private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationTo return ValueTask.FromCanceled(cancellationToken); if (_fileHandle.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); if (!CanWrite) // match Windows behavior; this gets thrown synchronously { - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } // Serialize operations using the semaphore. @@ -637,11 +637,11 @@ public override long Seek(long offset, SeekOrigin origin) } if (_fileHandle.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } if (!CanSeek) { - throw Error.GetSeekNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } VerifyOSHandlePosition(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs index 4eaf3d69d0e2f..ab8cc977a5e05 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs @@ -1,13 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; using System.Diagnostics; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; -using System.Runtime.CompilerServices; /* * Win32FileStream supports different modes of accessing the disk - async mode @@ -39,42 +36,19 @@ namespace System.IO { - internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy + internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy, IFileStreamCompletionSourceStrategy { private bool _canSeek; private bool _isPipe; // Whether to disable async buffering code. private long _appendStart; // When appending, prevent overwriting file. - private static readonly unsafe IOCompletionCallback s_ioCallback = FileStreamCompletionSource.IOCallback; - private Task _activeBufferOperation = Task.CompletedTask; // tracks in-progress async ops using the buffer private PreAllocatedOverlapped? _preallocatedOverlapped; // optimization for async ops to avoid per-op allocations private FileStreamCompletionSource? _currentOverlappedOwner; // async op currently using the preallocated overlapped private void Init(FileMode mode, FileShare share, string originalPath, FileOptions options) { - if (!PathInternal.IsExtended(originalPath)) - { - // To help avoid stumbling into opening COM/LPT ports by accident, we will block on non file handles unless - // we were explicitly passed a path that has \\?\. GetFullPath() will turn paths like C:\foo\con.txt into - // \\.\CON, so we'll only allow the \\?\ syntax. - - int fileType = Interop.Kernel32.GetFileType(_fileHandle); - if (fileType != Interop.Kernel32.FileTypes.FILE_TYPE_DISK) - { - int errorCode = fileType == Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN - ? Marshal.GetLastWin32Error() - : Interop.Errors.ERROR_SUCCESS; - - _fileHandle.Dispose(); - - if (errorCode != Interop.Errors.ERROR_SUCCESS) - { - throw Win32Marshal.GetExceptionForWin32Error(errorCode); - } - throw new NotSupportedException(SR.NotSupported_FileStreamOnNonFiles); - } - } + FileStreamHelpers.ValidateFileTypeForNonExtendedPaths(_fileHandle, originalPath); // This is necessary for async IO using IO Completion ports via our // managed Threadpool API's. This (theoretically) calls the OS's @@ -139,11 +113,7 @@ private void InitFromHandle(SafeFileHandle handle, FileAccess access, bool useAs private void InitFromHandleImpl(SafeFileHandle handle, bool useAsyncIO) { - int handleType = Interop.Kernel32.GetFileType(handle); - Debug.Assert(handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_CHAR, "FileStream was passed an unknown file type!"); - - _canSeek = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; - _isPipe = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE; + FileStreamHelpers.GetFileTypeSpecificInformation(handle, out _canSeek, out _isPipe); // This is necessary for async IO using IO Completion ports via our // managed Threadpool API's. This calls the OS's @@ -189,11 +159,7 @@ public unsafe override long Length { get { - Interop.Kernel32.FILE_STANDARD_INFO info; - - if (!Interop.Kernel32.GetFileInformationByHandleEx(_fileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))) - throw Win32Marshal.GetExceptionForLastWin32Error(_path); - long len = info.EndOfFile; + long len = FileStreamHelpers.GetFileLength(_fileHandle, _path); // If we're writing near the end of the file, we must include our // internal buffer in our Length calculation. Don't flush because @@ -226,7 +192,7 @@ protected override void Dispose(bool disposing) { FlushWriteBuffer(!disposing); } - catch (Exception e) when (IsIoRelatedException(e) && !disposing) + catch (Exception e) when (!disposing && FileStream.IsIoRelatedException(e)) { // On finalization, ignore failures from trying to flush the write buffer, // e.g. if this stream is wrapping a pipe and the pipe is now broken. @@ -275,13 +241,7 @@ public override async ValueTask DisposeAsync() } } - private void FlushOSBuffer() - { - if (!Interop.Kernel32.FlushFileBuffers(_fileHandle)) - { - throw Win32Marshal.GetExceptionForLastWin32Error(_path); - } - } + private void FlushOSBuffer() => FileStreamHelpers.FlushToDisk(_fileHandle, _path); // Returns a task that flushes the internal write buffer private Task FlushWriteAsync(CancellationToken cancellationToken) @@ -368,22 +328,7 @@ private unsafe void SetLengthCore(long value) Debug.Assert(value >= 0, "value >= 0"); VerifyOSHandlePosition(); - var eofInfo = new Interop.Kernel32.FILE_END_OF_FILE_INFO - { - EndOfFile = value - }; - - if (!Interop.Kernel32.SetFileInformationByHandle( - _fileHandle, - Interop.Kernel32.FileEndOfFileInfo, - &eofInfo, - (uint)sizeof(Interop.Kernel32.FILE_END_OF_FILE_INFO))) - { - int errorCode = Marshal.GetLastWin32Error(); - if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) - throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_FileLengthTooBig); - throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); - } + FileStreamHelpers.SetLength(_fileHandle, _path, value); if (_filePosition > value) { @@ -391,11 +336,6 @@ private unsafe void SetLengthCore(long value) } } - // Instance method to help code external to this MarshalByRefObject avoid - // accessing its fields by ref. This avoids a compiler warning. - private FileStreamCompletionSource? CompareExchangeCurrentOverlappedOwner(FileStreamCompletionSource? newSource, FileStreamCompletionSource? existingSource) => - Interlocked.CompareExchange(ref _currentOverlappedOwner, newSource, existingSource); - private int ReadSpan(Span destination) { Debug.Assert(!_useAsyncIO, "Must only be used when in synchronous mode"); @@ -408,7 +348,7 @@ private int ReadSpan(Span destination) // buffer, depending on number of bytes user asked for and buffer size. if (n == 0) { - if (!CanRead) throw Error.GetReadNotSupported(); + if (!CanRead) ThrowHelper.ThrowNotSupportedException_UnreadableStream(); if (_writePos > 0) FlushWriteBuffer(); if (!CanSeek || (destination.Length >= _bufferLength)) { @@ -509,8 +449,8 @@ public override long Seek(long offset, SeekOrigin origin) { if (origin < SeekOrigin.Begin || origin > SeekOrigin.End) throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)); - if (_fileHandle.IsClosed) throw Error.GetFileNotOpen(); - if (!CanSeek) throw Error.GetSeekNotSupported(); + if (_fileHandle.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); + if (!CanSeek) ThrowHelper.ThrowNotSupportedException_UnseekableStream(); Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); @@ -594,22 +534,8 @@ public override long Seek(long offset, SeekOrigin origin) private long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin, bool closeInvalidHandle = false) { Debug.Assert(!fileHandle.IsClosed && _canSeek, "!fileHandle.IsClosed && _canSeek"); - Debug.Assert(origin >= SeekOrigin.Begin && origin <= SeekOrigin.End, "origin >= SeekOrigin.Begin && origin <= SeekOrigin.End"); - - if (!Interop.Kernel32.SetFilePointerEx(fileHandle, offset, out long ret, (uint)origin)) - { - if (closeInvalidHandle) - { - throw Win32Marshal.GetExceptionForWin32Error(GetLastWin32ErrorAndDisposeHandleIfInvalid(), _path); - } - else - { - throw Win32Marshal.GetExceptionForLastWin32Error(_path); - } - } - _filePosition = ret; - return ret; + return _filePosition = FileStreamHelpers.Seek(fileHandle, _path, offset, origin, closeInvalidHandle); } partial void OnBufferAllocated() @@ -618,9 +544,16 @@ partial void OnBufferAllocated() Debug.Assert(_preallocatedOverlapped == null); if (_useAsyncIO) - _preallocatedOverlapped = new PreAllocatedOverlapped(s_ioCallback, this, _buffer); + _preallocatedOverlapped = new PreAllocatedOverlapped(FileStreamCompletionSource.s_ioCallback, this, _buffer); } + SafeFileHandle IFileStreamCompletionSourceStrategy.FileHandle => _fileHandle; + + FileStreamCompletionSource? IFileStreamCompletionSourceStrategy.CurrentOverlappedOwner => _currentOverlappedOwner; + + FileStreamCompletionSource? IFileStreamCompletionSourceStrategy.CompareExchangeCurrentOverlappedOwner(FileStreamCompletionSource? newSource, FileStreamCompletionSource? existingSource) + => Interlocked.CompareExchange(ref _currentOverlappedOwner, newSource, existingSource); + private void WriteSpan(ReadOnlySpan source) { Debug.Assert(!_useAsyncIO, "Must only be used when in synchronous mode"); @@ -628,7 +561,7 @@ private void WriteSpan(ReadOnlySpan source) if (_writePos == 0) { // Ensure we can write to the stream, and ready buffer for writing. - if (!CanWrite) throw Error.GetWriteNotSupported(); + if (!CanWrite) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); if (_readPos < _readLength) FlushReadBuffer(); _readPos = 0; _readLength = 0; @@ -722,7 +655,7 @@ private unsafe void WriteCore(ReadOnlySpan source) private Task? ReadAsyncInternal(Memory destination, CancellationToken cancellationToken, out int synchronousResult) { Debug.Assert(_useAsyncIO); - if (!CanRead) throw Error.GetReadNotSupported(); + if (!CanRead) ThrowHelper.ThrowNotSupportedException_UnreadableStream(); Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); @@ -828,7 +761,7 @@ private unsafe Task ReadNativeAsync(Memory destination, int numBuffer Debug.Assert(_useAsyncIO, "ReadNativeAsync doesn't work on synchronous file streams!"); // Create and store async stream class library specific data in the async result - FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, numBufferedBytesRead, destination); + FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, _preallocatedOverlapped, numBufferedBytesRead, destination); NativeOverlapped* intOverlapped = completionSource.Overlapped; // Calculate position in the file we should be at after the read is done @@ -905,7 +838,7 @@ private unsafe Task ReadNativeAsync(Memory destination, int numBuffer if (errorCode == ERROR_HANDLE_EOF) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } else { @@ -938,7 +871,7 @@ private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationTo Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); Debug.Assert(!_isPipe || (_readPos == 0 && _readLength == 0), "Win32FileStream must not have buffered data here! Pipes should be unidirectional."); - if (!CanWrite) throw Error.GetWriteNotSupported(); + if (!CanWrite) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); bool writeDataStoredInBuffer = false; if (!_isPipe) // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadInternalAsyncCore) @@ -1046,7 +979,7 @@ private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, Cancella Debug.Assert(_useAsyncIO, "WriteInternalCoreAsync doesn't work on synchronous file streams!"); // Create and store async stream class library specific data in the async result - FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, 0, source); + FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, _preallocatedOverlapped, 0, source); NativeOverlapped* intOverlapped = completionSource.Overlapped; if (CanSeek) @@ -1107,7 +1040,7 @@ private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, Cancella if (errorCode == ERROR_HANDLE_EOF) { - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } else { @@ -1144,85 +1077,16 @@ private unsafe Task WriteAsyncInternalCore(ReadOnlyMemory source, Cancella // __ConsoleStream also uses this code. private unsafe int ReadFileNative(SafeFileHandle handle, Span bytes, NativeOverlapped* overlapped, out int errorCode) { - Debug.Assert(handle != null, "handle != null"); Debug.Assert((_useAsyncIO && overlapped != null) || (!_useAsyncIO && overlapped == null), "Async IO and overlapped parameters inconsistent in call to ReadFileNative."); - int r; - int numBytesRead = 0; - - fixed (byte* p = &MemoryMarshal.GetReference(bytes)) - { - r = _useAsyncIO ? - Interop.Kernel32.ReadFile(handle, p, bytes.Length, IntPtr.Zero, overlapped) : - Interop.Kernel32.ReadFile(handle, p, bytes.Length, out numBytesRead, IntPtr.Zero); - } - - if (r == 0) - { - errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(); - return -1; - } - else - { - errorCode = 0; - return numBytesRead; - } + return FileStreamHelpers.ReadFileNative(handle, bytes, overlapped, out errorCode); } private unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan buffer, NativeOverlapped* overlapped, out int errorCode) { - Debug.Assert(handle != null, "handle != null"); Debug.Assert((_useAsyncIO && overlapped != null) || (!_useAsyncIO && overlapped == null), "Async IO and overlapped parameters inconsistent in call to WriteFileNative."); - int numBytesWritten = 0; - int r; - - fixed (byte* p = &MemoryMarshal.GetReference(buffer)) - { - r = _useAsyncIO ? - Interop.Kernel32.WriteFile(handle, p, buffer.Length, IntPtr.Zero, overlapped) : - Interop.Kernel32.WriteFile(handle, p, buffer.Length, out numBytesWritten, IntPtr.Zero); - } - - if (r == 0) - { - errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(); - return -1; - } - else - { - errorCode = 0; - return numBytesWritten; - } - } - - private int GetLastWin32ErrorAndDisposeHandleIfInvalid() - { - int errorCode = Marshal.GetLastWin32Error(); - - // If ERROR_INVALID_HANDLE is returned, it doesn't suffice to set - // the handle as invalid; the handle must also be closed. - // - // Marking the handle as invalid but not closing the handle - // resulted in exceptions during finalization and locked column - // values (due to invalid but unclosed handle) in SQL Win32FileStream - // scenarios. - // - // A more mainstream scenario involves accessing a file on a - // network share. ERROR_INVALID_HANDLE may occur because the network - // connection was dropped and the server closed the handle. However, - // the client side handle is still open and even valid for certain - // operations. - // - // Note that _parent.Dispose doesn't throw so we don't need to special case. - // SetHandleAsInvalid only sets _closed field to true (without - // actually closing handle) so we don't need to call that as well. - if (errorCode == Interop.Errors.ERROR_INVALID_HANDLE) - { - _fileHandle.Dispose(); - } - - return errorCode; + return FileStreamHelpers.WriteFileNative(handle, buffer, overlapped, out errorCode); } public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) @@ -1239,11 +1103,11 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio // Fail if the file was closed if (_fileHandle.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } if (!CanRead) { - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } // Bail early for cancellation if cancellation has been requested @@ -1281,164 +1145,20 @@ private async Task AsyncModeCopyToAsync(Stream destination, int bufferSize, Canc } } - // For efficiency, we avoid creating a new task and associated state for each asynchronous read. - // Instead, we create a single reusable awaitable object that will be triggered when an await completes - // and reset before going again. - var readAwaitable = new AsyncCopyToAwaitable(this); - - // Make sure we are reading from the position that we think we are. - // Only set the position in the awaitable if we can seek (e.g. not for pipes). bool canSeek = CanSeek; if (canSeek) { VerifyOSHandlePosition(); - readAwaitable._position = _filePosition; } - // Get the buffer to use for the copy operation, as the base CopyToAsync does. We don't try to use - // _buffer here, even if it's not null, as concurrent operations are allowed, and another operation may - // actually be using the buffer already. Plus, it'll be rare for _buffer to be non-null, as typically - // CopyToAsync is used as the only operation performed on the stream, and the buffer is lazily initialized. - // Further, typically the CopyToAsync buffer size will be larger than that used by the FileStream, such that - // we'd likely be unable to use it anyway. Instead, we rent the buffer from a pool. - byte[] copyBuffer = ArrayPool.Shared.Rent(bufferSize); - - // Allocate an Overlapped we can use repeatedly for all operations - var awaitableOverlapped = new PreAllocatedOverlapped(AsyncCopyToAwaitable.s_callback, readAwaitable, copyBuffer); - var cancellationReg = default(CancellationTokenRegistration); try { - // Register for cancellation. We do this once for the whole copy operation, and just try to cancel - // whatever read operation may currently be in progress, if there is one. It's possible the cancellation - // request could come in between operations, in which case we flag that with explicit calls to ThrowIfCancellationRequested - // in the read/write copy loop. - if (cancellationToken.CanBeCanceled) - { - cancellationReg = cancellationToken.UnsafeRegister(static s => - { - Debug.Assert(s is AsyncCopyToAwaitable); - var innerAwaitable = (AsyncCopyToAwaitable)s; - unsafe - { - lock (innerAwaitable.CancellationLock) // synchronize with cleanup of the overlapped - { - if (innerAwaitable._nativeOverlapped != null) - { - // Try to cancel the I/O. We ignore the return value, as cancellation is opportunistic and we - // don't want to fail the operation because we couldn't cancel it. - Interop.Kernel32.CancelIoEx(innerAwaitable._fileStream._fileHandle, innerAwaitable._nativeOverlapped); - } - } - } - }, readAwaitable); - } - - // Repeatedly read from this FileStream and write the results to the destination stream. - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - readAwaitable.ResetForNextOperation(); - - try - { - bool synchronousSuccess; - int errorCode; - unsafe - { - // Allocate a native overlapped for our reusable overlapped, and set position to read based on the next - // desired address stored in the awaitable. (This position may be 0, if either we're at the beginning or - // if the stream isn't seekable.) - readAwaitable._nativeOverlapped = _fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(awaitableOverlapped); - if (canSeek) - { - readAwaitable._nativeOverlapped->OffsetLow = unchecked((int)readAwaitable._position); - readAwaitable._nativeOverlapped->OffsetHigh = (int)(readAwaitable._position >> 32); - } - - // Kick off the read. - synchronousSuccess = ReadFileNative(_fileHandle, copyBuffer, readAwaitable._nativeOverlapped, out errorCode) >= 0; - } - - // If the operation did not synchronously succeed, it either failed or initiated the asynchronous operation. - if (!synchronousSuccess) - { - switch (errorCode) - { - case ERROR_IO_PENDING: - // Async operation in progress. - break; - case ERROR_BROKEN_PIPE: - case ERROR_HANDLE_EOF: - // We're at or past the end of the file, and the overlapped callback - // won't be raised in these cases. Mark it as completed so that the await - // below will see it as such. - readAwaitable.MarkCompleted(); - break; - default: - // Everything else is an error (and there won't be a callback). - throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); - } - } - - // Wait for the async operation (which may or may not have already completed), then throw if it failed. - await readAwaitable; - switch (readAwaitable._errorCode) - { - case 0: // success - break; - case ERROR_BROKEN_PIPE: // logically success with 0 bytes read (write end of pipe closed) - case ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file) - Debug.Assert(readAwaitable._numBytes == 0, $"Expected 0 bytes read, got {readAwaitable._numBytes}"); - break; - case Interop.Errors.ERROR_OPERATION_ABORTED: // canceled - throw new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(true)); - default: // error - throw Win32Marshal.GetExceptionForWin32Error((int)readAwaitable._errorCode, _path); - } - - // Successful operation. If we got zero bytes, we're done: exit the read/write loop. - int numBytesRead = (int)readAwaitable._numBytes; - if (numBytesRead == 0) - { - break; - } - - // Otherwise, update the read position for next time accordingly. - if (canSeek) - { - readAwaitable._position += numBytesRead; - } - } - finally - { - // Free the resources for this read operation - unsafe - { - NativeOverlapped* overlapped; - lock (readAwaitable.CancellationLock) // just an Exchange, but we need this to be synchronized with cancellation, so using the same lock - { - overlapped = readAwaitable._nativeOverlapped; - readAwaitable._nativeOverlapped = null; - } - if (overlapped != null) - { - _fileHandle.ThreadPoolBinding!.FreeNativeOverlapped(overlapped); - } - } - } - - // Write out the read data. - await destination.WriteAsync(new ReadOnlyMemory(copyBuffer, 0, (int)readAwaitable._numBytes), cancellationToken).ConfigureAwait(false); - } + await FileStreamHelpers + .AsyncModeCopyToAsync(_fileHandle, _path, canSeek, _filePosition, destination, bufferSize, cancellationToken) + .ConfigureAwait(false); } finally { - // Cleanup from the whole copy operation - cancellationReg.Dispose(); - awaitableOverlapped.Dispose(); - - ArrayPool.Shared.Return(copyBuffer); - // Make sure the stream's current position reflects where we ended up if (!_fileHandle.IsClosed && CanSeek) { @@ -1447,112 +1167,8 @@ private async Task AsyncModeCopyToAsync(Stream destination, int bufferSize, Canc } } - /// Used by CopyToAsync to enable awaiting the result of an overlapped I/O operation with minimal overhead. - private sealed unsafe class AsyncCopyToAwaitable : ICriticalNotifyCompletion - { - /// Sentinel object used to indicate that the I/O operation has completed before being awaited. - private static readonly Action s_sentinel = () => { }; - /// Cached delegate to IOCallback. - internal static readonly IOCompletionCallback s_callback = IOCallback; - - /// The FileStream that owns this instance. - internal readonly LegacyFileStreamStrategy _fileStream; - - /// Tracked position representing the next location from which to read. - internal long _position; - /// The current native overlapped pointer. This changes for each operation. - internal NativeOverlapped* _nativeOverlapped; - /// - /// null if the operation is still in progress, - /// s_sentinel if the I/O operation completed before the await, - /// s_callback if it completed after the await yielded. - /// - internal Action? _continuation; - /// Last error code from completed operation. - internal uint _errorCode; - /// Last number of read bytes from completed operation. - internal uint _numBytes; - - /// Lock object used to protect cancellation-related access to _nativeOverlapped. - internal object CancellationLock => this; - - /// Initialize the awaitable. - internal AsyncCopyToAwaitable(LegacyFileStreamStrategy fileStream) - { - _fileStream = fileStream; - } + internal override void Lock(long position, long length) => FileStreamHelpers.Lock(_fileHandle, _path, position, length); - /// Reset state to prepare for the next read operation. - internal void ResetForNextOperation() - { - Debug.Assert(_position >= 0, $"Expected non-negative position, got {_position}"); - _continuation = null; - _errorCode = 0; - _numBytes = 0; - } - - /// Overlapped callback: store the results, then invoke the continuation delegate. - internal static void IOCallback(uint errorCode, uint numBytes, NativeOverlapped* pOVERLAP) - { - var awaitable = (AsyncCopyToAwaitable?)ThreadPoolBoundHandle.GetNativeOverlappedState(pOVERLAP); - Debug.Assert(awaitable != null); - - Debug.Assert(!ReferenceEquals(awaitable._continuation, s_sentinel), "Sentinel must not have already been set as the continuation"); - awaitable._errorCode = errorCode; - awaitable._numBytes = numBytes; - - (awaitable._continuation ?? Interlocked.CompareExchange(ref awaitable._continuation, s_sentinel, null))?.Invoke(); - } - - /// - /// Called when it's known that the I/O callback for an operation will not be invoked but we'll - /// still be awaiting the awaitable. - /// - internal void MarkCompleted() - { - Debug.Assert(_continuation == null, "Expected null continuation"); - _continuation = s_sentinel; - } - - public AsyncCopyToAwaitable GetAwaiter() => this; - public bool IsCompleted => ReferenceEquals(_continuation, s_sentinel); - public void GetResult() { } - public void OnCompleted(Action continuation) => UnsafeOnCompleted(continuation); - public void UnsafeOnCompleted(Action continuation) - { - if (ReferenceEquals(_continuation, s_sentinel) || - Interlocked.CompareExchange(ref _continuation, continuation, null) != null) - { - Debug.Assert(ReferenceEquals(_continuation, s_sentinel), $"Expected continuation set to s_sentinel, got ${_continuation}"); - Task.Run(continuation); - } - } - } - - internal override void Lock(long position, long length) - { - int positionLow = unchecked((int)(position)); - int positionHigh = unchecked((int)(position >> 32)); - int lengthLow = unchecked((int)(length)); - int lengthHigh = unchecked((int)(length >> 32)); - - if (!Interop.Kernel32.LockFile(_fileHandle, positionLow, positionHigh, lengthLow, lengthHigh)) - { - throw Win32Marshal.GetExceptionForLastWin32Error(_path); - } - } - - internal override void Unlock(long position, long length) - { - int positionLow = unchecked((int)(position)); - int positionHigh = unchecked((int)(position >> 32)); - int lengthLow = unchecked((int)(length)); - int lengthHigh = unchecked((int)(length >> 32)); - - if (!Interop.Kernel32.UnlockFile(_fileHandle, positionLow, positionHigh, lengthLow, lengthHigh)) - { - throw Win32Marshal.GetExceptionForLastWin32Error(_path); - } - } + internal override void Unlock(long position, long length) => FileStreamHelpers.Unlock(_fileHandle, _path, position, length); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs index 01e5db4f0827c..5d90aecac6ac7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs @@ -61,7 +61,7 @@ internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy /// Whether the file stream's handle has been exposed. private bool _exposedHandle; - internal LegacyFileStreamStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) : base(fileStream) + internal LegacyFileStreamStrategy(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) { _exposedHandle = true; _bufferLength = bufferSize; @@ -78,7 +78,7 @@ internal LegacyFileStreamStrategy(FileStream fileStream, SafeFileHandle handle, _fileHandle = handle; } - internal LegacyFileStreamStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) : base(fileStream) + internal LegacyFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) { string fullPath = Path.GetFullPath(path); @@ -105,12 +105,7 @@ internal LegacyFileStreamStrategy(FileStream fileStream, string path, FileMode m } } - ~LegacyFileStreamStrategy() - { - // it looks like having this finalizer is mandatory, - // as we can not guarantee that the Strategy won't be null in FileStream finalizer - Dispose(false); - } + ~LegacyFileStreamStrategy() => Dispose(false); // mandatory to Flush the write buffer internal override void DisposeInternal(bool disposing) => Dispose(disposing); @@ -149,7 +144,7 @@ public override int Read(Span buffer) { if (_fileHandle.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } return ReadSpan(buffer); @@ -229,7 +224,7 @@ public override void Write(ReadOnlySpan buffer) { if (_fileHandle.IsClosed) { - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } WriteSpan(buffer); @@ -272,8 +267,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo return WriteAsyncInternal(buffer, cancellationToken); } - // this method might call Derived type implenentation of Flush(flushToDisk) - public override void Flush() => _fileStream.Flush(); + public override void Flush() => Flush(flushToDisk: false); internal override void Flush(bool flushToDisk) { @@ -351,9 +345,9 @@ private void AssertBufferInvariants() private void PrepareForReading() { if (_fileHandle.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); if (_readLength == 0 && !CanRead) - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); AssertBufferInvariants(); } @@ -383,22 +377,6 @@ public override long Position internal override bool IsClosed => _fileHandle.IsClosed; - private static bool IsIoRelatedException(Exception e) => - // These all derive from IOException - // DirectoryNotFoundException - // DriveNotFoundException - // EndOfStreamException - // FileLoadException - // FileNotFoundException - // PathTooLongException - // PipeException - e is IOException || - // Note that SecurityException is only thrown on runtimes that support CAS - // e is SecurityException || - e is UnauthorizedAccessException || - e is NotSupportedException || - (e is ArgumentException && !(e is ArgumentNullException)); - /// /// Gets the array used for buffering reading and writing. /// If the array hasn't been allocated, this will lazily allocate it. @@ -502,14 +480,14 @@ public override void WriteByte(byte value) private void PrepareForWriting() { if (_fileHandle.IsClosed) - throw Error.GetFileNotOpen(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); // Make sure we're good to write. We only need to do this if there's nothing already // in our write buffer, since if there is something in the buffer, we've already done // this checking and flushing. if (_writePos == 0) { - if (!CanWrite) throw Error.GetWriteNotSupported(); + if (!CanWrite) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); FlushReadBuffer(); Debug.Assert(_bufferLength > 0, "_bufferSize > 0"); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs index 59316e780c843..d9b30c0b2aaad 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs @@ -108,13 +108,13 @@ public MemoryStream(byte[] buffer, int index, int count, bool writable, bool pub private void EnsureNotClosed() { if (!_isOpen) - throw Error.GetStreamIsClosed(); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); } private void EnsureWriteable() { if (!CanWrite) - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } protected override void Dispose(bool disposing) @@ -233,7 +233,7 @@ internal ReadOnlySpan InternalReadSpan(int count) if ((uint)newPos > (uint)_length) { _position = _length; - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); } var span = new ReadOnlySpan(_buffer, origPos, count); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs index 6a364614ef99a..71ad1dbeddc48 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs @@ -55,9 +55,12 @@ public virtual void CopyTo(Stream destination, int bufferSize) ValidateCopyToArguments(destination, bufferSize); if (!CanRead) { - throw CanWrite ? (Exception) - new NotSupportedException(SR.NotSupported_UnreadableStream) : - new ObjectDisposedException(GetType().Name, SR.ObjectDisposed_StreamClosed); + if (CanWrite) + { + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); + } + + ThrowHelper.ThrowObjectDisposedException_StreamClosed(GetType().Name); } byte[] buffer = ArrayPool.Shared.Rent(bufferSize); @@ -86,9 +89,12 @@ public virtual Task CopyToAsync(Stream destination, int bufferSize, Cancellation ValidateCopyToArguments(destination, bufferSize); if (!CanRead) { - throw CanWrite ? (Exception) - new NotSupportedException(SR.NotSupported_UnreadableStream) : - new ObjectDisposedException(GetType().Name, SR.ObjectDisposed_StreamClosed); + if (CanWrite) + { + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); + } + + ThrowHelper.ThrowObjectDisposedException_StreamClosed(GetType().Name); } return Core(this, destination, bufferSize, cancellationToken); @@ -202,7 +208,7 @@ internal IAsyncResult BeginReadInternal( ValidateBufferArguments(buffer, offset, count); if (!CanRead) { - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } // To avoid a race with a stream's position pointer & generating race conditions @@ -359,7 +365,7 @@ internal IAsyncResult BeginWriteInternal( ValidateBufferArguments(buffer, offset, count); if (!CanWrite) { - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } // To avoid a race condition with a stream's position pointer & generating conditions @@ -759,9 +765,12 @@ protected static void ValidateCopyToArguments(Stream destination, int bufferSize if (!destination.CanWrite) { - throw destination.CanRead ? (Exception) - new NotSupportedException(SR.NotSupported_UnwritableStream) : - new ObjectDisposedException(destination.GetType().Name, SR.ObjectDisposed_StreamClosed); + if (destination.CanRead) + { + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); + } + + ThrowHelper.ThrowObjectDisposedException_StreamClosed(destination.GetType().Name); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs index 848da5bc1e62b..2e2821a2b17d6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @@ -138,7 +138,7 @@ public StreamReader(Stream stream, Encoding? encoding = null, bool detectEncodin { if (stream == null) { - throw new ArgumentNullException(nameof(stream)); + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stream); } if (encoding == null) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs index 7b1004b0800c9..ce84fbfe85b7e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs @@ -95,7 +95,7 @@ public StreamWriter(Stream stream, Encoding? encoding = null, int bufferSize = - { if (stream == null) { - throw new ArgumentNullException(nameof(stream)); + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stream); } if (encoding == null) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs new file mode 100644 index 0000000000000..30d01134481cb --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/SyncWindowsFileStreamStrategy.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + internal sealed class SyncWindowsFileStreamStrategy : WindowsFileStreamStrategy + { + internal SyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access) : base(handle, access) + { + } + + internal SyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + : base(path, mode, access, share, options) + { + } + + internal override bool IsAsync => false; + + protected override void OnInitFromHandle(SafeFileHandle handle) + { + // As we can accurately check the handle type when we have access to NtQueryInformationFile we don't need to skip for + // any particular file handle type. + + // If the handle was passed in without an explicit async setting, we already looked it up in GetDefaultIsAsync + if (!handle.IsAsync.HasValue) + return; + + // If we can't check the handle, just assume it is ok. + if (!(FileStreamHelpers.IsHandleSynchronous(handle, ignoreInvalid: false) ?? true)) + throw new ArgumentException(SR.Arg_HandleNotSync, nameof(handle)); + } + + public override int Read(byte[] buffer, int offset, int count) => ReadSpan(new Span(buffer, offset, count)); + + public override int Read(Span buffer) => ReadSpan(buffer); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Read is invoked asynchronously. But we can do so using the base Stream's internal helper + // that bypasses delegating to BeginRead, since we already know this is FileStream rather + // than something derived from it and what our BeginRead implementation is going to do. + return (Task)BeginReadInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Read is invoked asynchronously. But if we have a byte[], we can do so using the base Stream's + // internal helper that bypasses delegating to BeginRead, since we already know this is FileStream + // rather than something derived from it and what our BeginRead implementation is going to do. + return MemoryMarshal.TryGetArray(buffer, out ArraySegment segment) ? + new ValueTask((Task)BeginReadInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : + base.ReadAsync(buffer, cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + => WriteSpan(new ReadOnlySpan(buffer, offset, count)); + + public override void Write(ReadOnlySpan buffer) + { + if (_fileHandle.IsClosed) + { + ThrowHelper.ThrowObjectDisposedException_FileClosed(); + } + + WriteSpan(buffer); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Write is invoked asynchronously. But we can do so using the base Stream's internal helper + // that bypasses delegating to BeginWrite, since we already know this is FileStream rather + // than something derived from it and what our BeginWrite implementation is going to do. + return (Task)BeginWriteInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Write is invoked asynchronously. But if we have a byte[], we can do so using the base Stream's + // internal helper that bypasses delegating to BeginWrite, since we already know this is FileStream + // rather than something derived from it and what our BeginWrite implementation is going to do. + return MemoryMarshal.TryGetArray(buffer, out ArraySegment segment) ? + new ValueTask((Task)BeginWriteInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : + base.WriteAsync(buffer, cancellationToken); + } + + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; // no buffering = nothing to flush + + private unsafe int ReadSpan(Span destination) + { + if (!CanRead) + { + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); + } + + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + + // Make sure we are reading from the right spot + VerifyOSHandlePosition(); + + int r = FileStreamHelpers.ReadFileNative(_fileHandle, destination, null, out int errorCode); + + if (r == -1) + { + // For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe. + if (errorCode == ERROR_BROKEN_PIPE) + { + r = 0; + } + else + { + if (errorCode == ERROR_INVALID_PARAMETER) + throw new ArgumentException(SR.Arg_HandleNotSync, "_fileHandle"); + + throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); + } + } + Debug.Assert(r >= 0, "FileStream's ReadNative is likely broken."); + _filePosition += r; + + return r; + } + + private unsafe void WriteSpan(ReadOnlySpan source) + { + if (!CanWrite) + { + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); + } + + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + + // Make sure we are writing to the position that we think we are + VerifyOSHandlePosition(); + + int r = FileStreamHelpers.WriteFileNative(_fileHandle, source, null, out int errorCode); + + if (r == -1) + { + // For pipes, ERROR_NO_DATA is not an error, but the pipe is closing. + if (errorCode == ERROR_NO_DATA) + { + r = 0; + } + else + { + // ERROR_INVALID_PARAMETER may be returned for writes + // where the position is too large or for synchronous writes + // to a handle opened asynchronously. + if (errorCode == ERROR_INVALID_PARAMETER) + throw new IOException(SR.IO_FileTooLongOrHandleNotSync); + throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); + } + } + Debug.Assert(r >= 0, "FileStream's WriteCore is likely broken."); + _filePosition += r; + return; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStream.cs index f0d48773eebf5..c116c17e69ad8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStream.cs @@ -215,19 +215,19 @@ protected override void Dispose(bool disposing) private void EnsureNotClosed() { if (!_isOpen) - throw Error.GetStreamIsClosed(); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); } private void EnsureReadable() { if (!CanRead) - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } private void EnsureWriteable() { if (!CanWrite) - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } /// @@ -290,13 +290,13 @@ public override long Position { get { - if (!CanSeek) throw Error.GetStreamIsClosed(); + if (!CanSeek) ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); return Interlocked.Read(ref _position); } set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); - if (!CanSeek) throw Error.GetStreamIsClosed(); + if (!CanSeek) ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); Interlocked.Exchange(ref _position, value); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStreamWrapper.cs b/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStreamWrapper.cs index 4851eb6098d2e..b1adbb1c022c4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStreamWrapper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/UnmanagedMemoryStreamWrapper.cs @@ -148,17 +148,16 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio throw new ArgumentOutOfRangeException(nameof(bufferSize), SR.ArgumentOutOfRange_NeedPosNum); if (!CanRead && !CanWrite) - throw new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); if (!destination.CanRead && !destination.CanWrite) - throw new ObjectDisposedException(nameof(destination), SR.ObjectDisposed_StreamClosed); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(nameof(destination)); if (!CanRead) - throw new NotSupportedException(SR.NotSupported_UnreadableStream); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); if (!destination.CanWrite) - throw new NotSupportedException(SR.NotSupported_UnwritableStream); - + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); return _unmanagedStream.CopyToAsync(destination, bufferSize, cancellationToken); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs new file mode 100644 index 0000000000000..45917d0bc6d2c --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WindowsFileStreamStrategy.cs @@ -0,0 +1,301 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using System.Runtime.CompilerServices; + +namespace System.IO +{ + // this type serves some basic functionality that is common for Async and Sync Windows File Stream Strategies + internal abstract class WindowsFileStreamStrategy : FileStreamStrategy + { + // Error codes (not HRESULTS), from winerror.h + internal const int ERROR_BROKEN_PIPE = 109; + internal const int ERROR_NO_DATA = 232; + protected const int ERROR_HANDLE_EOF = 38; + protected const int ERROR_INVALID_PARAMETER = 87; + protected const int ERROR_IO_PENDING = 997; + + protected readonly SafeFileHandle _fileHandle; // only ever null if ctor throws + protected readonly string? _path; // The path to the opened file. + private readonly FileAccess _access; // What file was opened for. + private readonly bool _canSeek; // Whether can seek (file) or not (pipe). + private readonly bool _isPipe; // Whether to disable async buffering code. + + protected long _filePosition; + protected bool _exposedHandle; // Whether the file stream's handle has been exposed. + private long _appendStart; // When appending, prevent overwriting file. + + internal WindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access) + { + _exposedHandle = true; + + InitFromHandle(handle, access, out _canSeek, out _isPipe); + + // Note: Cleaner to set the following fields in ValidateAndInitFromHandle, + // but we can't as they're readonly. + _access = access; + + // As the handle was passed in, we must set the handle field at the very end to + // avoid the finalizer closing the handle when we throw errors. + _fileHandle = handle; + } + + internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + { + string fullPath = Path.GetFullPath(path); + + _path = fullPath; + _access = access; + + _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options); + + try + { + _canSeek = true; + + Init(mode, path); + } + catch + { + // If anything goes wrong while setting up the stream, make sure we deterministically dispose + // of the opened handle. + _fileHandle.Dispose(); + _fileHandle = null!; + throw; + } + } + + public sealed override bool CanSeek => _canSeek; + + public sealed override bool CanRead => !_fileHandle.IsClosed && (_access & FileAccess.Read) != 0; + + public sealed override bool CanWrite => !_fileHandle.IsClosed && (_access & FileAccess.Write) != 0; + + public unsafe sealed override long Length => FileStreamHelpers.GetFileLength(_fileHandle, _path); + + /// Gets or sets the position within the current stream + public override long Position + { + get + { + VerifyOSHandlePosition(); + + return _filePosition; + } + set + { + Seek(value, SeekOrigin.Begin); + } + } + + internal sealed override string Name => _path ?? SR.IO_UnknownFileName; + + internal sealed override bool IsClosed => _fileHandle.IsClosed; + + internal sealed override bool IsPipe => _isPipe; + + internal sealed override SafeFileHandle SafeFileHandle + { + get + { + // Flushing is the responsibility of BufferedFileStreamStrategy + _exposedHandle = true; + return _fileHandle; + } + } + + // ReadByte and WriteByte methods are used only when the user has disabled buffering on purpose + // their performance is going to be horrible + // TODO: should we consider adding a new event provider and log an event so it can be detected? + public override int ReadByte() + { + Span buffer = stackalloc byte[1]; + int bytesRead = Read(buffer); + return bytesRead == 1 ? buffer[0] : -1; + } + + public override void WriteByte(byte value) + { + Span buffer = stackalloc byte[1]; + buffer[0] = value; + Write(buffer); + } + + // this method just disposes everything (no buffer, no need to flush) + public override ValueTask DisposeAsync() + { + if (_fileHandle != null && !_fileHandle.IsClosed) + { + _fileHandle.ThreadPoolBinding?.Dispose(); + _fileHandle.Dispose(); + } + + return ValueTask.CompletedTask; + } + + internal sealed override void DisposeInternal(bool disposing) => Dispose(disposing); + + // this method just disposes everything (no buffer, no need to flush) + protected override void Dispose(bool disposing) + { + if (_fileHandle != null && !_fileHandle.IsClosed) + { + _fileHandle.ThreadPoolBinding?.Dispose(); + _fileHandle.Dispose(); + } + } + + public sealed override void Flush() => Flush(flushToDisk: false); // we have nothing to flush as there is no buffer here + + internal sealed override void Flush(bool flushToDisk) + { + if (flushToDisk && CanWrite) + { + FileStreamHelpers.FlushToDisk(_fileHandle, _path); + } + } + + public sealed override long Seek(long offset, SeekOrigin origin) + { + if (origin < SeekOrigin.Begin || origin > SeekOrigin.End) + throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)); + if (_fileHandle.IsClosed) ThrowHelper.ThrowObjectDisposedException_FileClosed(); + if (!CanSeek) ThrowHelper.ThrowNotSupportedException_UnseekableStream(); + + // Verify that internal position is in sync with the handle + VerifyOSHandlePosition(); + + long oldPos = _filePosition; + long pos = SeekCore(_fileHandle, offset, origin); + + // Prevent users from overwriting data in a file that was opened in + // append mode. + if (_appendStart != -1 && pos < _appendStart) + { + SeekCore(_fileHandle, oldPos, SeekOrigin.Begin); + throw new IOException(SR.IO_SeekAppendOverwrite); + } + + return pos; + } + + // This doesn't do argument checking. Necessary for SetLength, which must + // set the file pointer beyond the end of the file. This will update the + // internal position + protected long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin, bool closeInvalidHandle = false) + { + Debug.Assert(!fileHandle.IsClosed && _canSeek, "!fileHandle.IsClosed && _canSeek"); + + return _filePosition = FileStreamHelpers.Seek(fileHandle, _path, offset, origin, closeInvalidHandle); + } + + internal sealed override void Lock(long position, long length) => FileStreamHelpers.Lock(_fileHandle, _path, position, length); + + internal sealed override void Unlock(long position, long length) => FileStreamHelpers.Unlock(_fileHandle, _path, position, length); + + protected abstract void OnInitFromHandle(SafeFileHandle handle); + + protected virtual void OnInit() { } + + private void Init(FileMode mode, string originalPath) + { + FileStreamHelpers.ValidateFileTypeForNonExtendedPaths(_fileHandle, originalPath); + + OnInit(); + + // For Append mode... + if (mode == FileMode.Append) + { + _appendStart = SeekCore(_fileHandle, 0, SeekOrigin.End); + } + else + { + _appendStart = -1; + } + } + + private void InitFromHandle(SafeFileHandle handle, FileAccess access, out bool canSeek, out bool isPipe) + { +#if DEBUG + bool hadBinding = handle.ThreadPoolBinding != null; + + try + { +#endif + InitFromHandleImpl(handle, out canSeek, out isPipe); +#if DEBUG + } + catch + { + Debug.Assert(hadBinding || handle.ThreadPoolBinding == null, "We should never error out with a ThreadPoolBinding we've added"); + throw; + } +#endif + } + + private void InitFromHandleImpl(SafeFileHandle handle, out bool canSeek, out bool isPipe) + { + FileStreamHelpers.GetFileTypeSpecificInformation(handle, out canSeek, out isPipe); + + OnInitFromHandle(handle); + + if (_canSeek) + SeekCore(handle, 0, SeekOrigin.Current); + else + _filePosition = 0; + } + + public sealed override void SetLength(long value) + { + if (_appendStart != -1 && value < _appendStart) + throw new IOException(SR.IO_SetLengthAppendTruncate); + + SetLengthCore(value); + } + + // We absolutely need this method broken out so that WriteInternalCoreAsync can call + // a method without having to go through buffering code that might call FlushWrite. + protected unsafe void SetLengthCore(long value) + { + Debug.Assert(value >= 0, "value >= 0"); + VerifyOSHandlePosition(); + + FileStreamHelpers.SetLength(_fileHandle, _path, value); + + if (_filePosition > value) + { + SeekCore(_fileHandle, 0, SeekOrigin.End); + } + } + + /// + /// Verify that the actual position of the OS's handle equals what we expect it to. + /// This will fail if someone else moved the UnixFileStream's handle or if + /// our position updating code is incorrect. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void VerifyOSHandlePosition() + { + bool verifyPosition = _exposedHandle; // in release, only verify if we've given out the handle such that someone else could be manipulating it +#if DEBUG + verifyPosition = true; // in debug, always make sure our position matches what the OS says it should be +#endif + if (verifyPosition && CanSeek) + { + long oldPos = _filePosition; // SeekCore will override the current _position, so save it now + long curPos = SeekCore(_fileHandle, 0, SeekOrigin.Current); + if (oldPos != curPos) + { + // For reads, this is non-fatal but we still could have returned corrupted + // data in some cases, so discard the internal buffer. For writes, + // this is a problem; discard the buffer and error out. + + throw new IOException(SR.IO_FileStreamHandlePosition); + } + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/TranscodingStream.cs b/src/libraries/System.Private.CoreLib/src/System/Text/TranscodingStream.cs index d807d910b429d..5ad89cd3f98a6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/TranscodingStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/TranscodingStream.cs @@ -79,7 +79,7 @@ internal TranscodingStream(Stream innerStream, Encoding innerEncoding, Encoding public override long Position { get => throw new NotSupportedException(SR.NotSupported_UnseekableStream); - set => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + set => ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) @@ -185,7 +185,7 @@ void InitializeReadDataStructures() { if (!CanRead) { - throw Error.GetReadNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } _innerDecoder = _innerEncoding.GetDecoder(); @@ -217,7 +217,7 @@ void InitializeReadDataStructures() { if (!CanWrite) { - throw Error.GetWriteNotSupported(); + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } _innerEncoder = _innerEncoding.GetEncoder(); @@ -428,7 +428,7 @@ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(SR.NotSupported_UnseekableStream); public override void SetLength(long value) - => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + => ThrowHelper.ThrowNotSupportedException_UnseekableStream(); [StackTraceHidden] private void ThrowIfDisposed() @@ -443,9 +443,7 @@ private void ThrowIfDisposed() [StackTraceHidden] private void ThrowObjectDisposedException() { - throw new ObjectDisposedException( - objectName: GetType().Name, - message: SR.ObjectDisposed_StreamClosed); + ThrowHelper.ThrowObjectDisposedException_StreamClosed(GetType().Name); } public override void Write(byte[] buffer, int offset, int count) diff --git a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs index a5f2bcfb7b155..27c29017c3846 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs @@ -38,6 +38,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.Serialization; @@ -218,15 +219,10 @@ internal static void ThrowArgumentException(ExceptionResource resource, Exceptio throw GetArgumentException(resource, argument); } - private static ArgumentNullException GetArgumentNullException(ExceptionArgument argument) - { - return new ArgumentNullException(GetArgumentName(argument)); - } - [DoesNotReturn] internal static void ThrowArgumentNullException(ExceptionArgument argument) { - throw GetArgumentNullException(argument); + throw new ArgumentNullException(GetArgumentName(argument)); } [DoesNotReturn] @@ -259,6 +255,12 @@ internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument throw GetArgumentOutOfRangeException(argument, paramNumber, resource); } + [DoesNotReturn] + internal static void ThrowEndOfFileException() + { + throw new EndOfStreamException(SR.IO_EOF_ReadBeyondEOF); + } + [DoesNotReturn] internal static void ThrowInvalidOperationException() { @@ -307,6 +309,24 @@ internal static void ThrowNotSupportedException(ExceptionResource resource) throw new NotSupportedException(GetResourceString(resource)); } + [DoesNotReturn] + internal static void ThrowNotSupportedException_UnseekableStream() + { + throw new NotSupportedException(SR.NotSupported_UnseekableStream); + } + + [DoesNotReturn] + internal static void ThrowNotSupportedException_UnreadableStream() + { + throw new NotSupportedException(SR.NotSupported_UnreadableStream); + } + + [DoesNotReturn] + internal static void ThrowNotSupportedException_UnwritableStream() + { + throw new NotSupportedException(SR.NotSupported_UnwritableStream); + } + [DoesNotReturn] internal static void ThrowUnauthorizedAccessException(ExceptionResource resource) { @@ -319,6 +339,18 @@ internal static void ThrowObjectDisposedException(string objectName, ExceptionRe throw new ObjectDisposedException(objectName, GetResourceString(resource)); } + [DoesNotReturn] + internal static void ThrowObjectDisposedException_StreamClosed(string? objectName) + { + throw new ObjectDisposedException(objectName, SR.ObjectDisposed_StreamClosed); + } + + [DoesNotReturn] + internal static void ThrowObjectDisposedException_FileClosed() + { + throw new ObjectDisposedException(null, SR.ObjectDisposed_FileClosed); + } + [DoesNotReturn] internal static void ThrowObjectDisposedException(ExceptionResource resource) { @@ -715,6 +747,8 @@ private static string GetArgumentName(ExceptionArgument argument) return "buffer"; case ExceptionArgument.offset: return "offset"; + case ExceptionArgument.stream: + return "stream"; default: Debug.Fail("The enum value is not defined, please check the ExceptionArgument Enum."); return ""; @@ -975,6 +1009,7 @@ internal enum ExceptionArgument suffix, buffer, offset, + stream } // diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs index fd2d2556527c6..a7d6577f111c3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -583,7 +583,7 @@ private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] r { int n = stream.Read(buffer, index, count); if (n == 0) - throw Error.GetEndOfFile(); + ThrowHelper.ThrowEndOfFileException(); int end = index + n; for (; index < end; index++) diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PreAllocatedOverlapped.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PreAllocatedOverlapped.cs index 2bd19d2e9a2e6..d4a0daeff29c7 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/PreAllocatedOverlapped.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PreAllocatedOverlapped.cs @@ -8,5 +8,6 @@ public sealed class PreAllocatedOverlapped : System.IDisposable [System.CLSCompliantAttribute(false)] public PreAllocatedOverlapped(System.Threading.IOCompletionCallback callback, object? state, object? pinData) { } public void Dispose() { } + internal bool IsUserObject(byte[]? buffer) => false; } }