diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/Flush.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/Flush.cs index b9552084b60e36..10e522185726ee 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/Flush.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/Flush.cs @@ -131,6 +131,18 @@ public void FlushCallsFlush_flushToDisk_False() } } + [Fact] + public void SafeFileHandleCallsFlush_flushToDisk_False() + { + using (StoreFlushArgFileStream fs = new StoreFlushArgFileStream(GetTestFilePath(), FileMode.Create)) + { + GC.KeepAlive(fs.SafeFileHandle); // this should call Flush, which should call StoreFlushArgFileStream.Flush(false) + + Assert.True(fs.LastFlushArg.HasValue); + Assert.False(fs.LastFlushArg.Value); + } + } + [Theory] [InlineData(null)] [InlineData(false)] diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/Position.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/Position.cs index 364e3c95b76a0a..8157f6cc91efdb 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/Position.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/Position.cs @@ -28,5 +28,15 @@ public void SetPositionAppendModify() Assert.Equal(length + 1, fs.Position); } } + + [Fact] + public void GetPositionThrowsForUnseekableFileStream() + { + string fileName = GetTestFilePath(); + using (FileStream fs = new UnseekableFileStream(fileName, FileMode.Create)) + { + Assert.Throws(() => _ = fs.Position); + } + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/SetLength.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/SetLength.cs index 27a2d42a55a32a..67944a457716ce 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/SetLength.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/SetLength.cs @@ -25,5 +25,25 @@ public void SetLengthAppendModifyThrows() Assert.Equal(length, fs.Length); } } + + [Fact] + public void SetLengthThrowsForUnseekableFileStream() + { + string fileName = GetTestFilePath(); + using (FileStream fs = new UnseekableFileStream(fileName, FileMode.Create)) + { + Assert.Throws(() => fs.SetLength(1)); + } + } + + [Fact] + public void GetLengthThrowsForUnseekableFileStream() + { + string fileName = GetTestFilePath(); + using (FileStream fs = new UnseekableFileStream(fileName, FileMode.Create)) + { + Assert.Throws(() => _ = fs.Length); + } + } } } 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 69842a262f148f..8bcf1103554d3e 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 @@ -388,6 +388,7 @@ + @@ -400,10 +401,12 @@ + + @@ -1620,14 +1623,14 @@ + - - + - + @@ -1826,12 +1829,13 @@ - - - + + + + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/DerivedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/DerivedFileStreamStrategy.cs new file mode 100644 index 00000000000000..76d4441309f7b9 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/DerivedFileStreamStrategy.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + // this type exists so we can avoid GetType() != typeof(FileStream) checks in FileStream + // when FileStream was supposed to call base.Method() for such cases, we just call _fileStream.BaseMethod() + // for everything else we fall back to the actual strategy (like FileStream does) + // + // it's crucial to NOT use the "base" keyoword here! everything must be using _fileStream or _strategy + internal sealed class DerivedFileStreamStrategy : FileStreamStrategy + { + private readonly FileStreamStrategy _strategy; + + internal DerivedFileStreamStrategy(FileStream fileStream, FileStreamStrategy strategy) : base(fileStream) => _strategy = strategy; + + public override bool CanRead => _strategy.CanRead; + + public override bool CanWrite => _strategy.CanWrite; + + public override bool CanSeek => _strategy.CanSeek; + + public override long Length => _strategy.Length; + + public override long Position + { + get => _strategy.Position; + set => _strategy.Position = value; + } + + internal override bool IsAsync => _strategy.IsAsync; + + internal override string Name => _strategy.Name; + + internal override SafeFileHandle SafeFileHandle => _strategy.SafeFileHandle; + + internal override bool IsClosed => _strategy.IsClosed; + + internal override void Lock(long position, long length) => _strategy.Lock(position, length); + + internal override void Unlock(long position, long length) => _strategy.Unlock(position, length); + + public override long Seek(long offset, SeekOrigin origin) => _strategy.Seek(offset, origin); + + public override void SetLength(long value) => _strategy.SetLength(value); + + public override int ReadByte() => _strategy.ReadByte(); + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => _strategy.IsAsync + ? _strategy.BeginRead(buffer, offset, count, callback, state) + : _fileStream.BaseBeginRead(buffer, offset, count, callback, state); + + public override int EndRead(IAsyncResult asyncResult) + => _strategy.IsAsync ? _strategy.EndRead(asyncResult) : _fileStream.BaseEndRead(asyncResult); + + public override int Read(byte[] buffer, int offset, int count) => _strategy.Read(buffer, offset, count); + + // If this is a derived type, it may have overridden Read(byte[], int, int) prior to this Read(Span) + // overload being introduced. In that case, this Read(Span) overload should use the behavior + // of Read(byte[],int,int) overload. + public override int Read(Span buffer) + => _fileStream.BaseRead(buffer); + + // If we have been inherited into a subclass, the Strategy implementation could be incorrect + // since it does not call through to Read() which a subclass might have overridden. + // To be safe we will only use this implementation in cases where we know it is safe to do so, + // and delegate to FileStream base class (which will call into Read/ReadAsync) when we are not sure. + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => _fileStream.BaseReadAsync(buffer, offset, count, cancellationToken); + + // If this isn't a concrete FileStream, a derived type may have overridden ReadAsync(byte[],...), + // which was introduced first, so delegate to the base which will delegate to that. + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + => _fileStream.BaseReadAsync(buffer, cancellationToken); + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => _strategy.IsAsync + ? _strategy.BeginWrite(buffer, offset, count, callback, state) + : _fileStream.BaseBeginWrite(buffer, offset, count, callback, state); + + public override void EndWrite(IAsyncResult asyncResult) + { + if (_strategy.IsAsync) + { + _strategy.EndWrite(asyncResult); + } + else + { + _fileStream.BaseEndWrite(asyncResult); + } + } + + public override void WriteByte(byte value) => _strategy.WriteByte(value); + + public override void Write(byte[] buffer, int offset, int count) => _strategy.Write(buffer, offset, count); + + // If this is a derived type, it may have overridden Write(byte[], int, int) prior to this Write(ReadOnlySpan) + // overload being introduced. In that case, this Write(ReadOnlySpan) overload should use the behavior + // of Write(byte[],int,int) overload. + public override void Write(ReadOnlySpan buffer) + => _fileStream.BaseWrite(buffer); + + // If we have been inherited into a subclass, the Strategy implementation could be incorrect + // since it does not call through to Write() or WriteAsync() which a subclass might have overridden. + // To be safe we will only use this implementation in cases where we know it is safe to do so, + // and delegate to our base class (which will call into Write/WriteAsync) when we are not sure. + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => _fileStream.BaseWriteAsync(buffer, offset, count, cancellationToken); + + // If this isn't a concrete FileStream, a derived type may have overridden WriteAsync(byte[],...), + // which was introduced first, so delegate to the base which will delegate to that. + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + => _fileStream.BaseWriteAsync(buffer, cancellationToken); + + public override void Flush() => throw new InvalidOperationException("FileStream should never call this method."); + + internal override void Flush(bool flushToDisk) => _strategy.Flush(flushToDisk); + + // If we have been inherited into a subclass, the following implementation could be incorrect + // since it does not call through to Flush() which a subclass might have overridden. To be safe + // we will only use this implementation in cases where we know it is safe to do so, + // and delegate to our base class (which will call into Flush) when we are not sure. + public override Task FlushAsync(CancellationToken cancellationToken) + => _fileStream.BaseFlushAsync(cancellationToken); + + // We also need to take this path if this is a derived + // instance from FileStream, as a derived type could have overridden ReadAsync, in which + // case our custom CopyToAsync implementation isn't necessarily correct. + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + => _fileStream.BaseCopyToAsync(destination, bufferSize, cancellationToken); + + public override ValueTask DisposeAsync() => _fileStream.BaseDisposeAsync(); + + internal override void DisposeInternal(bool disposing) => _strategy.DisposeInternal(disposing); + } +} 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 9842cf1329fd48..078b0bd0a41860 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -1,8 +1,6 @@ // 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.Runtime.Serialization; using System.Runtime.Versioning; using System.Threading; @@ -11,64 +9,17 @@ namespace System.IO { - public partial class FileStream : Stream + public class FileStream : Stream { + internal const int DefaultBufferSize = 4096; private const FileShare DefaultShare = FileShare.Read; private const bool DefaultIsAsync = false; - internal const int DefaultBufferSize = 4096; - - private byte[]? _buffer; - private int _bufferLength; - private readonly SafeFileHandle _fileHandle; // only ever null if ctor throws - - /// Whether the file is opened for reading, writing, or both. - private readonly FileAccess _access; - - /// The path to the opened file. - private readonly string? _path; - - /// The next available byte to be read from the _buffer. - private int _readPos; - - /// The number of valid bytes in _buffer. - private int _readLength; - - /// The next location in which a write should occur to the buffer. - private int _writePos; - - /// - /// Whether asynchronous read/write/flush operations should be performed using async I/O. - /// On Windows FileOptions.Asynchronous controls how the file handle is configured, - /// and then as a result how operations are issued against that file handle. On Unix, - /// there isn't any distinction around how file descriptors are created for async vs - /// sync, but we still differentiate how the operations are issued in order to provide - /// similar behavioral semantics and performance characteristics as on Windows. On - /// Windows, if non-async, async read/write requests just delegate to the base stream, - /// and no attempt is made to synchronize between sync and async operations on the stream; - /// if async, then async read/write requests are implemented specially, and sync read/write - /// requests are coordinated with async ones by implementing the sync ones over the async - /// ones. On Unix, we do something similar. If non-async, async read/write requests just - /// delegate to the base stream, and no attempt is made to synchronize. If async, we use - /// a semaphore to coordinate both sync and async operations. - /// - private readonly bool _useAsyncIO; - - /// cached task for read ops that complete synchronously - private Task? _lastSynchronouslyCompletedTask; - - /// - /// Currently cached position in the stream. This should always mirror the underlying file's actual position, - /// and should only ever be out of sync if another stream with access to this same file manipulates it, at which - /// point we attempt to error out. - /// - private long _filePosition; - - /// Whether the file stream's handle has been exposed. - private bool _exposedHandle; /// Caches whether Serialization Guard has been disabled for file writes private static int s_cachedSerializationSwitch; + private readonly FileStreamStrategy _strategy; + [Obsolete("This constructor has been deprecated. Please use new FileStream(SafeFileHandle handle, FileAccess access) instead. https://go.microsoft.com/fwlink/?linkid=14202")] public FileStream(IntPtr handle, FileAccess access) : this(handle, access, true, DefaultBufferSize, false) @@ -93,7 +44,9 @@ public FileStream(IntPtr handle, FileAccess access, bool ownsHandle, int bufferS SafeFileHandle safeHandle = new SafeFileHandle(handle, ownsHandle: ownsHandle); try { - ValidateAndInitFromHandle(safeHandle, access, bufferSize, isAsync); + ValidateHandle(safeHandle, access, bufferSize, isAsync); + + _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(this, safeHandle, access, bufferSize, isAsync)); } catch { @@ -107,28 +60,9 @@ public FileStream(IntPtr handle, FileAccess access, bool ownsHandle, int bufferS // safeHandle.SetHandleAsInvalid(); throw; } - - // Note: Cleaner to set the following fields in ValidateAndInitFromHandle, - // but we can't as they're readonly. - _access = access; - _useAsyncIO = isAsync; - - // As the handle was passed in, we must set the handle field at the very end to - // avoid the finalizer closing the handle when we throw errors. - _fileHandle = safeHandle; - } - - public FileStream(SafeFileHandle handle, FileAccess access) - : this(handle, access, DefaultBufferSize) - { - } - - public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize) - : this(handle, access, bufferSize, GetDefaultIsAsync(handle)) - { } - private void ValidateAndInitFromHandle(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) + private static void ValidateHandle(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) { if (handle.IsInvalid) throw new ArgumentException(SR.Arg_InvalidHandle, nameof(handle)); @@ -142,25 +76,26 @@ private void ValidateAndInitFromHandle(SafeFileHandle handle, FileAccess access, throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed); if (handle.IsAsync.HasValue && isAsync != handle.IsAsync.GetValueOrDefault()) throw new ArgumentException(SR.Arg_HandleNotAsync, nameof(handle)); + } - _exposedHandle = true; - _bufferLength = bufferSize; + private FileStreamStrategy WrapIfDerivedType(FileStreamStrategy impl) + => GetType() == typeof(FileStream) ? impl : new DerivedFileStreamStrategy(this, impl); - InitFromHandle(handle, access, isAsync); + public FileStream(SafeFileHandle handle, FileAccess access) + : this(handle, access, DefaultBufferSize) + { } - public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) + public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize) + : this(handle, access, bufferSize, FileStreamHelpers.GetDefaultIsAsync(handle, DefaultIsAsync)) { - ValidateAndInitFromHandle(handle, access, bufferSize, isAsync); + } - // Note: Cleaner to set the following fields in ValidateAndInitFromHandle, - // but we can't as they're readonly. - _access = access; - _useAsyncIO = isAsync; + public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) + { + ValidateHandle(handle, access, bufferSize, isAsync); - // As the handle was passed in, we must set the handle field at the very end to - // avoid the finalizer closing the handle when we throw errors. - _fileHandle = handle; + _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(this, handle, access, bufferSize, isAsync)); } public FileStream(string path, FileMode mode) : @@ -224,38 +159,16 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share if ((access & FileAccess.Read) != 0 && mode == FileMode.Append) throw new ArgumentException(SR.Argument_InvalidAppendMode, nameof(access)); - string fullPath = Path.GetFullPath(path); - - _path = fullPath; - _access = access; - _bufferLength = bufferSize; - - if ((options & FileOptions.Asynchronous) != 0) - _useAsyncIO = true; - if ((access & FileAccess.Write) == FileAccess.Write) { SerializationInfo.ThrowIfDeserializationInProgress("AllowFileWrites", ref s_cachedSerializationSwitch); } - _fileHandle = OpenHandle(mode, share, options); - - try - { - Init(mode, share, 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; - } + _strategy = WrapIfDerivedType(FileStreamHelpers.ChooseStrategy(this, 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")] - public virtual IntPtr Handle => SafeFileHandle.DangerousGetHandle(); + public virtual IntPtr Handle => _strategy.Handle; [UnsupportedOSPlatform("macos")] public virtual void Lock(long position, long length) @@ -265,12 +178,12 @@ public virtual void Lock(long position, long length) throw new ArgumentOutOfRangeException(position < 0 ? nameof(position) : nameof(length), SR.ArgumentOutOfRange_NeedNonNegNum); } - if (_fileHandle.IsClosed) + if (_strategy.IsClosed) { throw Error.GetFileNotOpen(); } - LockInternal(position, length); + _strategy.Lock(position, length); } [UnsupportedOSPlatform("macos")] @@ -281,272 +194,100 @@ public virtual void Unlock(long position, long length) throw new ArgumentOutOfRangeException(position < 0 ? nameof(position) : nameof(length), SR.ArgumentOutOfRange_NeedNonNegNum); } - if (_fileHandle.IsClosed) + if (_strategy.IsClosed) { throw Error.GetFileNotOpen(); } - UnlockInternal(position, length); + _strategy.Unlock(position, length); } public override Task FlushAsync(CancellationToken cancellationToken) - { - // If we have been inherited into a subclass, the following implementation could be incorrect - // since it does not call through to Flush() which a subclass might have overridden. To be safe - // we will only use this implementation in cases where we know it is safe to do so, - // and delegate to our base class (which will call into Flush) when we are not sure. - if (GetType() != typeof(FileStream)) - return base.FlushAsync(cancellationToken); - - return FlushAsyncInternal(cancellationToken); - } - - /// Asynchronously clears all buffers for this stream, causing any buffered data to be written to the underlying device. - /// The token to monitor for cancellation requests. - /// A task that represents the asynchronous flush operation. - private Task FlushAsyncInternal(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } - if (_fileHandle.IsClosed) + if (_strategy.IsClosed) { throw Error.GetFileNotOpen(); } - // TODO: https://github.com/dotnet/runtime/issues/27643 (stop doing this synchronous work!!). - // The always synchronous data transfer between the OS and the internal buffer is intentional - // because this is needed to allow concurrent async IO requests. Concurrent data transfer - // between the OS and the internal buffer will result in race conditions. Since FlushWrite and - // FlushRead modify internal state of the stream and transfer data between the OS and the - // internal buffer, they cannot be truly async. We will, however, flush the OS file buffers - // asynchronously because it doesn't modify any internal state of the stream and is potentially - // a long running process. - try - { - FlushInternalBuffer(); - } - catch (Exception e) - { - return Task.FromException(e); - } - - return Task.CompletedTask; + return _strategy.FlushAsync(cancellationToken); } public override int Read(byte[] buffer, int offset, int count) { ValidateReadWriteArgs(buffer, offset, count); - return _useAsyncIO ? - ReadAsyncTask(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult() : - ReadSpan(new Span(buffer, offset, count)); - } - public override int Read(Span buffer) - { - if (GetType() == typeof(FileStream) && !_useAsyncIO) - { - if (_fileHandle.IsClosed) - { - throw Error.GetFileNotOpen(); - } - return ReadSpan(buffer); - } - else - { - // This type is derived from FileStream and/or the stream is in async mode. If this is a - // derived type, it may have overridden Read(byte[], int, int) prior to this Read(Span) - // overload being introduced. In that case, this Read(Span) overload should use the behavior - // of Read(byte[],int,int) overload. Or if the stream is in async mode, we can't call the - // synchronous ReadSpan, so we similarly call the base Read, which will turn delegate to - // Read(byte[],int,int), which will do the right thing if we're in async mode. - return base.Read(buffer); - } + return _strategy.Read(buffer, offset, count); } + public override int Read(Span buffer) => _strategy.Read(buffer); + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { ValidateBufferArguments(buffer, offset, count); - if (GetType() != typeof(FileStream)) - { - // If we have been inherited into a subclass, the following implementation could be incorrect - // since it does not call through to Read() which a subclass might have overridden. - // To be safe we will only use this implementation in cases where we know it is safe to do so, - // and delegate to our base class (which will call into Read/ReadAsync) when we are not sure. - return base.ReadAsync(buffer, offset, count, cancellationToken); - } - if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); - if (IsClosed) + if (_strategy.IsClosed) throw Error.GetFileNotOpen(); - if (!_useAsyncIO) - { - // If we weren't opened for asynchronous I/O, we still call to the base implementation so that - // Read is invoked asynchronously. But we can do so using the base Stream's internal helper - // that bypasses delegating to BeginRead, since we already know this is FileStream rather - // than something derived from it and what our BeginRead implementation is going to do. - return (Task)base.BeginReadInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); - } - - return ReadAsyncTask(buffer, offset, count, cancellationToken); + return _strategy.ReadAsync(buffer, offset, count, cancellationToken); } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { - if (GetType() != typeof(FileStream)) - { - // If this isn't a concrete FileStream, a derived type may have overridden ReadAsync(byte[],...), - // which was introduced first, so delegate to the base which will delegate to that. - return base.ReadAsync(buffer, cancellationToken); - } - if (cancellationToken.IsCancellationRequested) { return ValueTask.FromCanceled(cancellationToken); } - if (IsClosed) + if (_strategy.IsClosed) { throw Error.GetFileNotOpen(); } - if (!_useAsyncIO) - { - // If we weren't opened for asynchronous I/O, we still call to the base implementation so that - // Read is invoked asynchronously. But if we have a byte[], we can do so using the base Stream's - // internal helper that bypasses delegating to BeginRead, since we already know this is FileStream - // rather than something derived from it and what our BeginRead implementation is going to do. - return MemoryMarshal.TryGetArray(buffer, out ArraySegment segment) ? - new ValueTask((Task)base.BeginReadInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : - base.ReadAsync(buffer, cancellationToken); - } - - Task? t = ReadAsyncInternal(buffer, cancellationToken, out int synchronousResult); - return t != null ? - new ValueTask(t) : - new ValueTask(synchronousResult); - } - - private Task ReadAsyncTask(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - Task? t = ReadAsyncInternal(new Memory(buffer, offset, count), cancellationToken, out int synchronousResult); - - if (t == null) - { - t = _lastSynchronouslyCompletedTask; - Debug.Assert(t == null || t.IsCompletedSuccessfully, "Cached task should have completed successfully"); - - if (t == null || t.Result != synchronousResult) - { - _lastSynchronouslyCompletedTask = t = Task.FromResult(synchronousResult); - } - } - - return t; + return _strategy.ReadAsync(buffer, cancellationToken); } public override void Write(byte[] buffer, int offset, int count) { ValidateReadWriteArgs(buffer, offset, count); - if (_useAsyncIO) - { - WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), CancellationToken.None).AsTask().GetAwaiter().GetResult(); - } - else - { - WriteSpan(new ReadOnlySpan(buffer, offset, count)); - } - } - public override void Write(ReadOnlySpan buffer) - { - if (GetType() == typeof(FileStream) && !_useAsyncIO) - { - if (_fileHandle.IsClosed) - { - throw Error.GetFileNotOpen(); - } - WriteSpan(buffer); - } - else - { - // This type is derived from FileStream and/or the stream is in async mode. If this is a - // derived type, it may have overridden Write(byte[], int, int) prior to this Write(ReadOnlySpan) - // overload being introduced. In that case, this Write(ReadOnlySpan) overload should use the behavior - // of Write(byte[],int,int) overload. Or if the stream is in async mode, we can't call the - // synchronous WriteSpan, so we similarly call the base Write, which will turn delegate to - // Write(byte[],int,int), which will do the right thing if we're in async mode. - base.Write(buffer); - } + _strategy.Write(buffer, offset, count); } + public override void Write(ReadOnlySpan buffer) => _strategy.Write(buffer); + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { ValidateBufferArguments(buffer, offset, count); - if (GetType() != typeof(FileStream)) - { - // If we have been inherited into a subclass, the following implementation could be incorrect - // since it does not call through to Write() or WriteAsync() which a subclass might have overridden. - // To be safe we will only use this implementation in cases where we know it is safe to do so, - // and delegate to our base class (which will call into Write/WriteAsync) when we are not sure. - return base.WriteAsync(buffer, offset, count, cancellationToken); - } - if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); - if (IsClosed) + if (_strategy.IsClosed) throw Error.GetFileNotOpen(); - if (!_useAsyncIO) - { - // If we weren't opened for asynchronous I/O, we still call to the base implementation so that - // Write is invoked asynchronously. But we can do so using the base Stream's internal helper - // that bypasses delegating to BeginWrite, since we already know this is FileStream rather - // than something derived from it and what our BeginWrite implementation is going to do. - return (Task)base.BeginWriteInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); - } - - return WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + return _strategy.WriteAsync(buffer, offset, count, cancellationToken); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { - if (GetType() != typeof(FileStream)) - { - // If this isn't a concrete FileStream, a derived type may have overridden WriteAsync(byte[],...), - // which was introduced first, so delegate to the base which will delegate to that. - return base.WriteAsync(buffer, cancellationToken); - } - if (cancellationToken.IsCancellationRequested) { return ValueTask.FromCanceled(cancellationToken); } - if (IsClosed) + if (_strategy.IsClosed) { throw Error.GetFileNotOpen(); } - if (!_useAsyncIO) - { - // If we weren't opened for asynchronous I/O, we still call to the base implementation so that - // Write is invoked asynchronously. But if we have a byte[], we can do so using the base Stream's - // internal helper that bypasses delegating to BeginWrite, since we already know this is FileStream - // rather than something derived from it and what our BeginWrite implementation is going to do. - return MemoryMarshal.TryGetArray(buffer, out ArraySegment segment) ? - new ValueTask((Task)BeginWriteInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : - base.WriteAsync(buffer, cancellationToken); - } - - return WriteAsyncInternal(buffer, cancellationToken); + return _strategy.WriteAsync(buffer, cancellationToken); } /// @@ -564,21 +305,16 @@ public override void Flush() /// public virtual void Flush(bool flushToDisk) { - if (IsClosed) throw Error.GetFileNotOpen(); - - FlushInternalBuffer(); + if (_strategy.IsClosed) throw Error.GetFileNotOpen(); - if (flushToDisk && CanWrite) - { - FlushOSBuffer(); - } + _strategy.Flush(flushToDisk); } /// Gets a value indicating whether the current stream supports reading. - public override bool CanRead => !_fileHandle.IsClosed && (_access & FileAccess.Read) != 0; + public override bool CanRead => _strategy.CanRead; /// Gets a value indicating whether the current stream supports writing. - public override bool CanWrite => !_fileHandle.IsClosed && (_access & FileAccess.Write) != 0; + public override bool CanWrite => _strategy.CanWrite; /// Validates arguments to Read and Write and throws resulting exceptions. /// The buffer to read from or write to. @@ -587,7 +323,7 @@ public virtual void Flush(bool flushToDisk) private void ValidateReadWriteArgs(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); - if (_fileHandle.IsClosed) + if (_strategy.IsClosed) throw Error.GetFileNotOpen(); } @@ -597,265 +333,69 @@ public override void SetLength(long value) { if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); - if (_fileHandle.IsClosed) + if (_strategy.IsClosed) throw Error.GetFileNotOpen(); if (!CanSeek) throw Error.GetSeekNotSupported(); if (!CanWrite) throw Error.GetWriteNotSupported(); - SetLengthInternal(value); + _strategy.SetLength(value); } - public virtual SafeFileHandle SafeFileHandle - { - get - { - Flush(); - _exposedHandle = true; - return _fileHandle; - } - } + public virtual SafeFileHandle SafeFileHandle => _strategy.SafeFileHandle; /// Gets the path that was passed to the constructor. - public virtual string Name => _path ?? SR.IO_UnknownFileName; + public virtual string Name => _strategy.Name; /// Gets a value indicating whether the stream was opened for I/O to be performed synchronously or asynchronously. - public virtual bool IsAsync => _useAsyncIO; + public virtual bool IsAsync => _strategy.IsAsync; /// Gets the length of the stream in bytes. public override long Length { get { - if (_fileHandle.IsClosed) throw Error.GetFileNotOpen(); + if (_strategy.IsClosed) throw Error.GetFileNotOpen(); if (!CanSeek) throw Error.GetSeekNotSupported(); - return GetLengthInternal(); - } - } - - /// - /// Verify that the actual position of the OS's handle equals what we expect it to. - /// This will fail if someone else moved the UnixFileStream's handle or if - /// our position updating code is incorrect. - /// - private void VerifyOSHandlePosition() - { - bool verifyPosition = _exposedHandle; // in release, only verify if we've given out the handle such that someone else could be manipulating it -#if DEBUG - verifyPosition = true; // in debug, always make sure our position matches what the OS says it should be -#endif - if (verifyPosition && CanSeek) - { - long oldPos = _filePosition; // SeekCore will override the current _position, so save it now - long curPos = SeekCore(_fileHandle, 0, SeekOrigin.Current); - if (oldPos != curPos) - { - // For reads, this is non-fatal but we still could have returned corrupted - // data in some cases, so discard the internal buffer. For writes, - // this is a problem; discard the buffer and error out. - _readPos = _readLength = 0; - if (_writePos > 0) - { - _writePos = 0; - throw new IOException(SR.IO_FileStreamHandlePosition); - } - } + return _strategy.Length; } } - /// Verifies that state relating to the read/write buffer is consistent. - [Conditional("DEBUG")] - private void AssertBufferInvariants() - { - // Read buffer values must be in range: 0 <= _bufferReadPos <= _bufferReadLength <= _bufferLength - Debug.Assert(0 <= _readPos && _readPos <= _readLength && _readLength <= _bufferLength); - - // Write buffer values must be in range: 0 <= _bufferWritePos <= _bufferLength - Debug.Assert(0 <= _writePos && _writePos <= _bufferLength); - - // Read buffering and write buffering can't both be active - Debug.Assert((_readPos == 0 && _readLength == 0) || _writePos == 0); - } - - /// Validates that we're ready to read from the stream. - private void PrepareForReading() - { - if (_fileHandle.IsClosed) - throw Error.GetFileNotOpen(); - if (_readLength == 0 && !CanRead) - throw Error.GetReadNotSupported(); - - AssertBufferInvariants(); - } - /// Gets or sets the position within the current stream public override long Position { get { - if (_fileHandle.IsClosed) + if (_strategy.IsClosed) throw Error.GetFileNotOpen(); if (!CanSeek) throw Error.GetSeekNotSupported(); - AssertBufferInvariants(); - VerifyOSHandlePosition(); - - // We may have read data into our buffer from the handle, such that the handle position - // is artificially further along than the consumer's view of the stream's position. - // Thus, when reading, our position is really starting from the handle position negatively - // offset by the number of bytes in the buffer and positively offset by the number of - // bytes into that buffer we've read. When writing, both the read length and position - // must be zero, and our position is just the handle position offset positive by how many - // bytes we've written into the buffer. - return (_filePosition - _readLength) + _readPos + _writePos; + return _strategy.Position; } set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); - Seek(value, SeekOrigin.Begin); - } - } - - internal virtual 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. - /// - /// The buffer. - private byte[] GetBuffer() - { - Debug.Assert(_buffer == null || _buffer.Length == _bufferLength); - if (_buffer == null) - { - _buffer = new byte[_bufferLength]; - OnBufferAllocated(); + _strategy.Seek(value, SeekOrigin.Begin); } - - return _buffer; - } - - partial void OnBufferAllocated(); - - /// - /// Flushes the internal read/write buffer for this stream. If write data has been buffered, - /// that data is written out to the underlying file. Or if data has been buffered for - /// reading from the stream, the data is dumped and our position in the underlying file - /// is rewound as necessary. This does not flush the OS buffer. - /// - private void FlushInternalBuffer() - { - AssertBufferInvariants(); - if (_writePos > 0) - { - FlushWriteBuffer(); - } - else if (_readPos < _readLength && CanSeek) - { - FlushReadBuffer(); - } - } - - /// Dumps any read data in the buffer and rewinds our position in the stream, accordingly, as necessary. - private void FlushReadBuffer() - { - // Reading is done by blocks from the file, but someone could read - // 1 byte from the buffer then write. At that point, the OS's file - // pointer is out of sync with the stream's position. All write - // functions should call this function to preserve the position in the file. - - AssertBufferInvariants(); - Debug.Assert(_writePos == 0, "FileStream: Write buffer must be empty in FlushReadBuffer!"); - - int rewind = _readPos - _readLength; - if (rewind != 0) - { - Debug.Assert(CanSeek, "FileStream will lose buffered read data now."); - SeekCore(_fileHandle, rewind, SeekOrigin.Current); - } - _readPos = _readLength = 0; } /// /// Reads a byte from the file stream. Returns the byte cast to an int /// or -1 if reading from the end of the stream. /// - public override int ReadByte() - { - PrepareForReading(); - - byte[] buffer = GetBuffer(); - if (_readPos == _readLength) - { - FlushWriteBuffer(); - _readLength = FillReadBufferForReadByte(); - _readPos = 0; - if (_readLength == 0) - { - return -1; - } - } - - return buffer[_readPos++]; - } + public override int ReadByte() => _strategy.ReadByte(); /// /// Writes a byte to the current position in the stream and advances the position /// within the stream by one byte. /// /// The byte to write to the stream. - public override void WriteByte(byte value) - { - PrepareForWriting(); - - // Flush the write buffer if it's full - if (_writePos == _bufferLength) - FlushWriteBufferForWriteByte(); - - // We now have space in the buffer. Store the byte. - GetBuffer()[_writePos++] = value; - } - - /// - /// Validates that we're ready to write to the stream, - /// including flushing a read buffer if necessary. - /// - private void PrepareForWriting() - { - if (_fileHandle.IsClosed) - throw Error.GetFileNotOpen(); - - // 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(); - FlushReadBuffer(); - Debug.Assert(_bufferLength > 0, "_bufferSize > 0"); - } - } + public override void WriteByte(byte value) => _strategy.WriteByte(value); ~FileStream() { @@ -865,28 +405,23 @@ private void PrepareForWriting() Dispose(false); } - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + protected override void Dispose(bool disposing) { - ValidateBufferArguments(buffer, offset, count); - if (IsClosed) throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed); - if (!CanRead) throw new NotSupportedException(SR.NotSupported_UnreadableStream); - - if (!IsAsync) - return base.BeginRead(buffer, offset, count, callback, state); - else - return TaskToApm.Begin(ReadAsyncTask(buffer, offset, count, CancellationToken.None), callback, state); + _strategy?.DisposeInternal(disposing); // null _strategy possible in finalizer } - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + public override ValueTask DisposeAsync() => _strategy.DisposeAsync(); + + 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 (IsClosed) throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed); - if (!CanWrite) throw new NotSupportedException(SR.NotSupported_UnwritableStream); + if (_strategy.IsClosed) throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed); + if (!CanRead) throw new NotSupportedException(SR.NotSupported_UnreadableStream); - if (!IsAsync) - return base.BeginWrite(buffer, offset, count, callback, state); - else - return TaskToApm.Begin(WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), CancellationToken.None).AsTask(), callback, state); + return _strategy.BeginRead(buffer, offset, count, callback, state); } public override int EndRead(IAsyncResult asyncResult) @@ -894,10 +429,16 @@ public override int EndRead(IAsyncResult asyncResult) if (asyncResult == null) throw new ArgumentNullException(nameof(asyncResult)); - if (!IsAsync) - return base.EndRead(asyncResult); - else - return TaskToApm.End(asyncResult); + return _strategy.EndRead(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); + + return _strategy.BeginWrite(buffer, offset, count, callback, state); } public override void EndWrite(IAsyncResult asyncResult) @@ -905,10 +446,45 @@ public override void EndWrite(IAsyncResult asyncResult) if (asyncResult == null) throw new ArgumentNullException(nameof(asyncResult)); - if (!IsAsync) - base.EndWrite(asyncResult); - else - TaskToApm.End(asyncResult); + _strategy.EndWrite(asyncResult); } + + public override bool CanSeek => _strategy.CanSeek; + + public override long Seek(long offset, SeekOrigin origin) => _strategy.Seek(offset, origin); + + internal Task BaseFlushAsync(CancellationToken cancellationToken) + => base.FlushAsync(cancellationToken); + + internal int BaseRead(Span buffer) => base.Read(buffer); + + internal Task BaseReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => base.ReadAsync(buffer, offset, count, cancellationToken); + + internal ValueTask BaseReadAsync(Memory buffer, CancellationToken cancellationToken = default) + => base.ReadAsync(buffer, cancellationToken); + + internal void BaseWrite(ReadOnlySpan buffer) => base.Write(buffer); + + internal Task BaseWriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => base.WriteAsync(buffer, offset, count, cancellationToken); + + internal ValueTask BaseWriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + => base.WriteAsync(buffer, cancellationToken); + + internal ValueTask BaseDisposeAsync() => base.DisposeAsync(); + + internal Task BaseCopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + => base.CopyToAsync(destination, bufferSize, cancellationToken); + + internal IAsyncResult BaseBeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => base.BeginRead(buffer, offset, count, callback, state); + + internal int BaseEndRead(IAsyncResult asyncResult) => base.EndRead(asyncResult); + + internal IAsyncResult BaseBeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => base.BeginWrite(buffer, offset, count, callback, state); + + internal void BaseEndWrite(IAsyncResult asyncResult) => base.EndWrite(asyncResult); } } 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 226b5732e0a09b..c7b56290ca7eee 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 @@ -9,7 +9,7 @@ namespace System.IO { - public partial class FileStream : Stream + internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy { // This is an internal object extending TaskCompletionSource with fields // for all of the relevant data necessary to complete the IO operation. @@ -25,7 +25,7 @@ private unsafe class FileStreamCompletionSource : TaskCompletionSource private static Action? s_cancelCallback; - private readonly FileStream _stream; + private readonly LegacyFileStreamStrategy _stream; private readonly int _numBufferedBytes; private CancellationTokenRegistration _cancellationRegistration; #if DEBUG @@ -35,7 +35,7 @@ private unsafe class FileStreamCompletionSource : TaskCompletionSource 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(FileStream stream, int numBufferedBytes, byte[]? bytes) + protected FileStreamCompletionSource(LegacyFileStreamStrategy stream, int numBufferedBytes, byte[]? bytes) : base(TaskCreationOptions.RunContinuationsAsynchronously) { _numBufferedBytes = numBufferedBytes; @@ -132,8 +132,8 @@ internal static void IOCallback(uint errorCode, uint numBytes, NativeOverlapped* // 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 FileStream || state is FileStreamCompletionSource); - FileStreamCompletionSource completionSource = state is FileStream fs ? + 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); @@ -220,7 +220,7 @@ private static void Cancel(object? state) } } - public static FileStreamCompletionSource Create(FileStream stream, int numBufferedBytesRead, ReadOnlyMemory memory) + public static FileStreamCompletionSource Create(LegacyFileStreamStrategy stream, int numBufferedBytesRead, ReadOnlyMemory memory) { // If the memory passed in is the stream's internal buffer, we can use the base FileStreamCompletionSource, // which has a PreAllocatedOverlapped with the memory already pinned. Otherwise, we use the derived @@ -241,7 +241,7 @@ private sealed class MemoryFileStreamCompletionSource : FileStreamCompletionSour { private MemoryHandle _handle; // mutable struct; do not make this readonly - internal MemoryFileStreamCompletionSource(FileStream stream, int numBufferedBytes, ReadOnlyMemory 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(); 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 new file mode 100644 index 00000000000000..471005b833f746 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Unix.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32.SafeHandles; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO +{ + // this type defines a set of stateless FileStream/FileStreamStrategy helper methods + 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(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 SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + { + // Translate the arguments into arguments for an open call. + Interop.Sys.OpenFlags openFlags = PreOpenConfigurationFromOptions(mode, access, share, options); + + // If the file gets created a new, we'll select the permissions for it. Most Unix utilities by default use 666 (read and + // write for all), so we do the same (even though this doesn't match Windows, where by default it's possible to write out + // a file and then execute it). No matter what we choose, it'll be subject to the umask applied by the system, such that the + // actual permissions will typically be less than what we select here. + const Interop.Sys.Permissions OpenPermissions = + Interop.Sys.Permissions.S_IRUSR | Interop.Sys.Permissions.S_IWUSR | + Interop.Sys.Permissions.S_IRGRP | Interop.Sys.Permissions.S_IWGRP | + Interop.Sys.Permissions.S_IROTH | Interop.Sys.Permissions.S_IWOTH; + + // Open the file and store the safe handle. + return SafeFileHandle.Open(path!, openFlags, (int)OpenPermissions); + } + + internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsync) => handle.IsAsync ?? defaultIsAsync; + + /// Translates the FileMode, FileAccess, and FileOptions values into flags to be passed when opening the file. + /// The FileMode provided to the stream's constructor. + /// The FileAccess provided to the stream's constructor + /// The FileShare provided to the stream's constructor + /// The FileOptions provided to the stream's constructor + /// The flags value to be passed to the open system call. + private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mode, FileAccess access, FileShare share, FileOptions options) + { + // Translate FileMode. Most of the values map cleanly to one or more options for open. + Interop.Sys.OpenFlags flags = default; + switch (mode) + { + default: + case FileMode.Open: // Open maps to the default behavior for open(...). No flags needed. + case FileMode.Truncate: // We truncate the file after getting the lock + break; + + case FileMode.Append: // Append is the same as OpenOrCreate, except that we'll also separately jump to the end later + case FileMode.OpenOrCreate: + case FileMode.Create: // We truncate the file after getting the lock + flags |= Interop.Sys.OpenFlags.O_CREAT; + break; + + case FileMode.CreateNew: + flags |= (Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL); + break; + } + + // Translate FileAccess. All possible values map cleanly to corresponding values for open. + switch (access) + { + case FileAccess.Read: + flags |= Interop.Sys.OpenFlags.O_RDONLY; + break; + + case FileAccess.ReadWrite: + flags |= Interop.Sys.OpenFlags.O_RDWR; + break; + + case FileAccess.Write: + flags |= Interop.Sys.OpenFlags.O_WRONLY; + break; + } + + // Handle Inheritable, other FileShare flags are handled by Init + if ((share & FileShare.Inheritable) == 0) + { + flags |= Interop.Sys.OpenFlags.O_CLOEXEC; + } + + // Translate some FileOptions; some just aren't supported, and others will be handled after calling open. + // - Asynchronous: Handled in ctor, setting _useAsync and SafeFileHandle.IsAsync to true + // - DeleteOnClose: Doesn't have a Unix equivalent, but we approximate it in Dispose + // - Encrypted: No equivalent on Unix and is ignored + // - RandomAccess: Implemented after open if posix_fadvise is available + // - SequentialScan: Implemented after open if posix_fadvise is available + // - WriteThrough: Handled here + if ((options & FileOptions.WriteThrough) != 0) + { + flags |= Interop.Sys.OpenFlags.O_SYNC; + } + + return flags; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Win32.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs similarity index 52% rename from src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Win32.cs rename to src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs index aee2562cc9dde8..66dd3cb259e4fc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Win32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamHelpers.Windows.cs @@ -2,24 +2,30 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; namespace System.IO { - public partial class FileStream : Stream + // this type defines a set of stateless FileStream/FileStreamStrategy helper methods + internal static class FileStreamHelpers { - private SafeFileHandle OpenHandle(FileMode mode, FileShare share, FileOptions options) - { - return CreateFileOpenHandle(mode, share, options); - } + 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(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + => new LegacyFileStreamStrategy(fileStream, path, mode, access, share, bufferSize, options); - private unsafe SafeFileHandle CreateFileOpenHandle(FileMode mode, FileShare share, FileOptions options) + internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + => CreateFileOpenHandle(path, mode, access, share, options); + + private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) { Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = GetSecAttrs(share); int fAccess = - ((_access & FileAccess.Read) == FileAccess.Read ? Interop.Kernel32.GenericOperations.GENERIC_READ : 0) | - ((_access & FileAccess.Write) == FileAccess.Write ? Interop.Kernel32.GenericOperations.GENERIC_WRITE : 0); + ((access & FileAccess.Read) == FileAccess.Read ? Interop.Kernel32.GenericOperations.GENERIC_READ : 0) | + ((access & FileAccess.Write) == FileAccess.Write ? Interop.Kernel32.GenericOperations.GENERIC_WRITE : 0); // Our Inheritable bit was stolen from Windows, but should be set in // the security attributes class. Don't leave this bit set. @@ -39,15 +45,17 @@ private unsafe SafeFileHandle CreateFileOpenHandle(FileMode mode, FileShare shar using (DisableMediaInsertionPrompt.Create()) { - Debug.Assert(_path != null); + Debug.Assert(path != null); return ValidateFileHandle( - Interop.Kernel32.CreateFile(_path, fAccess, share, &secAttrs, mode, flagsAndAttributes, IntPtr.Zero)); + Interop.Kernel32.CreateFile(path, fAccess, share, &secAttrs, mode, flagsAndAttributes, IntPtr.Zero), + path, + (options & FileOptions.Asynchronous) != 0); } } - private static bool GetDefaultIsAsync(SafeFileHandle handle) + internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsync) { - return handle.IsAsync ?? !IsHandleSynchronous(handle, ignoreInvalid: true) ?? DefaultIsAsync; + return handle.IsAsync ?? !IsHandleSynchronous(handle, ignoreInvalid: true) ?? defaultIsAsync; } private static unsafe bool? IsHandleSynchronous(SafeFileHandle fileHandle, bool ignoreInvalid) @@ -57,7 +65,6 @@ private static bool GetDefaultIsAsync(SafeFileHandle handle) uint fileMode; - int status = Interop.NtDll.NtQueryInformationFile( FileHandle: fileHandle, IoStatusBlock: out _, @@ -89,7 +96,7 @@ private static bool GetDefaultIsAsync(SafeFileHandle handle) return (fileMode & (Interop.NtDll.FILE_SYNCHRONOUS_IO_ALERT | Interop.NtDll.FILE_SYNCHRONOUS_IO_NONALERT)) > 0; } - private static void VerifyHandleIsSync(SafeFileHandle handle) + internal static void VerifyHandleIsSync(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. @@ -102,5 +109,40 @@ private static void VerifyHandleIsSync(SafeFileHandle handle) if (!(IsHandleSynchronous(handle, ignoreInvalid: false) ?? true)) throw new ArgumentException(SR.Arg_HandleNotSync, nameof(handle)); } + + private static unsafe Interop.Kernel32.SECURITY_ATTRIBUTES GetSecAttrs(FileShare share) + { + Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = default; + if ((share & FileShare.Inheritable) != 0) + { + secAttrs = new Interop.Kernel32.SECURITY_ATTRIBUTES + { + nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES), + bInheritHandle = Interop.BOOL.TRUE + }; + } + return secAttrs; + } + + private static SafeFileHandle ValidateFileHandle(SafeFileHandle fileHandle, string path, bool useAsyncIO) + { + if (fileHandle.IsInvalid) + { + // Return a meaningful exception with the full path. + + // NT5 oddity - when trying to open "C:\" as a Win32FileStream, + // we usually get ERROR_PATH_NOT_FOUND from the OS. We should + // probably be consistent w/ every other directory. + int errorCode = Marshal.GetLastWin32Error(); + + if (errorCode == Interop.Errors.ERROR_PATH_NOT_FOUND && path!.Length == PathInternal.GetRootLength(path)) + errorCode = Interop.Errors.ERROR_ACCESS_DENIED; + + throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); + } + + fileHandle.IsAsync = useAsyncIO; + return fileHandle; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs new file mode 100644 index 00000000000000..97b5969355383c --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamStrategy.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32.SafeHandles; + +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; } + + internal abstract SafeFileHandle SafeFileHandle { get; } + + internal IntPtr Handle => SafeFileHandle.DangerousGetHandle(); + + internal abstract bool IsClosed { get; } + + internal abstract void Lock(long position, long length); + + internal abstract void Unlock(long position, long length); + + internal abstract void Flush(bool flushToDisk); + + internal abstract void DisposeInternal(bool disposing); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Lock.OSX.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Lock.OSX.cs similarity index 65% rename from src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Lock.OSX.cs rename to src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Lock.OSX.cs index 315f5a5997de6a..599b51694d179e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Lock.OSX.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Lock.OSX.cs @@ -3,14 +3,14 @@ namespace System.IO { - public partial class FileStream : Stream + internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy { - private static void LockInternal(long position, long length) + internal override void Lock(long position, long length) { throw new PlatformNotSupportedException(SR.PlatformNotSupported_OSXFileLocking); } - private static void UnlockInternal(long position, long length) + internal override void Unlock(long position, long length) { throw new PlatformNotSupportedException(SR.PlatformNotSupported_OSXFileLocking); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Lock.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Lock.Unix.cs similarity index 82% rename from src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Lock.Unix.cs rename to src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Lock.Unix.cs index 1869552a55471c..5233dcdb7087f5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Lock.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Lock.Unix.cs @@ -3,12 +3,12 @@ namespace System.IO { - public partial class FileStream : Stream + internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy { /// Prevents other processes from reading from or writing to the FileStream. /// The beginning of the range to lock. /// The range to be locked. - private void LockInternal(long position, long length) + internal override void Lock(long position, long length) { CheckFileCall(Interop.Sys.LockFileRegion(_fileHandle, position, length, CanWrite ? Interop.Sys.LockType.F_WRLCK : Interop.Sys.LockType.F_RDLCK)); } @@ -16,7 +16,7 @@ private void LockInternal(long position, long length) /// Allows access by other processes to all or part of a file that was previously locked. /// The beginning of the range to unlock. /// The range to be unlocked. - private void UnlockInternal(long position, long length) + internal override void Unlock(long position, long length) { CheckFileCall(Interop.Sys.LockFileRegion(_fileHandle, position, length, Interop.Sys.LockType.F_UNLCK)); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs similarity index 83% rename from src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Unix.cs rename to src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs index 38dc6d2d01174d..550b2acab25051 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Unix.cs @@ -11,7 +11,7 @@ namespace System.IO { /// Provides an implementation of a file stream for Unix files. - public partial class FileStream : Stream + internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy { /// File mode. private FileMode _mode; @@ -34,7 +34,12 @@ public partial class FileStream : Stream /// Lazily-initialized value for whether the file supports seeking. private bool? _canSeek; - private SafeFileHandle OpenHandle(FileMode mode, FileShare share, FileOptions options) + /// Initializes a stream for reading or writing a Unix file. + /// How the file should be opened. + /// What other access to the file should be allowed. This is currently ignored. + /// The original path specified for the FileStream. + /// Options, passed via arguments as we have no guarantee that _options field was already set. + private void Init(FileMode mode, FileShare share, string originalPath, FileOptions options) { // FileStream performs most of the general argument validation. We can assume here that the arguments // are all checked and consistent (e.g. non-null-or-empty path; valid enums in mode, access, share, and options; etc.) @@ -45,30 +50,6 @@ private SafeFileHandle OpenHandle(FileMode mode, FileShare share, FileOptions op if (_useAsyncIO) _asyncState = new AsyncState(); - // Translate the arguments into arguments for an open call. - Interop.Sys.OpenFlags openFlags = PreOpenConfigurationFromOptions(mode, _access, share, options); - - // If the file gets created a new, we'll select the permissions for it. Most Unix utilities by default use 666 (read and - // write for all), so we do the same (even though this doesn't match Windows, where by default it's possible to write out - // a file and then execute it). No matter what we choose, it'll be subject to the umask applied by the system, such that the - // actual permissions will typically be less than what we select here. - const Interop.Sys.Permissions OpenPermissions = - Interop.Sys.Permissions.S_IRUSR | Interop.Sys.Permissions.S_IWUSR | - Interop.Sys.Permissions.S_IRGRP | Interop.Sys.Permissions.S_IWGRP | - Interop.Sys.Permissions.S_IROTH | Interop.Sys.Permissions.S_IWOTH; - - // Open the file and store the safe handle. - return SafeFileHandle.Open(_path!, openFlags, (int)OpenPermissions); - } - - private static bool GetDefaultIsAsync(SafeFileHandle handle) => handle.IsAsync ?? DefaultIsAsync; - - /// Initializes a stream for reading or writing a Unix file. - /// How the file should be opened. - /// What other access to the file should be allowed. This is currently ignored. - /// The original path specified for the FileStream. - private void Init(FileMode mode, FileShare share, string originalPath) - { _fileHandle.IsAsync = _useAsyncIO; // Lock the file if requested via FileShare. This is only advisory locking. FileShare.None implies an exclusive @@ -92,8 +73,8 @@ private void Init(FileMode mode, FileShare share, string originalPath) // and Sequential together doesn't make sense as they are two competing options on the same spectrum, // so if both are specified, we prefer RandomAccess (behavior on Windows is unspecified if both are provided). Interop.Sys.FileAdvice fadv = - (_options & FileOptions.RandomAccess) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_RANDOM : - (_options & FileOptions.SequentialScan) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_SEQUENTIAL : + (options & FileOptions.RandomAccess) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_RANDOM : + (options & FileOptions.SequentialScan) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_SEQUENTIAL : 0; if (fadv != 0) { @@ -101,7 +82,7 @@ private void Init(FileMode mode, FileShare share, string originalPath) ignoreNotSupported: true); // just a hint. } - if (_mode == FileMode.Append) + if (mode == FileMode.Append) { // Jump to the end of the file if opened as Append. _appendStart = SeekCore(_fileHandle, 0, SeekOrigin.End); @@ -134,71 +115,6 @@ private void InitFromHandle(SafeFileHandle handle, FileAccess access, bool useAs SeekCore(handle, 0, SeekOrigin.Current); } - /// Translates the FileMode, FileAccess, and FileOptions values into flags to be passed when opening the file. - /// The FileMode provided to the stream's constructor. - /// The FileAccess provided to the stream's constructor - /// The FileShare provided to the stream's constructor - /// The FileOptions provided to the stream's constructor - /// The flags value to be passed to the open system call. - private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mode, FileAccess access, FileShare share, FileOptions options) - { - // Translate FileMode. Most of the values map cleanly to one or more options for open. - Interop.Sys.OpenFlags flags = default; - switch (mode) - { - default: - case FileMode.Open: // Open maps to the default behavior for open(...). No flags needed. - case FileMode.Truncate: // We truncate the file after getting the lock - break; - - case FileMode.Append: // Append is the same as OpenOrCreate, except that we'll also separately jump to the end later - case FileMode.OpenOrCreate: - case FileMode.Create: // We truncate the file after getting the lock - flags |= Interop.Sys.OpenFlags.O_CREAT; - break; - - case FileMode.CreateNew: - flags |= (Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL); - break; - } - - // Translate FileAccess. All possible values map cleanly to corresponding values for open. - switch (access) - { - case FileAccess.Read: - flags |= Interop.Sys.OpenFlags.O_RDONLY; - break; - - case FileAccess.ReadWrite: - flags |= Interop.Sys.OpenFlags.O_RDWR; - break; - - case FileAccess.Write: - flags |= Interop.Sys.OpenFlags.O_WRONLY; - break; - } - - // Handle Inheritable, other FileShare flags are handled by Init - if ((share & FileShare.Inheritable) == 0) - { - flags |= Interop.Sys.OpenFlags.O_CLOEXEC; - } - - // Translate some FileOptions; some just aren't supported, and others will be handled after calling open. - // - Asynchronous: Handled in ctor, setting _useAsync and SafeFileHandle.IsAsync to true - // - DeleteOnClose: Doesn't have a Unix equivalent, but we approximate it in Dispose - // - Encrypted: No equivalent on Unix and is ignored - // - RandomAccess: Implemented after open if posix_fadvise is available - // - SequentialScan: Implemented after open if posix_fadvise is available - // - WriteThrough: Handled here - if ((options & FileOptions.WriteThrough) != 0) - { - flags |= Interop.Sys.OpenFlags.O_SYNC; - } - - return flags; - } - /// Gets a value indicating whether the current stream supports seeking. public override bool CanSeek => CanSeekCore(_fileHandle); @@ -223,21 +139,24 @@ private bool CanSeekCore(SafeFileHandle fileHandle) return _canSeek.GetValueOrDefault(); } - private long GetLengthInternal() + public override long Length { - // Get the length of the file as reported by the OS - Interop.Sys.FileStatus status; - CheckFileCall(Interop.Sys.FStat(_fileHandle, out status)); - long length = status.Size; - - // But we may have buffered some data to be written that puts our length - // beyond what the OS is aware of. Update accordingly. - if (_writePos > 0 && _filePosition + _writePos > length) + get { - length = _writePos + _filePosition; - } + // Get the length of the file as reported by the OS + Interop.Sys.FileStatus status; + CheckFileCall(Interop.Sys.FStat(_fileHandle, out status)); + long length = status.Size; - return length; + // But we may have buffered some data to be written that puts our length + // beyond what the OS is aware of. Update accordingly. + if (_writePos > 0 && _filePosition + _writePos > length) + { + length = _writePos + _filePosition; + } + + return length; + } } /// Releases the unmanaged resources used by the stream. @@ -305,7 +224,7 @@ public override ValueTask DisposeAsync() // override may already exist on a derived type. if (_useAsyncIO && _writePos > 0) { - return new ValueTask(Task.Factory.StartNew(static s => ((FileStream)s!).Dispose(), this, + return new ValueTask(Task.Factory.StartNew(static s => ((LegacyFileStreamStrategy)s!).Dispose(), this, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default)); } @@ -342,7 +261,7 @@ private void FlushWriteBufferForWriteByte() } /// Writes any data in the write buffer to the underlying stream and resets the buffer. - private void FlushWriteBuffer() + private void FlushWriteBuffer(bool calledFromFinalizer = false) { AssertBufferInvariants(); if (_writePos > 0) @@ -354,7 +273,7 @@ private void FlushWriteBuffer() /// Sets the length of this stream to the given value. /// The new length of the stream. - private void SetLengthInternal(long value) + public override void SetLength(long value) { FlushInternalBuffer(); @@ -530,7 +449,7 @@ private unsafe int ReadNative(Span buffer) // whereas on Windows it may happen before the write has completed. Debug.Assert(t.Status == TaskStatus.RanToCompletion); - var thisRef = (FileStream)s!; + var thisRef = (LegacyFileStreamStrategy)s!; Debug.Assert(thisRef._asyncState != null); try { @@ -691,7 +610,7 @@ private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationTo // whereas on Windows it may happen before the write has completed. Debug.Assert(t.Status == TaskStatus.RanToCompletion); - var thisRef = (FileStream)s!; + var thisRef = (LegacyFileStreamStrategy)s!; Debug.Assert(thisRef._asyncState != null); try { @@ -703,11 +622,6 @@ private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationTo }, this, CancellationToken.None, TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default)); } - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => - // Windows version overrides this method, so the Unix version does as well, but it doesn't - // currently have any special optimizations to be done and so just calls to the base. - base.CopyToAsync(destination, bufferSize, cancellationToken); - /// Sets the current position of this stream to the given value. /// The point relative to origin from which to begin seeking. /// @@ -772,10 +686,11 @@ public override long Seek(long offset, SeekOrigin origin) /// Specifies the beginning, the end, or the current position as a reference /// point for offset, using a value of type SeekOrigin. /// + /// not used in Unix implementation /// The new position in the stream. - private long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin) + private long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin, bool closeInvalidHandle = false) { - Debug.Assert(!fileHandle.IsClosed && (GetType() != typeof(FileStream) || CanSeekCore(fileHandle))); // verify that we can seek, but only if CanSeek won't be a virtual call (which could happen in the ctor) + Debug.Assert(!fileHandle.IsClosed && CanSeekCore(fileHandle)); Debug.Assert(origin >= SeekOrigin.Begin && origin <= SeekOrigin.End); long pos = CheckFileCall(Interop.Sys.LSeek(fileHandle, offset, (Interop.Sys.SeekWhence)(int)origin)); // SeekOrigin values are the same as Interop.libc.SeekWhence values diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs similarity index 95% rename from src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Windows.cs rename to src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs index 6e7b5480509e07..4eaf3d69d0e2fe 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.Windows.cs @@ -39,7 +39,7 @@ namespace System.IO { - public partial class FileStream : Stream + internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy { private bool _canSeek; private bool _isPipe; // Whether to disable async buffering code. @@ -51,7 +51,7 @@ public partial class FileStream : Stream 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) + private void Init(FileMode mode, FileShare share, string originalPath, FileOptions options) { if (!PathInternal.IsExtended(originalPath)) { @@ -172,7 +172,7 @@ private void InitFromHandleImpl(SafeFileHandle handle, bool useAsyncIO) } else if (!useAsyncIO) { - VerifyHandleIsSync(handle); + FileStreamHelpers.VerifyHandleIsSync(handle); } if (_canSeek) @@ -181,39 +181,28 @@ private void InitFromHandleImpl(SafeFileHandle handle, bool useAsyncIO) _filePosition = 0; } - private static unsafe Interop.Kernel32.SECURITY_ATTRIBUTES GetSecAttrs(FileShare share) - { - Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = default; - if ((share & FileShare.Inheritable) != 0) - { - secAttrs = new Interop.Kernel32.SECURITY_ATTRIBUTES - { - nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES), - bInheritHandle = Interop.BOOL.TRUE - }; - } - return secAttrs; - } - private bool HasActiveBufferOperation => !_activeBufferOperation.IsCompleted; public override bool CanSeek => _canSeek; - private unsafe long GetLengthInternal() + public unsafe override long Length { - Interop.Kernel32.FILE_STANDARD_INFO info; + 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; + if (!Interop.Kernel32.GetFileInformationByHandleEx(_fileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))) + throw Win32Marshal.GetExceptionForLastWin32Error(_path); + long len = info.EndOfFile; - // If we're writing near the end of the file, we must include our - // internal buffer in our Length calculation. Don't flush because - // we use the length of the file in our async write method. - if (_writePos > 0 && _filePosition + _writePos > len) - len = _writePos + _filePosition; + // If we're writing near the end of the file, we must include our + // internal buffer in our Length calculation. Don't flush because + // we use the length of the file in our async write method. + if (_writePos > 0 && _filePosition + _writePos > len) + len = _writePos + _filePosition; - return len; + return len; + } } protected override void Dispose(bool disposing) @@ -261,12 +250,7 @@ protected override void Dispose(bool disposing) } } - public override ValueTask DisposeAsync() => - GetType() == typeof(FileStream) ? - DisposeAsyncCore() : - base.DisposeAsync(); - - private async ValueTask DisposeAsyncCore() + public override async ValueTask DisposeAsync() { // Same logic as in Dispose(), except with async counterparts. // TODO: https://github.com/dotnet/runtime/issues/27643: FlushAsync does synchronous work. @@ -274,7 +258,7 @@ private async ValueTask DisposeAsyncCore() { if (_fileHandle != null && !_fileHandle.IsClosed && _writePos > 0) { - await FlushAsyncInternal(default).ConfigureAwait(false); + await FlushAsync(default).ConfigureAwait(false); } } finally @@ -358,7 +342,7 @@ private void FlushWriteBuffer(bool calledFromFinalizer = false) _writePos = 0; } - private void SetLengthInternal(long value) + public override void SetLength(long value) { // Handle buffering updates. if (_writePos > 0) @@ -1244,10 +1228,8 @@ private int GetLastWin32ErrorAndDisposeHandleIfInvalid() public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { // If we're in sync mode, just use the shared CopyToAsync implementation that does - // typical read/write looping. We also need to take this path if this is a derived - // instance from FileStream, as a derived type could have overridden ReadAsync, in which - // case our custom CopyToAsync implementation isn't necessarily correct. - if (!_useAsyncIO || GetType() != typeof(FileStream)) + // typical read/write looping. + if (!_useAsyncIO) { return base.CopyToAsync(destination, bufferSize, cancellationToken); } @@ -1474,7 +1456,7 @@ private sealed unsafe class AsyncCopyToAwaitable : ICriticalNotifyCompletion internal static readonly IOCompletionCallback s_callback = IOCallback; /// The FileStream that owns this instance. - internal readonly FileStream _fileStream; + internal readonly LegacyFileStreamStrategy _fileStream; /// Tracked position representing the next location from which to read. internal long _position; @@ -1495,7 +1477,7 @@ private sealed unsafe class AsyncCopyToAwaitable : ICriticalNotifyCompletion internal object CancellationLock => this; /// Initialize the awaitable. - internal AsyncCopyToAwaitable(FileStream fileStream) + internal AsyncCopyToAwaitable(LegacyFileStreamStrategy fileStream) { _fileStream = fileStream; } @@ -1547,7 +1529,7 @@ public void UnsafeOnCompleted(Action continuation) } } - private void LockInternal(long position, long length) + internal override void Lock(long position, long length) { int positionLow = unchecked((int)(position)); int positionHigh = unchecked((int)(position >> 32)); @@ -1560,7 +1542,7 @@ private void LockInternal(long position, long length) } } - private void UnlockInternal(long position, long length) + internal override void Unlock(long position, long length) { int positionLow = unchecked((int)(position)); int positionHigh = unchecked((int)(position >> 32)); @@ -1572,26 +1554,5 @@ private void UnlockInternal(long position, long length) throw Win32Marshal.GetExceptionForLastWin32Error(_path); } } - - private SafeFileHandle ValidateFileHandle(SafeFileHandle fileHandle) - { - if (fileHandle.IsInvalid) - { - // Return a meaningful exception with the full path. - - // NT5 oddity - when trying to open "C:\" as a Win32FileStream, - // we usually get ERROR_PATH_NOT_FOUND from the OS. We should - // probably be consistent w/ every other directory. - int errorCode = Marshal.GetLastWin32Error(); - - if (errorCode == Interop.Errors.ERROR_PATH_NOT_FOUND && _path!.Length == PathInternal.GetRootLength(_path)) - errorCode = Interop.Errors.ERROR_ACCESS_DENIED; - - throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); - } - - fileHandle.IsAsync = _useAsyncIO; - return fileHandle; - } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs new file mode 100644 index 00000000000000..01e5db4f0827c0 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/LegacyFileStreamStrategy.cs @@ -0,0 +1,552 @@ +// 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 +{ + // This type is partial so we can avoid code duplication between Windows and Unix Legacy implementations + internal sealed partial class LegacyFileStreamStrategy : FileStreamStrategy + { + private byte[]? _buffer; + private readonly int _bufferLength; + private readonly SafeFileHandle _fileHandle; // only ever null if ctor throws + + /// Whether the file is opened for reading, writing, or both. + private readonly FileAccess _access; + + /// The path to the opened file. + private readonly string? _path; + + /// The next available byte to be read from the _buffer. + private int _readPos; + + /// The number of valid bytes in _buffer. + private int _readLength; + + /// The next location in which a write should occur to the buffer. + private int _writePos; + + /// + /// Whether asynchronous read/write/flush operations should be performed using async I/O. + /// On Windows FileOptions.Asynchronous controls how the file handle is configured, + /// and then as a result how operations are issued against that file handle. On Unix, + /// there isn't any distinction around how file descriptors are created for async vs + /// sync, but we still differentiate how the operations are issued in order to provide + /// similar behavioral semantics and performance characteristics as on Windows. On + /// Windows, if non-async, async read/write requests just delegate to the base stream, + /// and no attempt is made to synchronize between sync and async operations on the stream; + /// if async, then async read/write requests are implemented specially, and sync read/write + /// requests are coordinated with async ones by implementing the sync ones over the async + /// ones. On Unix, we do something similar. If non-async, async read/write requests just + /// delegate to the base stream, and no attempt is made to synchronize. If async, we use + /// a semaphore to coordinate both sync and async operations. + /// + private readonly bool _useAsyncIO; + + /// cached task for read ops that complete synchronously + private Task? _lastSynchronouslyCompletedTask; + + /// + /// Currently cached position in the stream. This should always mirror the underlying file's actual position, + /// and should only ever be out of sync if another stream with access to this same file manipulates it, at which + /// point we attempt to error out. + /// + private long _filePosition; + + /// Whether the file stream's handle has been exposed. + private bool _exposedHandle; + + internal LegacyFileStreamStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) : base(fileStream) + { + _exposedHandle = true; + _bufferLength = bufferSize; + + InitFromHandle(handle, access, isAsync); + + // Note: It would be cleaner to set the following fields in ValidateHandle, + // but we can't as they're readonly. + _access = access; + _useAsyncIO = isAsync; + + // As the handle was passed in, we must set the handle field at the very end to + // avoid the finalizer closing the handle when we throw errors. + _fileHandle = handle; + } + + internal LegacyFileStreamStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) : base(fileStream) + { + string fullPath = Path.GetFullPath(path); + + _path = fullPath; + _access = access; + _bufferLength = bufferSize; + + if ((options & FileOptions.Asynchronous) != 0) + _useAsyncIO = true; + + _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options); + + try + { + Init(mode, share, path, options); + } + catch + { + // If anything goes wrong while setting up the stream, make sure we deterministically dispose + // of the opened handle. + _fileHandle.Dispose(); + _fileHandle = null!; + throw; + } + } + + ~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); + } + + internal override void DisposeInternal(bool disposing) => Dispose(disposing); + + public override Task FlushAsync(CancellationToken cancellationToken) + { + // TODO: https://github.com/dotnet/runtime/issues/27643 (stop doing this synchronous work!!). + // The always synchronous data transfer between the OS and the internal buffer is intentional + // because this is needed to allow concurrent async IO requests. Concurrent data transfer + // between the OS and the internal buffer will result in race conditions. Since FlushWrite and + // FlushRead modify internal state of the stream and transfer data between the OS and the + // internal buffer, they cannot be truly async. We will, however, flush the OS file buffers + // asynchronously because it doesn't modify any internal state of the stream and is potentially + // a long running process. + try + { + FlushInternalBuffer(); + } + catch (Exception e) + { + return Task.FromException(e); + } + + return Task.CompletedTask; + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _useAsyncIO ? + ReadAsyncTask(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult() : + ReadSpan(new Span(buffer, offset, count)); + } + + public override int Read(Span buffer) + { + if (!_useAsyncIO) + { + if (_fileHandle.IsClosed) + { + throw Error.GetFileNotOpen(); + } + + return ReadSpan(buffer); + } + + // If the stream is in async mode, we can't call the synchronous ReadSpan, so we similarly call the base Read, + // which will turn delegate to Read(byte[],int,int), which will do the right thing if we're in async mode. + return base.Read(buffer); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (!_useAsyncIO) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Read is invoked asynchronously. But we can do so using the base Stream's internal helper + // that bypasses delegating to BeginRead, since we already know this is FileStream rather + // than something derived from it and what our BeginRead implementation is going to do. + return (Task)base.BeginReadInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); + } + + return ReadAsyncTask(buffer, offset, count, cancellationToken); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (!_useAsyncIO) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Read is invoked asynchronously. But if we have a byte[], we can do so using the base Stream's + // internal helper that bypasses delegating to BeginRead, since we already know this is FileStream + // rather than something derived from it and what our BeginRead implementation is going to do. + return MemoryMarshal.TryGetArray(buffer, out ArraySegment segment) ? + new ValueTask((Task)base.BeginReadInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : + base.ReadAsync(buffer, cancellationToken); + } + + Task? t = ReadAsyncInternal(buffer, cancellationToken, out int synchronousResult); + return t != null ? + new ValueTask(t) : + new ValueTask(synchronousResult); + } + + private Task ReadAsyncTask(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + Task? t = ReadAsyncInternal(new Memory(buffer, offset, count), cancellationToken, out int synchronousResult); + + if (t == null) + { + t = _lastSynchronouslyCompletedTask; + Debug.Assert(t == null || t.IsCompletedSuccessfully, "Cached task should have completed successfully"); + + if (t == null || t.Result != synchronousResult) + { + _lastSynchronouslyCompletedTask = t = Task.FromResult(synchronousResult); + } + } + + return t; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (_useAsyncIO) + { + WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), CancellationToken.None).AsTask().GetAwaiter().GetResult(); + } + else + { + WriteSpan(new ReadOnlySpan(buffer, offset, count)); + } + } + + public override void Write(ReadOnlySpan buffer) + { + if (!_useAsyncIO) + { + if (_fileHandle.IsClosed) + { + throw Error.GetFileNotOpen(); + } + + WriteSpan(buffer); + } + else + { + // If the stream is in async mode, we can't call the synchronous WriteSpan, so we similarly call the base Write, + // which will turn delegate to Write(byte[],int,int), which will do the right thing if we're in async mode. + base.Write(buffer); + } + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (!_useAsyncIO) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Write is invoked asynchronously. But we can do so using the base Stream's internal helper + // that bypasses delegating to BeginWrite, since we already know this is FileStream rather + // than something derived from it and what our BeginWrite implementation is going to do. + return (Task)base.BeginWriteInternal(buffer, offset, count, null, null, serializeAsynchronously: true, apm: false); + } + + return WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + if (!_useAsyncIO) + { + // If we weren't opened for asynchronous I/O, we still call to the base implementation so that + // Write is invoked asynchronously. But if we have a byte[], we can do so using the base Stream's + // internal helper that bypasses delegating to BeginWrite, since we already know this is FileStream + // rather than something derived from it and what our BeginWrite implementation is going to do. + return MemoryMarshal.TryGetArray(buffer, out ArraySegment segment) ? + new ValueTask((Task)base.BeginWriteInternal(segment.Array!, segment.Offset, segment.Count, null, null, serializeAsynchronously: true, apm: false)) : + base.WriteAsync(buffer, cancellationToken); + } + + return WriteAsyncInternal(buffer, cancellationToken); + } + + // this method might call Derived type implenentation of Flush(flushToDisk) + public override void Flush() => _fileStream.Flush(); + + internal override void Flush(bool flushToDisk) + { + FlushInternalBuffer(); + + if (flushToDisk && CanWrite) + { + FlushOSBuffer(); + } + } + + public override bool CanRead => !_fileHandle.IsClosed && (_access & FileAccess.Read) != 0; + + public override bool CanWrite => !_fileHandle.IsClosed && (_access & FileAccess.Write) != 0; + + internal override SafeFileHandle SafeFileHandle + { + get + { + Flush(); + _exposedHandle = true; + return _fileHandle; + } + } + + internal override string Name => _path ?? SR.IO_UnknownFileName; + + internal override bool IsAsync => _useAsyncIO; + + /// + /// Verify that the actual position of the OS's handle equals what we expect it to. + /// This will fail if someone else moved the UnixFileStream's handle or if + /// our position updating code is incorrect. + /// + private void VerifyOSHandlePosition() + { + bool verifyPosition = _exposedHandle; // in release, only verify if we've given out the handle such that someone else could be manipulating it +#if DEBUG + verifyPosition = true; // in debug, always make sure our position matches what the OS says it should be +#endif + if (verifyPosition && CanSeek) + { + long oldPos = _filePosition; // SeekCore will override the current _position, so save it now + long curPos = SeekCore(_fileHandle, 0, SeekOrigin.Current); + if (oldPos != curPos) + { + // For reads, this is non-fatal but we still could have returned corrupted + // data in some cases, so discard the internal buffer. For writes, + // this is a problem; discard the buffer and error out. + _readPos = _readLength = 0; + if (_writePos > 0) + { + _writePos = 0; + throw new IOException(SR.IO_FileStreamHandlePosition); + } + } + } + } + + /// Verifies that state relating to the read/write buffer is consistent. + [Conditional("DEBUG")] + private void AssertBufferInvariants() + { + // Read buffer values must be in range: 0 <= _bufferReadPos <= _bufferReadLength <= _bufferLength + Debug.Assert(0 <= _readPos && _readPos <= _readLength && _readLength <= _bufferLength); + + // Write buffer values must be in range: 0 <= _bufferWritePos <= _bufferLength + Debug.Assert(0 <= _writePos && _writePos <= _bufferLength); + + // Read buffering and write buffering can't both be active + Debug.Assert((_readPos == 0 && _readLength == 0) || _writePos == 0); + } + + /// Validates that we're ready to read from the stream. + private void PrepareForReading() + { + if (_fileHandle.IsClosed) + throw Error.GetFileNotOpen(); + if (_readLength == 0 && !CanRead) + throw Error.GetReadNotSupported(); + + AssertBufferInvariants(); + } + + /// Gets or sets the position within the current stream + public override long Position + { + get + { + AssertBufferInvariants(); + VerifyOSHandlePosition(); + + // We may have read data into our buffer from the handle, such that the handle position + // is artificially further along than the consumer's view of the stream's position. + // Thus, when reading, our position is really starting from the handle position negatively + // offset by the number of bytes in the buffer and positively offset by the number of + // bytes into that buffer we've read. When writing, both the read length and position + // must be zero, and our position is just the handle position offset positive by how many + // bytes we've written into the buffer. + return (_filePosition - _readLength) + _readPos + _writePos; + } + set + { + Seek(value, SeekOrigin.Begin); + } + } + + 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. + /// + /// The buffer. + private byte[] GetBuffer() + { + Debug.Assert(_buffer == null || _buffer.Length == _bufferLength); + if (_buffer == null) + { + _buffer = new byte[_bufferLength]; + OnBufferAllocated(); + } + + return _buffer; + } + + /// + /// Flushes the internal read/write buffer for this stream. If write data has been buffered, + /// that data is written out to the underlying file. Or if data has been buffered for + /// reading from the stream, the data is dumped and our position in the underlying file + /// is rewound as necessary. This does not flush the OS buffer. + /// + private void FlushInternalBuffer() + { + AssertBufferInvariants(); + if (_writePos > 0) + { + FlushWriteBuffer(); + } + else if (_readPos < _readLength && CanSeek) + { + FlushReadBuffer(); + } + } + + /// Dumps any read data in the buffer and rewinds our position in the stream, accordingly, as necessary. + private void FlushReadBuffer() + { + // Reading is done by blocks from the file, but someone could read + // 1 byte from the buffer then write. At that point, the OS's file + // pointer is out of sync with the stream's position. All write + // functions should call this function to preserve the position in the file. + + AssertBufferInvariants(); + Debug.Assert(_writePos == 0, "FileStream: Write buffer must be empty in FlushReadBuffer!"); + + int rewind = _readPos - _readLength; + if (rewind != 0) + { + Debug.Assert(CanSeek, "FileStream will lose buffered read data now."); + SeekCore(_fileHandle, rewind, SeekOrigin.Current); + } + _readPos = _readLength = 0; + } + + /// + /// Reads a byte from the file stream. Returns the byte cast to an int + /// or -1 if reading from the end of the stream. + /// + public override int ReadByte() + { + PrepareForReading(); + + byte[] buffer = GetBuffer(); + if (_readPos == _readLength) + { + FlushWriteBuffer(); + _readLength = FillReadBufferForReadByte(); + _readPos = 0; + if (_readLength == 0) + { + return -1; + } + } + + return buffer[_readPos++]; + } + + /// + /// Writes a byte to the current position in the stream and advances the position + /// within the stream by one byte. + /// + /// The byte to write to the stream. + public override void WriteByte(byte value) + { + PrepareForWriting(); + + // Flush the write buffer if it's full + if (_writePos == _bufferLength) + FlushWriteBufferForWriteByte(); + + // We now have space in the buffer. Store the byte. + GetBuffer()[_writePos++] = value; + } + + /// + /// Validates that we're ready to write to the stream, + /// including flushing a read buffer if necessary. + /// + private void PrepareForWriting() + { + if (_fileHandle.IsClosed) + throw Error.GetFileNotOpen(); + + // 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(); + FlushReadBuffer(); + Debug.Assert(_bufferLength > 0, "_bufferSize > 0"); + } + } + + partial void OnBufferAllocated(); + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + if (!_useAsyncIO) + return base.BeginRead(buffer, offset, count, callback, state); + else + return TaskToApm.Begin(ReadAsyncTask(buffer, offset, count, CancellationToken.None), callback, state); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + if (!_useAsyncIO) + return base.BeginWrite(buffer, offset, count, callback, state); + else + return TaskToApm.Begin(WriteAsyncInternal(new ReadOnlyMemory(buffer, offset, count), CancellationToken.None).AsTask(), callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + if (!_useAsyncIO) + return base.EndRead(asyncResult); + else + return TaskToApm.End(asyncResult); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + if (!_useAsyncIO) + base.EndWrite(asyncResult); + else + TaskToApm.End(asyncResult); + } + } +}