From c2d15279152dc1339dc4a580e4d906dff21bad38 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 31 Aug 2021 11:09:54 +0200 Subject: [PATCH 1/7] update XML docs --- .../src/System/IO/RandomAccess.cs | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs index 04b59ae472a769..4dbf7f8bed9a4e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO.Strategies; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; @@ -20,7 +19,6 @@ public static partial class RandomAccess /// is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). public static long GetLength(SafeFileHandle handle) { ValidateInput(handle, fileOffset: 0); @@ -33,12 +31,11 @@ public static long GetLength(SafeFileHandle handle) /// /// The file handle. /// A region of memory. When this method returns, the contents of this region are replaced by the bytes read from the file. - /// The file position to read from. + /// The file position to read from. For a file that does not support seeking (pipe or socket), it's ignored. /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated in the buffer if that many bytes are not currently available, or zero (0) if the end of the file has been reached. /// is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). /// is negative. /// was not opened for reading. /// An I/O error occurred. @@ -55,12 +52,11 @@ public static int Read(SafeFileHandle handle, Span buffer, long fileOffset /// /// The file handle. /// A list of memory buffers. When this method returns, the contents of the buffers are replaced by the bytes read from the file. - /// The file position to read from. + /// The file position to read from. For a file that does not support seeking (pipe or socket), it's ignored. /// The total number of bytes read into the buffers. This can be less than the number of bytes allocated in the buffers if that many bytes are not currently available, or zero (0) if the end of the file has been reached. /// or is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). /// is negative. /// was not opened for reading. /// An I/O error occurred. @@ -78,13 +74,12 @@ public static long Read(SafeFileHandle handle, IReadOnlyList> buffe /// /// The file handle. /// A region of memory. When this method returns, the contents of this region are replaced by the bytes read from the file. - /// The file position to read from. + /// The file position to read from. For a file that does not support seeking (pipe or socket), it's ignored. /// The token to monitor for cancellation requests. The default value is . /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated in the buffer if that many bytes are not currently available, or zero (0) if the end of the file has been reached. /// is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). /// is negative. /// was not opened for reading. /// An I/O error occurred. @@ -106,13 +101,12 @@ public static ValueTask ReadAsync(SafeFileHandle handle, Memory buffe /// /// The file handle. /// A list of memory buffers. When this method returns, the contents of these buffers are replaced by the bytes read from the file. - /// The file position to read from. + /// The file position to read from. For a file that does not support seeking (pipe or socket), it's ignored. /// The token to monitor for cancellation requests. The default value is . /// The total number of bytes read into the buffers. This can be less than the number of bytes allocated in the buffers if that many bytes are not currently available, or zero (0) if the end of the file has been reached. /// or is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). /// is negative. /// was not opened for reading. /// An I/O error occurred. @@ -135,11 +129,10 @@ public static ValueTask ReadAsync(SafeFileHandle handle, IReadOnlyList /// The file handle. /// A region of memory. This method copies the contents of this region to the file. - /// The file position to write to. + /// The file position to write to. For a file that does not support seeking (pipe or socket), it's ignored. /// is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). /// is negative. /// was not opened for writing. /// An I/O error occurred. @@ -156,11 +149,10 @@ public static void Write(SafeFileHandle handle, ReadOnlySpan buffer, long /// /// The file handle. /// A list of memory buffers. This method copies the contents of these buffers to the file. - /// The file position to write to. + /// The file position to write to. For a file that does not support seeking (pipe or socket), it's ignored. /// or is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). /// is negative. /// was not opened for writing. /// An I/O error occurred. @@ -178,13 +170,12 @@ public static void Write(SafeFileHandle handle, IReadOnlyList /// The file handle. /// A region of memory. This method copies the contents of this region to the file. - /// The file position to write to. + /// The file position to write to. For a file that does not support seeking (pipe or socket), it's ignored. /// The token to monitor for cancellation requests. The default value is . /// A task representing the asynchronous completion of the write operation. /// is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). /// is negative. /// was not opened for writing. /// An I/O error occurred. @@ -206,13 +197,12 @@ public static ValueTask WriteAsync(SafeFileHandle handle, ReadOnlyMemory b /// /// The file handle. /// A list of memory buffers. This method copies the contents of these buffers to the file. - /// The file position to write to. + /// The file position to write to. For a file that does not support seeking (pipe or socket), it's ignored. /// The token to monitor for cancellation requests. The default value is . /// A task representing the asynchronous completion of the write operation. /// or is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). /// is negative. /// was not opened for writing. /// An I/O error occurred. From 529d66bee3885eea3bbe9d391c512e88293abf55 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 31 Aug 2021 11:10:09 +0200 Subject: [PATCH 2/7] relax the requirements --- .../src/System/IO/RandomAccess.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs index 4dbf7f8bed9a4e..9071300ce4d034 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs @@ -230,15 +230,9 @@ private static void ValidateInput(SafeFileHandle handle, long fileOffset) { ThrowHelper.ThrowArgumentException_InvalidHandle(nameof(handle)); } - else if (!handle.CanSeek) + else if (handle.IsClosed) { - // CanSeek calls IsClosed, we don't want to call it twice for valid handles - if (handle.IsClosed) - { - ThrowHelper.ThrowObjectDisposedException_FileClosed(); - } - - ThrowHelper.ThrowNotSupportedException_UnseekableStream(); + ThrowHelper.ThrowObjectDisposedException_FileClosed(); } else if (fileOffset < 0) { From 9b7874c80bc1d5da2bfbbad36ea0fb2565b2d3cc Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 31 Aug 2021 11:10:22 +0200 Subject: [PATCH 3/7] Windows part --- ...eHandle.OverlappedValueTaskSource.Windows.cs | 7 +++++-- .../src/System/IO/RandomAccess.Windows.cs | 17 ++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs index 9f689b161bbd00..c2eab851bd60e2 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs @@ -86,8 +86,11 @@ internal static Exception GetIOError(int errorCode, string? path) _bufferSize = memory.Length; _memoryHandle = memory.Pin(); _overlapped = _fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(_preallocatedOverlapped); - _overlapped->OffsetLow = (int)fileOffset; - _overlapped->OffsetHigh = (int)(fileOffset >> 32); + if (_fileHandle.CanSeek) + { + _overlapped->OffsetLow = (int)fileOffset; + _overlapped->OffsetHigh = (int)(fileOffset >> 32); + } return _overlapped; } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs index 31adaca38fe5ee..5ffda1d7034031 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs @@ -73,7 +73,7 @@ private static unsafe int ReadSyncUsingAsyncHandle(SafeFileHandle handle, SpanOffsetLow = unchecked((int)fileOffset); - result->OffsetHigh = (int)(fileOffset >> 32); + // For pipes the offsets are in theory ignored by the OS + if (handle.CanSeek) + { + result->OffsetLow = unchecked((int)fileOffset); + result->OffsetHigh = (int)(fileOffset >> 32); + } // From https://docs.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-getoverlappedresult: // "If the hEvent member of the OVERLAPPED structure is NULL, the system uses the state of the hFile handle to signal when the operation has been completed. From f8a03bbd4c002e44f5047474535bb0d836eb3d95 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 31 Aug 2021 12:55:26 +0200 Subject: [PATCH 4/7] tests --- .../tests/RandomAccess/Base.cs | 14 +- .../tests/RandomAccess/NonSeekable.cs | 383 ++++++++++++++++++ .../RandomAccess/NonSeekable_AsyncHandles.cs | 44 ++ .../tests/System.IO.FileSystem.Tests.csproj | 2 + 4 files changed, 430 insertions(+), 13 deletions(-) create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable_AsyncHandles.cs diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/Base.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/Base.cs index 6f8726500d3b3b..3a0a23020d7bc8 100644 --- a/src/libraries/System.IO.FileSystem/tests/RandomAccess/Base.cs +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/Base.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.IO.Pipes; using System.Threading; using Microsoft.Win32.SafeHandles; using Xunit; @@ -48,17 +47,6 @@ public void ThrowsObjectDisposedExceptionForDisposedHandle() Assert.Throws(() => MethodUnderTest(handle, Array.Empty(), 0)); } - [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "System.IO.Pipes aren't supported on browser")] - public void ThrowsNotSupportedExceptionForUnseekableFile() - { - using (var server = new AnonymousPipeServerStream(PipeDirection.Out)) - using (SafeFileHandle handle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false)) - { - Assert.Throws(() => MethodUnderTest(handle, Array.Empty(), 0)); - } - } - [Theory] [MemberData(nameof(GetSyncAsyncOptions))] public void ThrowsArgumentOutOfRangeExceptionForNegativeFileOffset(FileOptions options) @@ -72,7 +60,7 @@ public void ThrowsArgumentOutOfRangeExceptionForNegativeFileOffset(FileOptions o } } - protected static CancellationTokenSource GetCancelledTokenSource() + internal static CancellationTokenSource GetCancelledTokenSource() { CancellationTokenSource source = new CancellationTokenSource(); source.Cancel(); diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs new file mode 100644 index 00000000000000..276a820e4740d6 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs @@ -0,0 +1,383 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Pipes; +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")] + public class RandomAccess_NonSeekable + { + private const int VectorCount = 10; + private const int BufferSize = 3; + private const int VectorsByteCount = VectorCount * BufferSize; + + protected virtual PipeOptions PipeOptions => PipeOptions.None; + + private async Task<(SafeFileHandle readHandle, SafeFileHandle writeHandle)> GetNamedPipeHandlesAsync() + { + string name = FileSystemTest.GetNamedPipeServerStreamName(); + + var server = new NamedPipeServerStream(name, PipeDirection.In, -1, PipeTransmissionMode.Byte, PipeOptions); + var client = new NamedPipeClientStream(".", name, PipeDirection.Out, PipeOptions); + + await Task.WhenAll(server.WaitForConnectionAsync(), client.ConnectAsync()); + + bool isAsync = (PipeOptions & PipeOptions.Asynchronous) != 0; + return (GetFileHandle(server, isAsync), GetFileHandle(client, isAsync)); + } + + [Fact] + public async Task ThrowsUnauthorizedAccessExceptionWhenOperationIsNotAllowed() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + Assert.Throws(() => RandomAccess.Read(writeHandle, new byte[1], 0)); + Assert.Throws(() => RandomAccess.Write(readHandle, new byte[1], 0)); + + await Assert.ThrowsAsync(async () => await RandomAccess.ReadAsync(writeHandle, new byte[1], 0)); + await Assert.ThrowsAsync(async () => await RandomAccess.WriteAsync(readHandle, new byte[1], 0)); + } + } + + [Fact] + public async Task ThrowsTaskAlreadyCanceledForCancelledTokenAsync() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + CancellationTokenSource cts = RandomAccess_Base.GetCancelledTokenSource(); + CancellationToken token = cts.Token; + + Assert.True(RandomAccess.ReadAsync(readHandle, new byte[1], 0, token).IsCanceled); + Assert.True(RandomAccess.WriteAsync(writeHandle, new byte[1], 0, token).IsCanceled); + + TaskCanceledException ex = await Assert.ThrowsAsync(() => RandomAccess.ReadAsync(readHandle, new byte[1], 0, token).AsTask()); + Assert.Equal(token, ex.CancellationToken); + ex = await Assert.ThrowsAsync(() => RandomAccess.WriteAsync(writeHandle, new byte[1], 0, token).AsTask()); + Assert.Equal(token, ex.CancellationToken); + } + } + + [Fact] + public async Task ReadToAnEmptyBufferReturnsZeroWhenDataIsAvailable() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + byte[] content = RandomNumberGenerator.GetBytes(BufferSize); + ValueTask write = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 0); + + Assert.Equal(0, RandomAccess.Read(readHandle, Array.Empty(), fileOffset: 0)); // what we test + byte[] buffer = new byte[content.Length * 2]; + Assert.Equal(content.Length, RandomAccess.Read(readHandle, buffer, fileOffset: 0)); // what is required for the above write to succeed + Assert.Equal(content, buffer.AsSpan(0, content.Length).ToArray()); + + await write; + } + } + + [Fact] + public async Task ReadToAnEmptyBufferReturnsZeroWhenDataIsAvailableAsync() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + byte[] content = RandomNumberGenerator.GetBytes(BufferSize); + Task write = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 0).AsTask(); + Task readToEmpty = RandomAccess.ReadAsync(readHandle, Array.Empty(), fileOffset: 0).AsTask(); // what we test + + Assert.Equal(0, await readToEmpty); + + byte[] buffer = new byte[content.Length * 2]; + Task readToNonEmpty = RandomAccess.ReadAsync(readHandle, buffer, fileOffset: 0).AsTask(); // what is required for the above write to succeed + + await Task.WhenAll(readToNonEmpty, write); + + Assert.Equal(content.Length, readToNonEmpty.Result); + Assert.Equal(content, buffer.AsSpan(0, readToNonEmpty.Result).ToArray()); + } + } + + [Fact] + public async Task CanReadToStackAllocatedMemory() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + byte[] content = RandomNumberGenerator.GetBytes(BufferSize); + Task write = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 0).AsTask(); + + ReadToStackAllocatedBuffer(readHandle, content); + + await write; + } + + void ReadToStackAllocatedBuffer(SafeFileHandle handle, byte[] array) + { + Span buffer = stackalloc byte[array.Length * 2]; + Assert.Equal(array.Length, RandomAccess.Read(handle, buffer, fileOffset: 0)); + Assert.Equal(array, buffer.Slice(0, array.Length).ToArray()); + } + } + + [Fact] + public async Task CanWriteFromStackAllocatedMemory() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + byte[] content = RandomNumberGenerator.GetBytes(BufferSize); + byte[] buffer = new byte[content.Length * 2]; + Task read = RandomAccess.ReadAsync(readHandle, buffer, fileOffset: 0).AsTask(); + + WriteFromStackAllocatedBuffer(writeHandle, content); + + Assert.Equal(content.Length, await read); + Assert.Equal(content, buffer.AsSpan(0, content.Length).ToArray()); + } + + void WriteFromStackAllocatedBuffer(SafeFileHandle handle, byte[] array) + { + Span buffer = stackalloc byte[array.Length]; + array.CopyTo(buffer); + RandomAccess.Write(handle, buffer, fileOffset: 0); + } + } + + [Fact] + public async Task FileOffsetsAreIgnored_AsyncWrite_SyncRead() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + byte[] content = RandomNumberGenerator.GetBytes(BufferSize); + Task writeToOffset123 = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 123).AsTask(); + byte[] buffer = new byte[content.Length * 2]; + int readFromOffset456 = RandomAccess.Read(readHandle, buffer, fileOffset: 456); + + Assert.Equal(content.Length, readFromOffset456); + Assert.Equal(content, buffer.AsSpan(0, readFromOffset456).ToArray()); + + await writeToOffset123; + } + } + + [Fact] + public async Task FileOffsetsAreIgnored_AsyncRead_SyncWrite() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + byte[] content = RandomNumberGenerator.GetBytes(BufferSize); + byte[] buffer = new byte[content.Length * 2]; + Task readFromOffset456 = RandomAccess.ReadAsync(readHandle, buffer, fileOffset: 456).AsTask(); + + RandomAccess.Write(writeHandle, content, fileOffset: 123); + + Assert.Equal(content.Length, readFromOffset456.Result); + Assert.Equal(content, buffer.AsSpan(0, readFromOffset456.Result).ToArray()); + } + } + + [Fact] + public async Task FileOffsetsAreIgnoredAsync() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + byte[] content = RandomNumberGenerator.GetBytes(BufferSize); + Task writeToOffset123 = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 123).AsTask(); + byte[] buffer = new byte[content.Length * 2]; + Task readFromOffset456 = RandomAccess.ReadAsync(readHandle, buffer, fileOffset: 456).AsTask(); + + await Task.WhenAll(readFromOffset456, writeToOffset123); + + Assert.Equal(content.Length, readFromOffset456.Result); + Assert.Equal(content, buffer.AsSpan(0, readFromOffset456.Result).ToArray()); + } + } + + [Fact] + public async Task PartialReadsAreSupported() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + byte[] content = RandomNumberGenerator.GetBytes(BufferSize); + ValueTask write = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 0); + + byte[] buffer = new byte[content.Length * 2]; + + for (int i = 0; i < content.Length; i++) + { + Assert.Equal(1, RandomAccess.Read(readHandle, buffer.AsSpan(i, 1), fileOffset: 0)); + } + Assert.Equal(content, buffer.AsSpan(0, content.Length).ToArray()); + + await write; + } + } + + [Fact] + public async Task PartialReadsAreSupportedAsync() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + byte[] content = RandomNumberGenerator.GetBytes(BufferSize); + ValueTask write = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 0); + + byte[] buffer = new byte[content.Length * 2]; + Assert.Equal(1, await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(0, 1), fileOffset: 0)); + Assert.Equal(2, await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(1), fileOffset: 0)); + Assert.Equal(content, buffer.AsSpan(0, 3).ToArray()); + } + } + + [Fact] + public async Task VectorizedIOIsSupported_AsyncWrite_SyncReads() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + ReadOnlyMemory[] vectors = GenerateReadOnlyVectors(VectorCount, BufferSize); + Task write = RandomAccess.WriteAsync(writeHandle, vectors, fileOffset: 123).AsTask(); + byte[] buffer = new byte[VectorsByteCount * 2]; + + int read = 0; + + do + { + read += RandomAccess.Read(readHandle, buffer.AsSpan(read), fileOffset: 456); + } while (read != VectorsByteCount); + + Assert.Equal(vectors.SelectMany(vector => vector.ToArray()), buffer.AsSpan(0, read).ToArray()); + + await write; + } + } + + [Fact] + public async Task VectorizedIOIsSupported_AsyncWrite_SyncRead() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + ReadOnlyMemory[] readOnlyVectors = GenerateReadOnlyVectors(VectorCount, BufferSize); + Task write = RandomAccess.WriteAsync(writeHandle, readOnlyVectors, fileOffset: 123).AsTask(); + byte[] buffer = new byte[VectorsByteCount * 2]; + + Memory[] writableVectors = GenerateVectors(VectorCount, BufferSize); + long read = RandomAccess.Read(readHandle, writableVectors, fileOffset: 456); + + Assert.Equal(VectorsByteCount, read); + Assert.Equal(readOnlyVectors.SelectMany(vector => vector.ToArray()), writableVectors.SelectMany(vector => vector.ToArray())); + + await write; + } + } + + [Fact] + public async Task VectorizedIOIsSupported_AsyncWrite_AsyncRead() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + ReadOnlyMemory[] readOnlyVectors = GenerateReadOnlyVectors(VectorCount, BufferSize); + Task write = RandomAccess.WriteAsync(writeHandle, readOnlyVectors, fileOffset: 123).AsTask(); + byte[] buffer = new byte[VectorsByteCount * 2]; + + Memory[] writableVectors = GenerateVectors(VectorCount, BufferSize); + + Assert.Equal(VectorsByteCount, await RandomAccess.ReadAsync(readHandle, writableVectors, fileOffset: 456)); + Assert.Equal(readOnlyVectors.SelectMany(vector => vector.ToArray()), writableVectors.SelectMany(vector => vector.ToArray())); + + await write; + } + } + + [Fact] + public async Task VectorizedIOIsSupported_AsyncRead_SyncWrite() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); + + using (readHandle) + using (writeHandle) + { + Memory[] writableVectors = GenerateVectors(VectorCount, BufferSize); + ValueTask read = RandomAccess.ReadAsync(readHandle, writableVectors, fileOffset: 456); + + byte[] content = RandomNumberGenerator.GetBytes(VectorsByteCount); + RandomAccess.Write(writeHandle, content, fileOffset: 123); + + Assert.Equal(VectorsByteCount, await read); + Assert.Equal(content, writableVectors.SelectMany(vector => vector.ToArray())); + } + } + + private static ReadOnlyMemory[] GenerateReadOnlyVectors(int vectorCount, int bufferSize) + => Enumerable.Range(0, vectorCount).Select(_ => new ReadOnlyMemory(RandomNumberGenerator.GetBytes(bufferSize))).ToArray(); + + private static Memory[] GenerateVectors(int vectorCount, int bufferSize) + => Enumerable.Range(0, vectorCount).Select(_ => new Memory(RandomNumberGenerator.GetBytes(bufferSize))).ToArray(); + + private static SafeFileHandle GetFileHandle(PipeStream pipeStream, bool isAsync) + { + var serverHandle = new SafeFileHandle(pipeStream.SafePipeHandle.DangerousGetHandle(), ownsHandle: true); + + try + { + if (OperatingSystem.IsWindows() && isAsync) + { + // Currently it's impossible to duplicate an async safe handle that has already been bound to Thread Pool: https://github.com/dotnet/runtime/issues/28585 + // I am opened for ideas on how we could solve it without an ugly reflection hack.. + ThreadPoolBoundHandle threadPoolBinding = (ThreadPoolBoundHandle)typeof(PipeStream).GetField("_threadPoolBinding", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance).GetValue(pipeStream); + typeof(SafeFileHandle).GetProperty("ThreadPoolBinding", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance).GetSetMethod(true).Invoke(serverHandle, new object[] { threadPoolBinding }); + } + + return serverHandle; + } + finally + { + pipeStream.SafePipeHandle.SetHandleAsInvalid(); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable_AsyncHandles.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable_AsyncHandles.cs new file mode 100644 index 00000000000000..7ccec951a62444 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable_AsyncHandles.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + public class RandomAccess_NonSeekable_AsyncHandles : RandomAccess_NonSeekable + { + protected override PipeOptions PipeOptions => PipeOptions.Asynchronous; + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] // cancellable file IO is supported only on Windows + [InlineData(FileAccess.Read)] + [InlineData(FileAccess.Write)] + public async Task CancellationIsSupported(FileAccess access) + { + string pipeName = FileSystemTest.GetNamedPipeServerStreamName(); + string pipePath = Path.GetFullPath($@"\\.\pipe\{pipeName}"); + + using (var server = new NamedPipeServerStream(pipeName, PipeDirection.InOut)) + using (SafeFileHandle clientHandle = File.OpenHandle(pipePath, FileMode.Open, access, FileShare.None, FileOptions.Asynchronous)) + { + await server.WaitForConnectionAsync(); + + Assert.True(clientHandle.IsAsync); + + CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(250)); + CancellationToken token = cts.Token; + byte[] buffer = new byte[1]; + + OperationCanceledException ex = await Assert.ThrowsAsync( + () => access == FileAccess.Write + ? RandomAccess.WriteAsync(clientHandle, buffer, 0, token).AsTask() + : RandomAccess.ReadAsync(clientHandle, buffer, 0, token).AsTask()); + + Assert.Equal(token, ex.CancellationToken); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index 439ab465cf9cb1..3f0a2a8d550818 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -58,6 +58,8 @@ + + From 501ebfad041340cd25696fcc464dc0b9f11f7372 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 1 Sep 2021 17:16:49 +0200 Subject: [PATCH 5/7] Unix part --- .../tests/RandomAccess/NonSeekable.cs | 1 + .../src/System/IO/RandomAccess.Unix.cs | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs index 276a820e4740d6..aeae1242414f62 100644 --- a/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs @@ -35,6 +35,7 @@ public class RandomAccess_NonSeekable } [Fact] + [SkipOnPlatform(TestPlatforms.AnyUnix, "named pipe implementation used by this test is using Sockets on Unix, which allow for both reading and writing")] public async Task ThrowsUnauthorizedAccessExceptionWhenOperationIsNotAllowed() { (SafeFileHandle readHandle, SafeFileHandle writeHandle) = await GetNamedPipeHandlesAsync(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs index 992dcc54f5be58..b0a629bf1612c9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs @@ -43,6 +43,11 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset) { + if (!handle.CanSeek) + { + return ReadScatterNoOffset(handle, buffers); + } + MemoryHandle[] handles = new MemoryHandle[buffers.Count]; Span vectors = buffers.Count <= IovStackThreshold ? stackalloc Interop.Sys.IOVector[IovStackThreshold] : new Interop.Sys.IOVector[buffers.Count]; @@ -74,6 +79,26 @@ internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnly return FileStreamHelpers.CheckFileCall(result, handle.Path); } + private static long ReadScatterNoOffset(SafeFileHandle handle, IReadOnlyList> buffers) + { + long result = 0; + + int buffersCount = buffers.Count; + for (int i = 0; i < buffersCount; i++) + { + Span buffer = buffers[i].Span; + int read = ReadAtOffset(handle, buffer, fileOffset: 0); + result += read; + + if (read != buffer.Length) + { + return result; // stop on the first incomplete read + } + } + + return result; + } + internal static ValueTask ReadAtOffsetAsync(SafeFileHandle handle, Memory buffer, long fileOffset, CancellationToken cancellationToken, OSFileStreamStrategy? strategy = null) => ScheduleSyncReadAtOffsetAsync(handle, buffer, fileOffset, cancellationToken, strategy); @@ -130,6 +155,12 @@ internal static unsafe void WriteGatherAtOffset(SafeFileHandle handle, IReadOnly return; } + if (!handle.CanSeek) + { + WriteGatherNoOffset(handle, buffers); + return; + } + var handles = new MemoryHandle[buffersCount]; Span vectors = buffersCount <= IovStackThreshold ? stackalloc Interop.Sys.IOVector[IovStackThreshold] : @@ -202,6 +233,15 @@ internal static unsafe void WriteGatherAtOffset(SafeFileHandle handle, IReadOnly } } + private static void WriteGatherNoOffset(SafeFileHandle handle, IReadOnlyList> buffers) + { + int buffersCount = buffers.Count; + for (int i = 0; i < buffersCount; i++) + { + WriteAtOffset(handle, buffers[i].Span, fileOffset: 0); + } + } + internal static ValueTask WriteAtOffsetAsync(SafeFileHandle handle, ReadOnlyMemory buffer, long fileOffset, CancellationToken cancellationToken, OSFileStreamStrategy? strategy = null) => ScheduleSyncWriteAtOffsetAsync(handle, buffer, fileOffset, cancellationToken, strategy); From ffb5cf2bf3bc687c3ce4e2c606691d5943e93081 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 1 Sep 2021 17:33:35 +0200 Subject: [PATCH 6/7] cover the unhappy paths for vectorized IO --- .../tests/RandomAccess/NonSeekable.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs index aeae1242414f62..1ddcec6e918817 100644 --- a/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/NonSeekable.cs @@ -46,8 +46,14 @@ public async Task ThrowsUnauthorizedAccessExceptionWhenOperationIsNotAllowed() Assert.Throws(() => RandomAccess.Read(writeHandle, new byte[1], 0)); Assert.Throws(() => RandomAccess.Write(readHandle, new byte[1], 0)); + Assert.Throws(() => RandomAccess.Read(writeHandle, GenerateVectors(1, 1), 0)); + Assert.Throws(() => RandomAccess.Write(readHandle, GenerateReadOnlyVectors(1, 1), 0)); + await Assert.ThrowsAsync(async () => await RandomAccess.ReadAsync(writeHandle, new byte[1], 0)); await Assert.ThrowsAsync(async () => await RandomAccess.WriteAsync(readHandle, new byte[1], 0)); + + await Assert.ThrowsAsync(async () => await RandomAccess.ReadAsync(writeHandle, GenerateVectors(1, 1), 0)); + await Assert.ThrowsAsync(async () => await RandomAccess.WriteAsync(readHandle, GenerateReadOnlyVectors(1, 1), 0)); } } @@ -64,11 +70,17 @@ public async Task ThrowsTaskAlreadyCanceledForCancelledTokenAsync() Assert.True(RandomAccess.ReadAsync(readHandle, new byte[1], 0, token).IsCanceled); Assert.True(RandomAccess.WriteAsync(writeHandle, new byte[1], 0, token).IsCanceled); + Assert.True(RandomAccess.ReadAsync(readHandle, GenerateVectors(1, 1), 0, token).IsCanceled); + Assert.True(RandomAccess.WriteAsync(writeHandle, GenerateReadOnlyVectors(1, 1), 0, token).IsCanceled); TaskCanceledException ex = await Assert.ThrowsAsync(() => RandomAccess.ReadAsync(readHandle, new byte[1], 0, token).AsTask()); Assert.Equal(token, ex.CancellationToken); ex = await Assert.ThrowsAsync(() => RandomAccess.WriteAsync(writeHandle, new byte[1], 0, token).AsTask()); Assert.Equal(token, ex.CancellationToken); + ex = await Assert.ThrowsAsync(() => RandomAccess.ReadAsync(writeHandle, GenerateVectors(1, 1), 0, token).AsTask()); + Assert.Equal(token, ex.CancellationToken); + ex = await Assert.ThrowsAsync(() => RandomAccess.WriteAsync(writeHandle, GenerateReadOnlyVectors(1, 1), 0, token).AsTask()); + Assert.Equal(token, ex.CancellationToken); } } From 2b1510fbd40e0251204b3852572dfbcf8adec05b Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 2 Sep 2021 10:19:14 +0200 Subject: [PATCH 7/7] don't perform artificial partial writes for non-seekable files, as it break testing reads --- .../System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs index b0a629bf1612c9..c89fb9fda7e811 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs @@ -117,7 +117,7 @@ internal static unsafe void WriteAtOffset(SafeFileHandle handle, ReadOnlySpan