From b1a3fd8abbdd4c1d6041495b9bc2e030078bc5a2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 22:15:04 +0000
Subject: [PATCH 01/19] Initial plan
From 5d43e897949ed02b1d475aef3a558ccea4970304 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 22:49:11 +0000
Subject: [PATCH 02/19] Relax RandomAccess type requirements - make Read/Write
work with non-seekable files
- Add native readv/writev sys-call wrappers in pal_io.c and pal_io.h
- Add native entrypoints for ReadV/WriteV in entrypoints.c
- Add managed interop files Interop.ReadV.cs and Interop.WriteV.cs
- Add interop references to System.Private.CoreLib.Shared.projitems
- Modify RandomAccess.cs - relax ValidateInput to allow unseekable handles
- Update XML docs for Read/Write methods (remove NotSupportedException, note offset ignored)
- Modify RandomAccess.Unix.cs - use readv/writev for non-seekable scatter/gather
- Update Base.cs - remove ThrowsNotSupportedExceptionForUnseekableFile test
- Add NonSeekable.cs tests using SafeFileHandle.CreateAnonymousPipe
- Add NonSeekable_AsyncHandles.cs tests
- Update test .csproj to include new test files
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Unix/System.Native/Interop.ReadV.cs | 13 +
.../Unix/System.Native/Interop.WriteV.cs | 13 +
.../System.Private.CoreLib.Shared.projitems | 6 +
.../src/System/IO/RandomAccess.Unix.cs | 8 +-
.../src/System/IO/RandomAccess.cs | 49 +--
.../RandomAccess/Base.cs | 19 +-
.../RandomAccess/FlushToDisk.cs | 3 -
.../RandomAccess/NonSeekable.cs | 383 ++++++++++++++++++
.../RandomAccess/NonSeekable_AsyncHandles.cs | 44 ++
.../System.IO.FileSystem.Tests.csproj | 2 +
src/native/libs/System.Native/entrypoints.c | 2 +
src/native/libs/System.Native/pal_io.c | 28 ++
src/native/libs/System.Native/pal_io.h | 14 +
13 files changed, 530 insertions(+), 54 deletions(-)
create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadV.cs
create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.WriteV.cs
create mode 100644 src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
create mode 100644 src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadV.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadV.cs
new file mode 100644
index 00000000000000..10d21b40d22c20
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadV.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+ internal static partial class Sys
+ {
+ [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadV", SetLastError = true)]
+ internal static unsafe partial long ReadV(SafeHandle fd, IOVector* vectors, int vectorCount);
+ }
+}
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.WriteV.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.WriteV.cs
new file mode 100644
index 00000000000000..e890545f14ba39
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.WriteV.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+ internal static partial class Sys
+ {
+ [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_WriteV", SetLastError = true)]
+ internal static unsafe partial long WriteV(SafeHandle fd, IOVector* vectors, int vectorCount);
+ }
+}
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 a546bc4df703da..1b37f2335480cf 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
@@ -2556,6 +2556,9 @@
Common\Interop\Unix\System.Native\Interop.PRead.cs
+
+ Common\Interop\Unix\System.Native\Interop.ReadV.cs
+
Common\Interop\Unix\System.Native\Interop.PReadV.cs
@@ -2568,6 +2571,9 @@
Common\Interop\Unix\System.Native\Interop.Read.cs
+
+ Common\Interop\Unix\System.Native\Interop.WriteV.cs
+
Common\Interop\Unix\System.Native\Interop.ReadDir.cs
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 1fb4140feac36c..0291254a15bfbc 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
@@ -81,7 +81,9 @@ internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnly
fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(vectors))
{
- result = Interop.Sys.PReadV(handle, pinnedVectors, buffers.Count, fileOffset);
+ result = handle.SupportsRandomAccess
+ ? Interop.Sys.PReadV(handle, pinnedVectors, buffers.Count, fileOffset)
+ : Interop.Sys.ReadV(handle, pinnedVectors, buffers.Count);
}
}
finally
@@ -199,7 +201,9 @@ stackalloc Interop.Sys.IOVector[IovStackThreshold].Slice(0, buffersCount) :
Span left = vectors.Slice(buffersOffset);
fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(left))
{
- bytesWritten = Interop.Sys.PWriteV(handle, pinnedVectors, left.Length, fileOffset);
+ bytesWritten = handle.SupportsRandomAccess
+ ? Interop.Sys.PWriteV(handle, pinnedVectors, left.Length, fileOffset)
+ : Interop.Sys.WriteV(handle, pinnedVectors, left.Length);
}
FileStreamHelpers.CheckFileCall(bytesWritten, handle.Path);
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 166d2faf3fbb14..1b8e2e3d29d715 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs
@@ -22,7 +22,7 @@ public static partial class RandomAccess
/// The file does not support seeking (pipe or socket).
public static long GetLength(SafeFileHandle handle)
{
- ValidateInput(handle, fileOffset: 0);
+ ValidateInput(handle, fileOffset: 0, allowUnseekableHandles: false);
return handle.GetFileLength();
}
@@ -39,7 +39,7 @@ public static long GetLength(SafeFileHandle handle)
/// is negative.
public static void SetLength(SafeFileHandle handle, long length)
{
- ValidateInput(handle, fileOffset: 0);
+ ValidateInput(handle, fileOffset: 0, allowUnseekableHandles: false);
if (length < 0)
{
@@ -54,12 +54,11 @@ public static void SetLength(SafeFileHandle handle, long length)
///
/// 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.
@@ -76,12 +75,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.
@@ -99,13 +97,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.
@@ -127,13 +124,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.
@@ -156,11 +152,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.
@@ -177,11 +172,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.
@@ -199,13 +193,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.
@@ -227,13 +220,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.
@@ -276,12 +268,12 @@ public static void FlushToDisk(SafeFileHandle handle)
// Unix does NOT support unseekable handles however, the code that ultimately runs on Unix when we
// call FileStreamHelpers.FlushToDisk() later below, will silently ignore those errors, effectively
// making FlushToDisk() a no-op on Unix when used with unseekable handles.
- ValidateInput(handle, fileOffset: 0, allowUnseekableHandles: true);
+ ValidateInput(handle, fileOffset: 0);
FileStreamHelpers.FlushToDisk(handle);
}
- private static void ValidateInput(SafeFileHandle handle, long fileOffset, bool allowUnseekableHandles = false)
+ private static void ValidateInput(SafeFileHandle handle, long fileOffset, bool allowUnseekableHandles = true)
{
if (handle is null)
{
@@ -291,18 +283,13 @@ private static void ValidateInput(SafeFileHandle handle, long fileOffset, bool a
{
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();
- }
-
- if (!allowUnseekableHandles)
- {
- ThrowHelper.ThrowNotSupportedException_UnseekableStream();
- }
+ ThrowHelper.ThrowObjectDisposedException_FileClosed();
+ }
+ else if (!allowUnseekableHandles && !handle.CanSeek)
+ {
+ ThrowHelper.ThrowNotSupportedException_UnseekableStream();
}
else if (fileOffset < 0)
{
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/Base.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/Base.cs
index 9506d954d28374..3a0a23020d7bc8 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/Base.cs
+++ b/src/libraries/System.Runtime/tests/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;
@@ -15,8 +14,6 @@ public abstract class RandomAccess_Base : FileSystemTest
protected virtual bool UsesOffsets => true;
- protected virtual bool ThrowsForUnseekableFile => true;
-
public static IEnumerable GetSyncAsyncOptions()
{
yield return new object[] { FileOptions.None };
@@ -50,20 +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()
- {
- if (ThrowsForUnseekableFile)
- {
- 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)
@@ -77,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.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.cs
index e821ad654e6b5d..9bd6299cbf3dfa 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.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.Security.Cryptography;
using Microsoft.Win32.SafeHandles;
using Xunit;
@@ -24,8 +23,6 @@ public partial class RandomAccess_FlushToDisk : RandomAccess_Base
protected override bool UsesOffsets => false;
- protected override bool ThrowsForUnseekableFile => false;
-
protected override long MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset)
{
RandomAccess.FlushToDisk(handle);
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
new file mode 100644
index 00000000000000..e4c64c124f64de
--- /dev/null
+++ b/src/libraries/System.Runtime/tests/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.Linq;
+using System.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Win32.SafeHandles;
+using Xunit;
+
+namespace System.IO.Tests
+{
+ [SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")]
+ public class RandomAccess_NonSeekable : FileSystemTest
+ {
+ private const int VectorCount = 10;
+ private const int BufferSize = 3;
+ private const int VectorsByteCount = VectorCount * BufferSize;
+
+ protected virtual bool AsyncHandles => false;
+
+ private (SafeFileHandle readHandle, SafeFileHandle writeHandle) GetAnonymousPipeHandles()
+ {
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle,
+ asyncRead: AsyncHandles, asyncWrite: AsyncHandles);
+ return (readHandle, writeHandle);
+ }
+
+ [Fact]
+ public void ThrowsTaskAlreadyCanceledForCancelledTokenAsync()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ 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);
+ Assert.True(RandomAccess.ReadAsync(readHandle, GenerateVectors(1, 1), 0, token).IsCanceled);
+ Assert.True(RandomAccess.WriteAsync(writeHandle, GenerateReadOnlyVectors(1, 1), 0, token).IsCanceled);
+
+ TaskCanceledException ex = Assert.ThrowsAsync(() => RandomAccess.ReadAsync(readHandle, new byte[1], 0, token).AsTask()).Result;
+ Assert.Equal(token, ex.CancellationToken);
+ ex = Assert.ThrowsAsync(() => RandomAccess.WriteAsync(writeHandle, new byte[1], 0, token).AsTask()).Result;
+ Assert.Equal(token, ex.CancellationToken);
+ ex = Assert.ThrowsAsync(() => RandomAccess.ReadAsync(writeHandle, GenerateVectors(1, 1), 0, token).AsTask()).Result;
+ Assert.Equal(token, ex.CancellationToken);
+ ex = Assert.ThrowsAsync(() => RandomAccess.WriteAsync(writeHandle, GenerateReadOnlyVectors(1, 1), 0, token).AsTask()).Result;
+ Assert.Equal(token, ex.CancellationToken);
+ }
+ }
+
+ [Fact]
+ public void ReadToAnEmptyBufferReturnsZeroWhenDataIsAvailable()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ using (readHandle)
+ using (writeHandle)
+ {
+ byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
+ RandomAccess.Write(writeHandle, content, fileOffset: 0);
+
+ Assert.Equal(0, RandomAccess.Read(readHandle, Array.Empty(), fileOffset: 0));
+ byte[] buffer = new byte[content.Length * 2];
+
+ ReadExactly(readHandle, buffer, content.Length);
+
+ Assert.Equal(content, buffer.AsSpan(0, content.Length).ToArray());
+ }
+ }
+
+ [Fact]
+ public async Task ReadToAnEmptyBufferReturnsZeroWhenDataIsAvailableAsync()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ 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();
+
+ Assert.Equal(0, await readToEmpty);
+
+ byte[] buffer = new byte[content.Length * 2];
+ Task readToNonEmpty = ReadExactlyAsync(readHandle, buffer, content.Length);
+
+ await Task.WhenAll(readToNonEmpty, write);
+
+ Assert.Equal(content, buffer.AsSpan(0, content.Length).ToArray());
+ }
+ }
+
+ [Fact]
+ public void CanReadToStackAllocatedMemory()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ using (readHandle)
+ using (writeHandle)
+ {
+ byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
+ RandomAccess.Write(writeHandle, content, fileOffset: 0);
+
+ ReadToStackAllocatedBuffer(readHandle, content);
+ }
+
+ void ReadToStackAllocatedBuffer(SafeFileHandle handle, byte[] array)
+ {
+ Span buffer = stackalloc byte[array.Length * 2];
+ ReadExactly(handle, buffer, array.Length);
+ Assert.Equal(array, buffer.Slice(0, array.Length).ToArray());
+ }
+ }
+
+ [Fact]
+ public void CanWriteFromStackAllocatedMemory()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ using (readHandle)
+ using (writeHandle)
+ {
+ byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
+ byte[] buffer = new byte[content.Length * 2];
+
+ WriteFromStackAllocatedBuffer(writeHandle, content);
+
+ ReadExactly(readHandle, buffer, content.Length);
+ 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 void FileOffsetsAreIgnored_SyncWrite_SyncRead()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ using (readHandle)
+ using (writeHandle)
+ {
+ byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
+ RandomAccess.Write(writeHandle, content, fileOffset: 123);
+ byte[] buffer = new byte[content.Length * 2];
+ int readFromOffset456 = RandomAccess.Read(readHandle, buffer, fileOffset: 456);
+
+ Assert.InRange(readFromOffset456, 1, content.Length);
+ Assert.Equal(content.Take(readFromOffset456), buffer.AsSpan(0, readFromOffset456).ToArray());
+ }
+ }
+
+ [Fact]
+ public async Task FileOffsetsAreIgnored_AsyncWrite_SyncRead()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ 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.InRange(readFromOffset456, 1, content.Length);
+ Assert.Equal(content.Take(readFromOffset456), buffer.AsSpan(0, readFromOffset456).ToArray());
+
+ await writeToOffset123;
+ }
+ }
+
+ [Fact]
+ public async Task FileOffsetsAreIgnored_AsyncRead_SyncWrite()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ 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);
+
+ int bytesRead = await readFromOffset456;
+ Assert.InRange(bytesRead, 1, content.Length);
+ Assert.Equal(content.Take(bytesRead), buffer.AsSpan(0, readFromOffset456.Result).ToArray());
+ }
+ }
+
+ [Fact]
+ public async Task FileOffsetsAreIgnoredAsync()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ 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.InRange(readFromOffset456.Result, 1, content.Length);
+ Assert.Equal(content.Take(readFromOffset456.Result), buffer.AsSpan(0, readFromOffset456.Result).ToArray());
+ }
+ }
+
+ [Fact]
+ public void PartialReadsAreSupported()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ using (readHandle)
+ using (writeHandle)
+ {
+ byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
+ RandomAccess.Write(writeHandle, content, fileOffset: 0);
+
+ byte[] buffer = new byte[BufferSize];
+
+ for (int i = 0; i < BufferSize; i++)
+ {
+ Assert.Equal(1, RandomAccess.Read(readHandle, buffer.AsSpan(i, 1), fileOffset: 0));
+ }
+ Assert.Equal(content, buffer);
+ }
+ }
+
+ [Fact]
+ public async Task PartialReadsAreSupportedAsync()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ using (readHandle)
+ using (writeHandle)
+ {
+ byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
+ ValueTask write = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 0);
+
+ byte[] buffer = new byte[BufferSize];
+
+ for (int i = 0; i < BufferSize; i++)
+ {
+ Assert.Equal(1, await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(i, 1), fileOffset: 0));
+ }
+ Assert.Equal(content, buffer);
+ }
+ }
+
+ [Fact]
+ public async Task MultipleBuffersAreSupported_AsyncWrite_SyncReads()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ 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 bytesRead = 0;
+ do
+ {
+ bytesRead += RandomAccess.Read(readHandle, buffer.AsSpan(bytesRead), fileOffset: 456);
+ } while (bytesRead != VectorsByteCount);
+
+ Assert.Equal(vectors.SelectMany(vector => vector.ToArray()), buffer.AsSpan(0, bytesRead).ToArray());
+
+ await write;
+ }
+ }
+
+ [Fact]
+ public async Task MultipleBuffersAreSupported_AsyncWrite_SyncRead()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ 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);
+ int bytesRead = (int)RandomAccess.Read(readHandle, writableVectors, fileOffset: 456);
+
+ Assert.InRange(bytesRead, 1, VectorsByteCount);
+ AssertEqual(readOnlyVectors, writableVectors, bytesRead);
+
+ await write;
+ }
+ }
+
+ [Fact]
+ public async Task MultipleBuffersAreSupported_AsyncWrite_AsyncRead()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ 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 bytesRead = await RandomAccess.ReadAsync(readHandle, writableVectors, fileOffset: 456);
+ Assert.InRange(bytesRead, 1, VectorsByteCount);
+ AssertEqual(readOnlyVectors, writableVectors, (int)bytesRead);
+
+ await write;
+ }
+ }
+
+ [Fact]
+ public async Task MultipleBuffersAreSupported_AsyncRead_SyncWrite()
+ {
+ (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+
+ 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);
+
+ int bytesRead = (int)await read;
+ Assert.InRange(bytesRead, 1, VectorsByteCount);
+ Assert.Equal(content.Take(bytesRead), writableVectors.SelectMany(vector => vector.ToArray()).Take(bytesRead));
+ }
+ }
+
+ 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 void ReadExactly(SafeFileHandle readHandle, Span buffer, int expectedByteCount)
+ {
+ int bytesRead = 0;
+ do
+ {
+ bytesRead += RandomAccess.Read(readHandle, buffer.Slice(bytesRead), fileOffset: 0);
+ } while (bytesRead != expectedByteCount);
+ }
+
+ private static async Task ReadExactlyAsync(SafeFileHandle readHandle, byte[] buffer, int expectedByteCount)
+ {
+ int bytesRead = 0;
+ do
+ {
+ bytesRead += await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(bytesRead), fileOffset: 0);
+ } while (bytesRead != expectedByteCount);
+ }
+
+ private static void AssertEqual(ReadOnlyMemory[] readOnlyVectors, Memory[] writableVectors, int byteCount)
+ => Assert.Equal(
+ readOnlyVectors.SelectMany(vector => vector.ToArray()).Take(byteCount),
+ writableVectors.SelectMany(vector => vector.ToArray()).Take(byteCount));
+ }
+}
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs
new file mode 100644
index 00000000000000..edec0ee6f8158b
--- /dev/null
+++ b/src/libraries/System.Runtime/tests/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.Threading;
+using System.Threading.Tasks;
+using Microsoft.Win32.SafeHandles;
+using Xunit;
+
+namespace System.IO.Tests
+{
+ [SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")]
+ public class RandomAccess_NonSeekable_AsyncHandles : RandomAccess_NonSeekable
+ {
+ protected override bool AsyncHandles => true;
+
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))]
+ [InlineData(FileAccess.Read)]
+ [InlineData(FileAccess.Write)]
+ public async Task CancellationIsSupported(FileAccess access)
+ {
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle,
+ asyncRead: true, asyncWrite: true);
+
+ using (readHandle)
+ using (writeHandle)
+ {
+ SafeFileHandle handle = access == FileAccess.Read ? readHandle : writeHandle;
+
+ Assert.True(handle.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(handle, buffer, 0, token).AsTask()
+ : RandomAccess.ReadAsync(handle, buffer, 0, token).AsTask());
+
+ Assert.Equal(token, ex.CancellationToken);
+ }
+ }
+ }
+}
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
index a81429675984db..e25561d68230a8 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
@@ -85,6 +85,8 @@
+
+
diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c
index 458980b89bd75e..ada6d523e8a51b 100644
--- a/src/native/libs/System.Native/entrypoints.c
+++ b/src/native/libs/System.Native/entrypoints.c
@@ -278,6 +278,8 @@ static const Entry s_sysNative[] =
DllImportEntry(SystemNative_PWrite)
DllImportEntry(SystemNative_PReadV)
DllImportEntry(SystemNative_PWriteV)
+ DllImportEntry(SystemNative_ReadV)
+ DllImportEntry(SystemNative_WriteV)
DllImportEntry(SystemNative_CreateThread)
DllImportEntry(SystemNative_EnablePosixSignalHandling)
DllImportEntry(SystemNative_DisablePosixSignalHandling)
diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c
index cd488c01c2c339..08b42ba1791853 100644
--- a/src/native/libs/System.Native/pal_io.c
+++ b/src/native/libs/System.Native/pal_io.c
@@ -1996,6 +1996,20 @@ static int GetAllowedVectorCount(IOVector* vectors, int32_t vectorCount)
}
#endif // (HAVE_PREADV || HAVE_PWRITEV) && !defined(TARGET_WASM)
+int64_t SystemNative_ReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount)
+{
+ assert(vectors != NULL);
+ assert(vectorCount >= 0);
+
+ int64_t count = 0;
+ int fileDescriptor = ToFileDescriptor(fd);
+
+ while ((count = readv(fileDescriptor, (struct iovec*)vectors, (int)vectorCount)) < 0 && errno == EINTR);
+
+ assert(count >= -1);
+ return count;
+}
+
int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset)
{
assert(vectors != NULL);
@@ -2037,6 +2051,20 @@ int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount,
return count;
}
+int64_t SystemNative_WriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount)
+{
+ assert(vectors != NULL);
+ assert(vectorCount >= 0);
+
+ int64_t count = 0;
+ int fileDescriptor = ToFileDescriptor(fd);
+
+ while ((count = writev(fileDescriptor, (struct iovec*)vectors, (int)vectorCount)) < 0 && errno == EINTR);
+
+ assert(count >= -1);
+ return count;
+}
+
int64_t SystemNative_PWriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset)
{
assert(vectors != NULL);
diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h
index bc6c108828d256..3927921b5d411a 100644
--- a/src/native/libs/System.Native/pal_io.h
+++ b/src/native/libs/System.Native/pal_io.h
@@ -885,3 +885,17 @@ PALEXPORT int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t ve
* Returns the number of bytes written on success; otherwise, -1 is returned an errno is set.
*/
PALEXPORT int64_t SystemNative_PWriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset);
+
+/**
+ * Reads the number of bytes specified into the provided buffers from the specified, opened file descriptor.
+ *
+ * Returns the number of bytes read on success; otherwise, -1 is returned an errno is set.
+ */
+PALEXPORT int64_t SystemNative_ReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount);
+
+/**
+ * Writes the number of bytes specified in the buffers into the specified, opened file descriptor
+ *
+ * Returns the number of bytes written on success; otherwise, -1 is returned an errno is set.
+ */
+PALEXPORT int64_t SystemNative_WriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount);
From ea20ad62a3745987bce67f6452ddb821cd9ed4fa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 22:59:39 +0000
Subject: [PATCH 03/19] Fix FlushToDisk test to use
SafeFileHandle.CreateAnonymousPipe instead of AnonymousPipeServerStream
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../RandomAccess/FlushToDisk.cs | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.cs
index 9bd6299cbf3dfa..10fa74f1b56291 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.cs
@@ -81,18 +81,20 @@ public void CanFlushWithoutWriting()
}
[Fact]
- [SkipOnPlatform(TestPlatforms.Browser, "System.IO.Pipes aren't supported on browser")]
+ [SkipOnPlatform(TestPlatforms.Browser, "pipes aren't supported on browser")]
public void CanFlushUnseekableFile()
{
- using (var server = new AnonymousPipeServerStream(PipeDirection.Out))
- using (SafeFileHandle handle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false))
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle);
+
+ using (readHandle)
+ using (writeHandle)
{
// Flushing a non-seekable handle (in this case, a pipe handle) should work without throwing an
// exception. On Windows, the FlushFileBuffers() function DOES work with non-seekable handles
// (e.g. pipe handles) and that is what we are testing here. The fsync() function on Unix does
// NOT support non-seekable handles but no exception is thrown on Unix either because we silently
// ignore the errors effectively making the call below a no-op.
- RandomAccess.FlushToDisk(handle);
+ RandomAccess.FlushToDisk(writeHandle);
}
}
From 62659a48ff89fbc746c2d69fa0087fdede32ccd5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 23:01:06 +0000
Subject: [PATCH 04/19] Fix test: use bytesRead local instead of
readFromOffset456.Result for consistency
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
index e4c64c124f64de..3dbdaa66e1afe7 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
@@ -197,7 +197,7 @@ public async Task FileOffsetsAreIgnored_AsyncRead_SyncWrite()
int bytesRead = await readFromOffset456;
Assert.InRange(bytesRead, 1, content.Length);
- Assert.Equal(content.Take(bytesRead), buffer.AsSpan(0, readFromOffset456.Result).ToArray());
+ Assert.Equal(content.Take(bytesRead), buffer.AsSpan(0, bytesRead).ToArray());
}
}
From 098316d16500faf1b4d933570f5353c522766e19 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Mar 2026 08:00:36 +0000
Subject: [PATCH 05/19] Add PReadV/PWriteV -> ReadV/WriteV fallback and
EAGAIN/EWOULDBLOCK handling
- ReadScatterAtOffset: try PReadV first, fall back to ReadV on ENXIO/ESPIPE
- WriteGatherAtOffset: try PWriteV first, fall back to WriteV on ENXIO/ESPIPE
- SystemNative_ReadV: handle EAGAIN/EWOULDBLOCK with poll loop (like ReadFromNonblocking)
- SystemNative_WriteV: handle EAGAIN/EWOULDBLOCK with poll loop (like WriteToNonblocking)
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/System/IO/RandomAccess.Unix.cs | 48 ++++++++++++--
src/native/libs/System.Native/pal_io.c | 65 ++++++++++++++++---
2 files changed, 99 insertions(+), 14 deletions(-)
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 0291254a15bfbc..bc5fd1edaaf76b 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
@@ -81,9 +81,27 @@ internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnly
fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(vectors))
{
- result = handle.SupportsRandomAccess
- ? Interop.Sys.PReadV(handle, pinnedVectors, buffers.Count, fileOffset)
- : Interop.Sys.ReadV(handle, pinnedVectors, buffers.Count);
+ if (handle.SupportsRandomAccess)
+ {
+ result = Interop.Sys.PReadV(handle, pinnedVectors, buffers.Count, fileOffset);
+ if (result == -1)
+ {
+ // We need to fallback to the non-offset version for certain file types
+ // e.g: character devices (such as /dev/tty), pipes, and sockets.
+ Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
+
+ if (errorInfo.Error == Interop.Error.ENXIO ||
+ errorInfo.Error == Interop.Error.ESPIPE)
+ {
+ handle.SupportsRandomAccess = false;
+ result = Interop.Sys.ReadV(handle, pinnedVectors, buffers.Count);
+ }
+ }
+ }
+ else
+ {
+ result = Interop.Sys.ReadV(handle, pinnedVectors, buffers.Count);
+ }
}
}
finally
@@ -201,9 +219,27 @@ stackalloc Interop.Sys.IOVector[IovStackThreshold].Slice(0, buffersCount) :
Span left = vectors.Slice(buffersOffset);
fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(left))
{
- bytesWritten = handle.SupportsRandomAccess
- ? Interop.Sys.PWriteV(handle, pinnedVectors, left.Length, fileOffset)
- : Interop.Sys.WriteV(handle, pinnedVectors, left.Length);
+ if (handle.SupportsRandomAccess)
+ {
+ bytesWritten = Interop.Sys.PWriteV(handle, pinnedVectors, left.Length, fileOffset);
+ if (bytesWritten == -1)
+ {
+ // We need to fallback to the non-offset version for certain file types
+ // e.g: character devices (such as /dev/tty), pipes, and sockets.
+ Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
+
+ if (errorInfo.Error == Interop.Error.ENXIO ||
+ errorInfo.Error == Interop.Error.ESPIPE)
+ {
+ handle.SupportsRandomAccess = false;
+ bytesWritten = Interop.Sys.WriteV(handle, pinnedVectors, left.Length);
+ }
+ }
+ }
+ else
+ {
+ bytesWritten = Interop.Sys.WriteV(handle, pinnedVectors, left.Length);
+ }
}
FileStreamHelpers.CheckFileCall(bytesWritten, handle.Path);
diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c
index 08b42ba1791853..e3d62af03f1604 100644
--- a/src/native/libs/System.Native/pal_io.c
+++ b/src/native/libs/System.Native/pal_io.c
@@ -2001,13 +2001,37 @@ int64_t SystemNative_ReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount)
assert(vectors != NULL);
assert(vectorCount >= 0);
- int64_t count = 0;
int fileDescriptor = ToFileDescriptor(fd);
- while ((count = readv(fileDescriptor, (struct iovec*)vectors, (int)vectorCount)) < 0 && errno == EINTR);
+ while (1)
+ {
+ int64_t count;
+ while ((count = readv(fileDescriptor, (struct iovec*)vectors, (int)vectorCount)) < 0 && errno == EINTR);
- assert(count >= -1);
- return count;
+ if (count != -1 || (errno != EAGAIN && errno != EWOULDBLOCK))
+ {
+ assert(count >= -1);
+ return count;
+ }
+
+ // The fd is non-blocking and no data is available yet.
+ // Block (on a thread pool thread) until data arrives or the pipe/socket is closed.
+ PollEvent pollEvent = { .FileDescriptor = fileDescriptor, .Events = PAL_POLLIN, .TriggeredEvents = 0 };
+ uint32_t triggered = 0;
+ int32_t pollResult = Common_Poll(&pollEvent, 1, -1, &triggered);
+ if (pollResult != Error_SUCCESS)
+ {
+ errno = ConvertErrorPalToPlatform(pollResult);
+ return -1;
+ }
+
+ if ((pollEvent.TriggeredEvents & (PAL_POLLHUP | PAL_POLLERR)) != 0 &&
+ (pollEvent.TriggeredEvents & PAL_POLLIN) == 0)
+ {
+ // The pipe/socket was closed with no data available (EOF).
+ return 0;
+ }
+ }
}
int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset)
@@ -2056,13 +2080,38 @@ int64_t SystemNative_WriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount)
assert(vectors != NULL);
assert(vectorCount >= 0);
- int64_t count = 0;
int fileDescriptor = ToFileDescriptor(fd);
- while ((count = writev(fileDescriptor, (struct iovec*)vectors, (int)vectorCount)) < 0 && errno == EINTR);
+ while (1)
+ {
+ int64_t count;
+ while ((count = writev(fileDescriptor, (struct iovec*)vectors, (int)vectorCount)) < 0 && errno == EINTR);
- assert(count >= -1);
- return count;
+ if (count != -1 || (errno != EAGAIN && errno != EWOULDBLOCK))
+ {
+ assert(count >= -1);
+ return count;
+ }
+
+ // The fd is non-blocking and the write buffer is full.
+ // Block (on a thread pool thread) until space is available or the pipe/socket is closed.
+ PollEvent pollEvent = { .FileDescriptor = fileDescriptor, .Events = PAL_POLLOUT, .TriggeredEvents = 0 };
+ uint32_t triggered = 0;
+ int32_t pollResult = Common_Poll(&pollEvent, 1, -1, &triggered);
+ if (pollResult != Error_SUCCESS)
+ {
+ errno = ConvertErrorPalToPlatform(pollResult);
+ return -1;
+ }
+
+ if ((pollEvent.TriggeredEvents & (PAL_POLLHUP | PAL_POLLERR)) != 0 &&
+ (pollEvent.TriggeredEvents & PAL_POLLOUT) == 0)
+ {
+ // The pipe/socket was closed.
+ errno = EPIPE;
+ return -1;
+ }
+ }
}
int64_t SystemNative_PWriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset)
From a5d8cb20cadec6385dc6f52277db2d3a4b7448a7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Mar 2026 09:37:17 +0000
Subject: [PATCH 06/19] Extract NeedsNonOffsetFallback helper, improve tests
per jozkee feedback, use RandomAccess in Console
- Extract NeedsNonOffsetFallback helper method in RandomAccess.Unix.cs
- Improve tests: deduplicate cancellation checks, handle Read returning 0 in loops, use AssertExtensions.SequenceEqual
- Use RandomAccess.Read/Write in ConsolePal.Browser.cs, ConsolePal.Unix.cs, ConsolePal.Wasi.cs
- Remove unused Interop.Read.cs, Interop.Write.cs, Interop.Poll.cs references from System.Console.csproj
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../System.Console/src/System.Console.csproj | 18 ----
.../src/System/ConsolePal.Browser.cs | 29 ++----
.../src/System/ConsolePal.Unix.cs | 79 +++++-----------
.../src/System/ConsolePal.Wasi.cs | 56 +++---------
.../src/System/IO/RandomAccess.Unix.cs | 68 +++++---------
.../RandomAccess/NonSeekable.cs | 89 ++++++++++---------
6 files changed, 112 insertions(+), 227 deletions(-)
diff --git a/src/libraries/System.Console/src/System.Console.csproj b/src/libraries/System.Console/src/System.Console.csproj
index 341c9947b9c829..5f6a7012117b62 100644
--- a/src/libraries/System.Console/src/System.Console.csproj
+++ b/src/libraries/System.Console/src/System.Console.csproj
@@ -59,8 +59,6 @@
-
-
-
-
-
-
-
-
-
buffer) => throw Error.GetReadNotSupported();
- public override unsafe void Write(ReadOnlySpan buffer)
+ public override void Write(ReadOnlySpan buffer)
{
- fixed (byte* bufPtr = buffer)
+ try
{
- Write(_handle, bufPtr, buffer.Length);
+ RandomAccess.Write(_handle, buffer, fileOffset: 0);
}
- }
-
- private static unsafe void Write(SafeFileHandle fd, byte* bufPtr, int count)
- {
- while (count > 0)
+ catch (IOException ex) when (Interop.Sys.ConvertErrorPlatformToPal(ex.HResult) == Interop.Error.EPIPE)
{
- int bytesWritten = Interop.Sys.Write(fd, bufPtr, count);
- if (bytesWritten < 0)
- {
- Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
- if (errorInfo.Error == Interop.Error.EPIPE)
- {
- return;
- }
- else
- {
- throw Interop.GetIOException(errorInfo);
- }
- }
-
- count -= bytesWritten;
- bufPtr += bytesWritten;
+ // Broken pipe... simply pretend we were successful.
}
}
diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
index 57d4ef11fd62e3..2c9bb184800404 100644
--- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
+++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
@@ -943,14 +943,11 @@ private static unsafe void EnsureInitializedCore()
/// The file descriptor.
/// The buffer to read into.
/// The number of bytes read, or an exception if there's an error.
- private static unsafe int Read(SafeFileHandle fd, Span buffer)
+ private static int Read(SafeFileHandle fd, Span buffer)
{
- fixed (byte* bufPtr = buffer)
- {
- int result = Interop.CheckIo(Interop.Sys.Read(fd, bufPtr, buffer.Length));
- Debug.Assert(result <= buffer.Length);
- return result;
- }
+ int result = RandomAccess.Read(fd, buffer, fileOffset: 0);
+ Debug.Assert(result <= buffer.Length);
+ return result;
}
internal static void WriteToTerminal(ReadOnlySpan buffer, SafeFileHandle? handle = null, bool mayChangeCursorPosition = true)
@@ -978,65 +975,35 @@ internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySp
/// The file descriptor.
/// The buffer from which to write data.
/// Writing this buffer may change the cursor position.
- private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan buffer, bool mayChangeCursorPosition = true)
+ private static void Write(SafeFileHandle fd, ReadOnlySpan buffer, bool mayChangeCursorPosition = true)
{
- fixed (byte* p = buffer)
- {
- byte* bufPtr = p;
- int count = buffer.Length;
- while (count > 0)
- {
- int cursorVersion = mayChangeCursorPosition ? Volatile.Read(ref s_cursorVersion) : -1;
+ int cursorVersion = mayChangeCursorPosition ? Volatile.Read(ref s_cursorVersion) : -1;
- int bytesWritten = Interop.Sys.Write(fd, bufPtr, count);
- if (bytesWritten < 0)
- {
- Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
- if (errorInfo.Error == Interop.Error.EPIPE)
- {
- // Broken pipe... likely due to being redirected to a program
- // that ended, so simply pretend we were successful.
- return;
- }
- else if (errorInfo.Error == Interop.Error.EAGAIN) // aka EWOULDBLOCK
- {
- // May happen if the file handle is configured as non-blocking.
- // In that case, we need to wait to be able to write and then
- // try again. We poll, but don't actually care about the result,
- // only the blocking behavior, and thus ignore any poll errors
- // and loop around to do another write (which may correctly fail
- // if something else has gone wrong).
- Interop.Sys.Poll(fd, Interop.PollEvents.POLLOUT, Timeout.Infinite, out Interop.PollEvents triggered);
- continue;
- }
- else
- {
- // Something else... fail.
- throw Interop.GetExceptionForIoErrno(errorInfo);
- }
- }
- else
- {
- if (mayChangeCursorPosition)
- {
- UpdatedCachedCursorPosition(bufPtr, bytesWritten, cursorVersion);
- }
- }
+ try
+ {
+ RandomAccess.Write(fd, buffer, fileOffset: 0);
+ }
+ catch (IOException ex) when (Interop.Sys.ConvertErrorPlatformToPal(ex.HResult) == Interop.Error.EPIPE)
+ {
+ // Broken pipe... likely due to being redirected to a program
+ // that ended, so simply pretend we were successful.
+ return;
+ }
- count -= bytesWritten;
- bufPtr += bytesWritten;
- }
+ if (mayChangeCursorPosition)
+ {
+ UpdatedCachedCursorPosition(buffer, cursorVersion);
}
}
- private static unsafe void UpdatedCachedCursorPosition(byte* bufPtr, int count, int cursorVersion)
+ private static void UpdatedCachedCursorPosition(ReadOnlySpan buffer, int cursorVersion)
{
lock (Console.Out)
{
int left, top;
if (cursorVersion != s_cursorVersion || // the cursor was changed during the write by another operation
!TryGetCachedCursorPosition(out left, out top) || // we don't have a cursor position
- count > InteractiveBufferSize) // limit the amount of bytes we are willing to inspect
+ buffer.Length > InteractiveBufferSize) // limit the amount of bytes we are willing to inspect
{
InvalidateCachedCursorPosition();
return;
@@ -1044,9 +1011,9 @@ private static unsafe void UpdatedCachedCursorPosition(byte* bufPtr, int count,
GetWindowSize(out int width, out int height);
- for (int i = 0; i < count; i++)
+ for (int i = 0; i < buffer.Length; i++)
{
- byte c = bufPtr[i];
+ byte c = buffer[i];
if (c < 127 && c >= 32) // ASCII/UTF-8 characters that take up a single position
{
left++;
diff --git a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs
index 70aec828947c03..ebf8552ed9f811 100644
--- a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs
+++ b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs
@@ -248,14 +248,11 @@ internal static void EnsureConsoleInitialized()
/// The file descriptor.
/// The buffer to read into.
/// The number of bytes read, or an exception if there's an error.
- private static unsafe int Read(SafeFileHandle fd, Span buffer)
+ private static int Read(SafeFileHandle fd, Span buffer)
{
- fixed (byte* bufPtr = buffer)
- {
- int result = Interop.CheckIo(Interop.Sys.Read(fd, bufPtr, buffer.Length));
- Debug.Assert(result <= buffer.Length);
- return result;
- }
+ int result = RandomAccess.Read(fd, buffer, fileOffset: 0);
+ Debug.Assert(result <= buffer.Length);
+ return result;
}
internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySpan buffer)
@@ -271,45 +268,16 @@ internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySp
/// Writes data from the buffer into the file descriptor.
/// The file descriptor.
/// The buffer from which to write data.
- private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan buffer)
+ private static void Write(SafeFileHandle fd, ReadOnlySpan buffer)
{
- fixed (byte* p = buffer)
+ try
+ {
+ RandomAccess.Write(fd, buffer, fileOffset: 0);
+ }
+ catch (IOException ex) when (Interop.Sys.ConvertErrorPlatformToPal(ex.HResult) == Interop.Error.EPIPE)
{
- byte* bufPtr = p;
- int count = buffer.Length;
- while (count > 0)
- {
- int bytesWritten = Interop.Sys.Write(fd, bufPtr, count);
- if (bytesWritten < 0)
- {
- Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
- if (errorInfo.Error == Interop.Error.EPIPE)
- {
- // Broken pipe... likely due to being redirected to a program
- // that ended, so simply pretend we were successful.
- return;
- }
- else if (errorInfo.Error == Interop.Error.EAGAIN) // aka EWOULDBLOCK
- {
- // May happen if the file handle is configured as non-blocking.
- // In that case, we need to wait to be able to write and then
- // try again. We poll, but don't actually care about the result,
- // only the blocking behavior, and thus ignore any poll errors
- // and loop around to do another write (which may correctly fail
- // if something else has gone wrong).
- Interop.Sys.Poll(fd, Interop.PollEvents.POLLOUT, Timeout.Infinite, out Interop.PollEvents triggered);
- continue;
- }
- else
- {
- // Something else... fail.
- throw Interop.GetExceptionForIoErrno(errorInfo);
- }
- }
-
- count -= bytesWritten;
- bufPtr += bytesWritten;
- }
+ // Broken pipe... likely due to being redirected to a program
+ // that ended, so simply pretend we were successful.
}
}
}
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 bc5fd1edaaf76b..41dcdc19cee51d 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
@@ -38,18 +38,9 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer
{
// Try pread for seekable files.
result = Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset);
- if (result == -1)
+ if (result == -1 && NeedsNonOffsetFallback(handle))
{
- // We need to fallback to the non-offset version for certain file types
- // e.g: character devices (such as /dev/tty), pipes, and sockets.
- Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
-
- if (errorInfo.Error == Interop.Error.ENXIO ||
- errorInfo.Error == Interop.Error.ESPIPE)
- {
- handle.SupportsRandomAccess = false;
- result = Interop.Sys.Read(handle, bufPtr, buffer.Length);
- }
+ result = Interop.Sys.Read(handle, bufPtr, buffer.Length);
}
}
else
@@ -84,18 +75,9 @@ internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnly
if (handle.SupportsRandomAccess)
{
result = Interop.Sys.PReadV(handle, pinnedVectors, buffers.Count, fileOffset);
- if (result == -1)
+ if (result == -1 && NeedsNonOffsetFallback(handle))
{
- // We need to fallback to the non-offset version for certain file types
- // e.g: character devices (such as /dev/tty), pipes, and sockets.
- Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
-
- if (errorInfo.Error == Interop.Error.ENXIO ||
- errorInfo.Error == Interop.Error.ESPIPE)
- {
- handle.SupportsRandomAccess = false;
- result = Interop.Sys.ReadV(handle, pinnedVectors, buffers.Count);
- }
+ result = Interop.Sys.ReadV(handle, pinnedVectors, buffers.Count);
}
}
else
@@ -139,18 +121,9 @@ internal static unsafe void WriteAtOffset(SafeFileHandle handle, ReadOnlySpan> buffers, long fileOffset, CancellationToken cancellationToken)
=> handle.GetThreadPoolValueTaskSource().QueueWriteGather(buffers, fileOffset, cancellationToken);
+
+ private static bool NeedsNonOffsetFallback(SafeFileHandle handle)
+ {
+ // We need to fallback to the non-offset version for certain file types
+ // e.g: character devices (such as /dev/tty), pipes, and sockets.
+ Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
+
+ if (errorInfo.Error == Interop.Error.ENXIO ||
+ errorInfo.Error == Interop.Error.ESPIPE)
+ {
+ handle.SupportsRandomAccess = false;
+ return true;
+ }
+
+ return false;
+ }
}
}
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
index 3dbdaa66e1afe7..7066bb746bffc1 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
@@ -37,18 +37,16 @@ public void ThrowsTaskAlreadyCanceledForCancelledTokenAsync()
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);
- Assert.True(RandomAccess.ReadAsync(readHandle, GenerateVectors(1, 1), 0, token).IsCanceled);
- Assert.True(RandomAccess.WriteAsync(writeHandle, GenerateReadOnlyVectors(1, 1), 0, token).IsCanceled);
+ AssertCanceled(RandomAccess.ReadAsync(readHandle, new byte[1], 0, token).AsTask(), token);
+ AssertCanceled(RandomAccess.WriteAsync(writeHandle, new byte[1], 0, token).AsTask(), token);
+ AssertCanceled(RandomAccess.ReadAsync(readHandle, GenerateVectors(1, 1), 0, token).AsTask(), token);
+ AssertCanceled(RandomAccess.WriteAsync(writeHandle, GenerateReadOnlyVectors(1, 1), 0, token).AsTask(), token);
+ }
- TaskCanceledException ex = Assert.ThrowsAsync(() => RandomAccess.ReadAsync(readHandle, new byte[1], 0, token).AsTask()).Result;
- Assert.Equal(token, ex.CancellationToken);
- ex = Assert.ThrowsAsync(() => RandomAccess.WriteAsync(writeHandle, new byte[1], 0, token).AsTask()).Result;
- Assert.Equal(token, ex.CancellationToken);
- ex = Assert.ThrowsAsync(() => RandomAccess.ReadAsync(writeHandle, GenerateVectors(1, 1), 0, token).AsTask()).Result;
- Assert.Equal(token, ex.CancellationToken);
- ex = Assert.ThrowsAsync(() => RandomAccess.WriteAsync(writeHandle, GenerateReadOnlyVectors(1, 1), 0, token).AsTask()).Result;
+ static void AssertCanceled(Task task, CancellationToken token)
+ {
+ Assert.True(task.IsCanceled);
+ TaskCanceledException ex = Assert.ThrowsAsync(() => task).Result;
Assert.Equal(token, ex.CancellationToken);
}
}
@@ -65,11 +63,11 @@ public void ReadToAnEmptyBufferReturnsZeroWhenDataIsAvailable()
RandomAccess.Write(writeHandle, content, fileOffset: 0);
Assert.Equal(0, RandomAccess.Read(readHandle, Array.Empty(), fileOffset: 0));
- byte[] buffer = new byte[content.Length * 2];
+ byte[] buffer = new byte[content.Length];
ReadExactly(readHandle, buffer, content.Length);
- Assert.Equal(content, buffer.AsSpan(0, content.Length).ToArray());
+ AssertExtensions.SequenceEqual(content, buffer);
}
}
@@ -87,12 +85,12 @@ public async Task ReadToAnEmptyBufferReturnsZeroWhenDataIsAvailableAsync()
Assert.Equal(0, await readToEmpty);
- byte[] buffer = new byte[content.Length * 2];
+ byte[] buffer = new byte[content.Length];
Task readToNonEmpty = ReadExactlyAsync(readHandle, buffer, content.Length);
await Task.WhenAll(readToNonEmpty, write);
- Assert.Equal(content, buffer.AsSpan(0, content.Length).ToArray());
+ AssertExtensions.SequenceEqual(content, buffer);
}
}
@@ -112,9 +110,9 @@ public void CanReadToStackAllocatedMemory()
void ReadToStackAllocatedBuffer(SafeFileHandle handle, byte[] array)
{
- Span buffer = stackalloc byte[array.Length * 2];
+ Span buffer = stackalloc byte[array.Length];
ReadExactly(handle, buffer, array.Length);
- Assert.Equal(array, buffer.Slice(0, array.Length).ToArray());
+ AssertExtensions.SequenceEqual((ReadOnlySpan)array, (ReadOnlySpan)buffer);
}
}
@@ -127,12 +125,12 @@ public void CanWriteFromStackAllocatedMemory()
using (writeHandle)
{
byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- byte[] buffer = new byte[content.Length * 2];
+ byte[] buffer = new byte[content.Length];
WriteFromStackAllocatedBuffer(writeHandle, content);
ReadExactly(readHandle, buffer, content.Length);
- Assert.Equal(content, buffer.AsSpan(0, content.Length).ToArray());
+ AssertExtensions.SequenceEqual(content, buffer);
}
void WriteFromStackAllocatedBuffer(SafeFileHandle handle, byte[] array)
@@ -153,11 +151,11 @@ public void FileOffsetsAreIgnored_SyncWrite_SyncRead()
{
byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
RandomAccess.Write(writeHandle, content, fileOffset: 123);
- byte[] buffer = new byte[content.Length * 2];
+ byte[] buffer = new byte[content.Length];
int readFromOffset456 = RandomAccess.Read(readHandle, buffer, fileOffset: 456);
Assert.InRange(readFromOffset456, 1, content.Length);
- Assert.Equal(content.Take(readFromOffset456), buffer.AsSpan(0, readFromOffset456).ToArray());
+ AssertExtensions.SequenceEqual(content.AsSpan(0, readFromOffset456), buffer.AsSpan(0, readFromOffset456));
}
}
@@ -171,11 +169,11 @@ public async Task FileOffsetsAreIgnored_AsyncWrite_SyncRead()
{
byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
Task writeToOffset123 = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 123).AsTask();
- byte[] buffer = new byte[content.Length * 2];
+ byte[] buffer = new byte[content.Length];
int readFromOffset456 = RandomAccess.Read(readHandle, buffer, fileOffset: 456);
Assert.InRange(readFromOffset456, 1, content.Length);
- Assert.Equal(content.Take(readFromOffset456), buffer.AsSpan(0, readFromOffset456).ToArray());
+ AssertExtensions.SequenceEqual(content.AsSpan(0, readFromOffset456), buffer.AsSpan(0, readFromOffset456));
await writeToOffset123;
}
@@ -190,14 +188,14 @@ public async Task FileOffsetsAreIgnored_AsyncRead_SyncWrite()
using (writeHandle)
{
byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- byte[] buffer = new byte[content.Length * 2];
+ byte[] buffer = new byte[content.Length];
Task readFromOffset456 = RandomAccess.ReadAsync(readHandle, buffer, fileOffset: 456).AsTask();
RandomAccess.Write(writeHandle, content, fileOffset: 123);
int bytesRead = await readFromOffset456;
Assert.InRange(bytesRead, 1, content.Length);
- Assert.Equal(content.Take(bytesRead), buffer.AsSpan(0, bytesRead).ToArray());
+ AssertExtensions.SequenceEqual(content.AsSpan(0, bytesRead), buffer.AsSpan(0, bytesRead));
}
}
@@ -211,13 +209,13 @@ public async Task FileOffsetsAreIgnoredAsync()
{
byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
Task writeToOffset123 = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 123).AsTask();
- byte[] buffer = new byte[content.Length * 2];
+ byte[] buffer = new byte[content.Length];
Task readFromOffset456 = RandomAccess.ReadAsync(readHandle, buffer, fileOffset: 456).AsTask();
await Task.WhenAll(readFromOffset456, writeToOffset123);
Assert.InRange(readFromOffset456.Result, 1, content.Length);
- Assert.Equal(content.Take(readFromOffset456.Result), buffer.AsSpan(0, readFromOffset456.Result).ToArray());
+ AssertExtensions.SequenceEqual(content.AsSpan(0, readFromOffset456.Result), buffer.AsSpan(0, readFromOffset456.Result));
}
}
@@ -273,15 +271,20 @@ public async Task MultipleBuffersAreSupported_AsyncWrite_SyncReads()
{
ReadOnlyMemory[] vectors = GenerateReadOnlyVectors(VectorCount, BufferSize);
Task write = RandomAccess.WriteAsync(writeHandle, vectors, fileOffset: 123).AsTask();
- byte[] buffer = new byte[VectorsByteCount * 2];
+ byte[] buffer = new byte[VectorsByteCount];
int bytesRead = 0;
+ int read;
do
{
- bytesRead += RandomAccess.Read(readHandle, buffer.AsSpan(bytesRead), fileOffset: 456);
- } while (bytesRead != VectorsByteCount);
+ read = RandomAccess.Read(readHandle, buffer.AsSpan(bytesRead), fileOffset: 456);
+ Assert.InRange(read, 0, VectorsByteCount - bytesRead);
+ bytesRead += read;
+ } while (bytesRead != VectorsByteCount && read > 0);
- Assert.Equal(vectors.SelectMany(vector => vector.ToArray()), buffer.AsSpan(0, bytesRead).ToArray());
+ AssertExtensions.SequenceEqual(
+ vectors.SelectMany(vector => vector.ToArray()).ToArray().AsSpan(0, bytesRead),
+ buffer.AsSpan(0, bytesRead));
await write;
}
@@ -297,7 +300,6 @@ public async Task MultipleBuffersAreSupported_AsyncWrite_SyncRead()
{
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);
int bytesRead = (int)RandomAccess.Read(readHandle, writableVectors, fileOffset: 456);
@@ -319,7 +321,6 @@ public async Task MultipleBuffersAreSupported_AsyncWrite_AsyncRead()
{
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);
@@ -347,7 +348,7 @@ public async Task MultipleBuffersAreSupported_AsyncRead_SyncWrite()
int bytesRead = (int)await read;
Assert.InRange(bytesRead, 1, VectorsByteCount);
- Assert.Equal(content.Take(bytesRead), writableVectors.SelectMany(vector => vector.ToArray()).Take(bytesRead));
+ Assert.Equal(content.Take(bytesRead).ToArray(), writableVectors.SelectMany(vector => vector.ToArray()).Take(bytesRead).ToArray());
}
}
@@ -360,24 +361,30 @@ private static Memory[] GenerateVectors(int vectorCount, int bufferSize)
private static void ReadExactly(SafeFileHandle readHandle, Span buffer, int expectedByteCount)
{
int bytesRead = 0;
+ int read;
do
{
- bytesRead += RandomAccess.Read(readHandle, buffer.Slice(bytesRead), fileOffset: 0);
- } while (bytesRead != expectedByteCount);
+ read = RandomAccess.Read(readHandle, buffer.Slice(bytesRead), fileOffset: 0);
+ Assert.InRange(read, 0, expectedByteCount - bytesRead);
+ bytesRead += read;
+ } while (bytesRead != expectedByteCount && read > 0);
}
private static async Task ReadExactlyAsync(SafeFileHandle readHandle, byte[] buffer, int expectedByteCount)
{
int bytesRead = 0;
+ int read;
do
{
- bytesRead += await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(bytesRead), fileOffset: 0);
- } while (bytesRead != expectedByteCount);
+ read = await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(bytesRead), fileOffset: 0);
+ Assert.InRange(read, 0, expectedByteCount - bytesRead);
+ bytesRead += read;
+ } while (bytesRead != expectedByteCount && read > 0);
}
private static void AssertEqual(ReadOnlyMemory[] readOnlyVectors, Memory[] writableVectors, int byteCount)
- => Assert.Equal(
- readOnlyVectors.SelectMany(vector => vector.ToArray()).Take(byteCount),
- writableVectors.SelectMany(vector => vector.ToArray()).Take(byteCount));
+ => AssertExtensions.SequenceEqual(
+ readOnlyVectors.SelectMany(vector => vector.ToArray()).Take(byteCount).ToArray(),
+ writableVectors.SelectMany(vector => vector.ToArray()).Take(byteCount).ToArray());
}
}
From 8982b12768aca87fba9784fde3e8dd427c90f705 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Mar 2026 11:57:31 +0000
Subject: [PATCH 07/19] Replace ConsolePal.Read with RandomAccess.Read, merge
PartialReads tests into Theory, fix SkipOnPlatform text
- Replace ConsolePal.Read(_handle, buffer) with RandomAccess.Read(_handle, buffer, fileOffset: 0) in UnixConsoleStream
- Remove private Read(SafeFileHandle, Span) helper from ConsolePal.Unix.cs and ConsolePal.Wasi.cs
- Merge PartialReadsAreSupported and PartialReadsAreSupportedAsync into one [Theory] with useAsync parameter
- Fix SkipOnPlatform text: "Pipes are not supported on browser"
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../System/ConsolePal.Unix.ConsoleStream.cs | 2 +-
.../src/System/ConsolePal.Unix.cs | 11 -----
.../src/System/ConsolePal.Wasi.cs | 11 -----
.../RandomAccess/NonSeekable.cs | 43 ++++++++-----------
4 files changed, 20 insertions(+), 47 deletions(-)
diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs
index cd0adce8675ae3..fe0384b6a8160c 100644
--- a/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs
+++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs
@@ -44,7 +44,7 @@ public override int Read(Span buffer) =>
_useReadLine ?
ConsolePal.StdInReader.ReadLine(buffer) :
#endif
- ConsolePal.Read(_handle, buffer);
+ RandomAccess.Read(_handle, buffer, fileOffset: 0);
public override void Write(ReadOnlySpan buffer) =>
ConsolePal.WriteFromConsoleStream(_handle, buffer);
diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
index 2c9bb184800404..e62ea5ca79cb57 100644
--- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
+++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
@@ -939,17 +939,6 @@ private static unsafe void EnsureInitializedCore()
}
}
- /// Reads data from the file descriptor into the buffer.
- /// The file descriptor.
- /// The buffer to read into.
- /// The number of bytes read, or an exception if there's an error.
- private static int Read(SafeFileHandle fd, Span buffer)
- {
- int result = RandomAccess.Read(fd, buffer, fileOffset: 0);
- Debug.Assert(result <= buffer.Length);
- return result;
- }
-
internal static void WriteToTerminal(ReadOnlySpan buffer, SafeFileHandle? handle = null, bool mayChangeCursorPosition = true)
{
handle ??= s_terminalHandle;
diff --git a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs
index ebf8552ed9f811..53d20868d0d028 100644
--- a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs
+++ b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs
@@ -244,17 +244,6 @@ internal static void EnsureConsoleInitialized()
{
}
- /// Reads data from the file descriptor into the buffer.
- /// The file descriptor.
- /// The buffer to read into.
- /// The number of bytes read, or an exception if there's an error.
- private static int Read(SafeFileHandle fd, Span buffer)
- {
- int result = RandomAccess.Read(fd, buffer, fileOffset: 0);
- Debug.Assert(result <= buffer.Length);
- return result;
- }
-
internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySpan buffer)
{
EnsureConsoleInitialized();
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
index 7066bb746bffc1..4e0f3d59e635c2 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
@@ -10,7 +10,7 @@
namespace System.IO.Tests
{
- [SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")]
+ [SkipOnPlatform(TestPlatforms.Browser, "Pipes are not supported on browser")]
public class RandomAccess_NonSeekable : FileSystemTest
{
private const int VectorCount = 10;
@@ -219,8 +219,10 @@ public async Task FileOffsetsAreIgnoredAsync()
}
}
- [Fact]
- public void PartialReadsAreSupported()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task PartialReadsAreSupported(bool useAsync)
{
(SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
@@ -228,34 +230,27 @@ public void PartialReadsAreSupported()
using (writeHandle)
{
byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- RandomAccess.Write(writeHandle, content, fileOffset: 0);
-
- byte[] buffer = new byte[BufferSize];
-
- for (int i = 0; i < BufferSize; i++)
+ if (useAsync)
{
- Assert.Equal(1, RandomAccess.Read(readHandle, buffer.AsSpan(i, 1), fileOffset: 0));
+ await RandomAccess.WriteAsync(writeHandle, content, fileOffset: 0);
+ }
+ else
+ {
+ RandomAccess.Write(writeHandle, content, fileOffset: 0);
}
- Assert.Equal(content, buffer);
- }
- }
-
- [Fact]
- public async Task PartialReadsAreSupportedAsync()
- {
- (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
-
- using (readHandle)
- using (writeHandle)
- {
- byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- ValueTask write = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 0);
byte[] buffer = new byte[BufferSize];
for (int i = 0; i < BufferSize; i++)
{
- Assert.Equal(1, await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(i, 1), fileOffset: 0));
+ if (useAsync)
+ {
+ Assert.Equal(1, await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(i, 1), fileOffset: 0));
+ }
+ else
+ {
+ Assert.Equal(1, RandomAccess.Read(readHandle, buffer.AsSpan(i, 1), fileOffset: 0));
+ }
}
Assert.Equal(content, buffer);
}
From 1d5307cc6f29d4bf85ad6034bec73a59728d9a81 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Mar 2026 12:03:30 +0000
Subject: [PATCH 08/19] Fix ProcessWaitingTests: use ReadBlock instead of Read
to ensure all data is read
Replace StreamReader.Read with StreamReader.ReadBlock in both WaitForPeerProcess
and WaitAsyncForPeerProcess tests to prevent partial reads that leave null
characters in the output buffer.
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../System.Diagnostics.Process/tests/ProcessWaitingTests.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs
index 8ef602113be272..22db2706974389 100644
--- a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs
@@ -350,7 +350,7 @@ public void WaitForPeerProcess()
child2.StartInfo.RedirectStandardOutput = true;
child2.Start();
char[] output = new char[6];
- child2.StandardOutput.Read(output, 0, output.Length);
+ child2.StandardOutput.ReadBlock(output, 0, output.Length);
Assert.Equal("Signal", new string(output)); // wait for the signal before killing the peer
child1.Kill();
@@ -380,7 +380,7 @@ public async Task WaitAsyncForPeerProcess()
child2.StartInfo.RedirectStandardOutput = true;
child2.Start();
char[] output = new char[6];
- child2.StandardOutput.Read(output, 0, output.Length);
+ child2.StandardOutput.ReadBlock(output, 0, output.Length);
Assert.Equal("Signal", new string(output)); // wait for the signal before killing the peer
child1.Kill();
From 1dd370b72ada023aa395b3eb0e36c5d629b5090e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Mar 2026 14:46:27 +0000
Subject: [PATCH 09/19] Address reviewer feedback: revert Browser ConsolePal
changes, restore Interop.Write.cs for Browser, update docs to say "In .NET 11
and later versions"
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../System.Console/src/System.Console.csproj | 2 ++
.../src/System/ConsolePal.Browser.cs | 29 +++++++++++++++----
.../src/System/IO/RandomAccess.cs | 16 +++++-----
3 files changed, 34 insertions(+), 13 deletions(-)
diff --git a/src/libraries/System.Console/src/System.Console.csproj b/src/libraries/System.Console/src/System.Console.csproj
index 5f6a7012117b62..34c35a2f62cd64 100644
--- a/src/libraries/System.Console/src/System.Console.csproj
+++ b/src/libraries/System.Console/src/System.Console.csproj
@@ -59,6 +59,8 @@
+
buffer) => throw Error.GetReadNotSupported();
- public override void Write(ReadOnlySpan buffer)
+ public override unsafe void Write(ReadOnlySpan buffer)
{
- try
+ fixed (byte* bufPtr = buffer)
{
- RandomAccess.Write(_handle, buffer, fileOffset: 0);
+ Write(_handle, bufPtr, buffer.Length);
}
- catch (IOException ex) when (Interop.Sys.ConvertErrorPlatformToPal(ex.HResult) == Interop.Error.EPIPE)
+ }
+
+ private static unsafe void Write(SafeFileHandle fd, byte* bufPtr, int count)
+ {
+ while (count > 0)
{
- // Broken pipe... simply pretend we were successful.
+ int bytesWritten = Interop.Sys.Write(fd, bufPtr, count);
+ if (bytesWritten < 0)
+ {
+ Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
+ if (errorInfo.Error == Interop.Error.EPIPE)
+ {
+ return;
+ }
+ else
+ {
+ throw Interop.GetIOException(errorInfo);
+ }
+ }
+
+ count -= bytesWritten;
+ bufPtr += bytesWritten;
}
}
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 1b8e2e3d29d715..8b24c1aebd71e4 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs
@@ -54,7 +54,7 @@ public static void SetLength(SafeFileHandle handle, long length)
///
/// 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. For a file that does not support seeking (pipe or socket), it's ignored.
+ /// The file position to read from. In .NET 11 and later versions, 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.
@@ -75,7 +75,7 @@ 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. For a file that does not support seeking (pipe or socket), it's ignored.
+ /// The file position to read from. In .NET 11 and later versions, 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.
@@ -97,7 +97,7 @@ 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. For a file that does not support seeking (pipe or socket), it's ignored.
+ /// The file position to read from. In .NET 11 and later versions, 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 .
@@ -124,7 +124,7 @@ 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. For a file that does not support seeking (pipe or socket), it's ignored.
+ /// The file position to read from. In .NET 11 and later versions, 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 .
@@ -152,7 +152,7 @@ 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. For a file that does not support seeking (pipe or socket), it's ignored.
+ /// The file position to write to. In .NET 11 and later versions, for a file that does not support seeking (pipe or socket), it's ignored.
/// is .
/// is invalid.
/// The file is closed.
@@ -172,7 +172,7 @@ 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. For a file that does not support seeking (pipe or socket), it's ignored.
+ /// The file position to write to. In .NET 11 and later versions, for a file that does not support seeking (pipe or socket), it's ignored.
/// or is .
/// is invalid.
/// The file is closed.
@@ -193,7 +193,7 @@ 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. For a file that does not support seeking (pipe or socket), it's ignored.
+ /// The file position to write to. In .NET 11 and later versions, 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 .
@@ -220,7 +220,7 @@ 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. For a file that does not support seeking (pipe or socket), it's ignored.
+ /// The file position to write to. In .NET 11 and later versions, 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 .
From 38df64ce0eb5c38564c35ccab87972a8ac2a9868 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Fri, 13 Mar 2026 20:37:51 +0100
Subject: [PATCH 10/19] fix test bugs introduced by Opus when it was moving my
old tests to the new PR
---
.../RandomAccess/NonSeekable.cs | 312 +++++++-----------
1 file changed, 128 insertions(+), 184 deletions(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
index 4e0f3d59e635c2..95d7a5b34e75b9 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs
@@ -10,6 +10,15 @@
namespace System.IO.Tests
{
+ // This class uses SafeFileHandle.CreateAnonymousPipe to create non-seekable file handles.
+ // On Windows, anonymous pipes are just named pipes.
+ // By default, all named pipes are created with blocking-wait mode enabled (PIPE_WAIT).
+ // With a blocking-wait handle (it's orthogonal FILE_FLAG_OVERLAPPED), the write operation
+ // cannot succeed until sufficient space is created in the buffer by reading from the other end of the pipe.
+ // It means that even small write operations may not complete until the corresponding
+ // read operations are issued on the other end of the pipe.
+ // That is why this class issues async reads before synchronous writes and async writes before synchronous reads.
+ // Source: https://learn.microsoft.com/windows/win32/ipc/named-pipe-type-read-and-wait-modes
[SkipOnPlatform(TestPlatforms.Browser, "Pipes are not supported on browser")]
public class RandomAccess_NonSeekable : FileSystemTest
{
@@ -51,86 +60,81 @@ static void AssertCanceled(Task task, CancellationToken token)
}
}
- [Fact]
- public void ReadToAnEmptyBufferReturnsZeroWhenDataIsAvailable()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ReadToAnEmptyBufferReturnsZeroWhenDataIsAvailable(bool asyncRead)
{
(SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
using (readHandle)
using (writeHandle)
{
- byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- RandomAccess.Write(writeHandle, content, fileOffset: 0);
+ byte[] writeBuffer = RandomNumberGenerator.GetBytes(BufferSize);
+ byte[] readBuffer = new byte[writeBuffer.Length];
- Assert.Equal(0, RandomAccess.Read(readHandle, Array.Empty(), fileOffset: 0));
- byte[] buffer = new byte[content.Length];
+ ValueTask writeTask = RandomAccess.WriteAsync(writeHandle, writeBuffer, fileOffset: 0);
- ReadExactly(readHandle, buffer, content.Length);
+ Assert.Equal(0, asyncRead
+ ? await RandomAccess.ReadAsync(readHandle, Array.Empty(), fileOffset: 0)
+ : RandomAccess.Read(readHandle, Array.Empty(), fileOffset: 0));
- AssertExtensions.SequenceEqual(content, buffer);
+ if (asyncRead)
+ {
+ await ReadExactlyAsync(readHandle, readBuffer, writeBuffer.Length);
+ }
+ else
+ {
+ ReadExactly(readHandle, readBuffer, writeBuffer.Length);
+ }
+
+ await writeTask;
+ AssertExtensions.SequenceEqual(writeBuffer, readBuffer);
}
}
[Fact]
- public async Task ReadToAnEmptyBufferReturnsZeroWhenDataIsAvailableAsync()
+ public async Task CanReadToStackAllocatedMemory()
{
(SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
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();
-
- Assert.Equal(0, await readToEmpty);
-
- byte[] buffer = new byte[content.Length];
- Task readToNonEmpty = ReadExactlyAsync(readHandle, buffer, content.Length);
-
- await Task.WhenAll(readToNonEmpty, write);
+ byte[] writeBuffer = RandomNumberGenerator.GetBytes(BufferSize);
- AssertExtensions.SequenceEqual(content, buffer);
- }
- }
+ ValueTask writeTask = RandomAccess.WriteAsync(writeHandle, writeBuffer, fileOffset: 0);
- [Fact]
- public void CanReadToStackAllocatedMemory()
- {
- (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
+ ReadToStackAllocatedBuffer(readHandle, writeBuffer);
- using (readHandle)
- using (writeHandle)
- {
- byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- RandomAccess.Write(writeHandle, content, fileOffset: 0);
-
- ReadToStackAllocatedBuffer(readHandle, content);
+ await writeTask;
}
- void ReadToStackAllocatedBuffer(SafeFileHandle handle, byte[] array)
+ void ReadToStackAllocatedBuffer(SafeFileHandle handle, byte[] writeBuffer)
{
- Span buffer = stackalloc byte[array.Length];
- ReadExactly(handle, buffer, array.Length);
- AssertExtensions.SequenceEqual((ReadOnlySpan)array, (ReadOnlySpan)buffer);
+ Span readBuffer = stackalloc byte[writeBuffer.Length];
+ ReadExactly(handle, readBuffer, writeBuffer.Length);
+ AssertExtensions.SequenceEqual((ReadOnlySpan)writeBuffer, readBuffer);
}
}
[Fact]
- public void CanWriteFromStackAllocatedMemory()
+ public async Task CanWriteFromStackAllocatedMemory()
{
(SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
using (readHandle)
using (writeHandle)
{
- byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- byte[] buffer = new byte[content.Length];
+ byte[] writeBuffer = RandomNumberGenerator.GetBytes(BufferSize);
+ byte[] readBuffer = new byte[writeBuffer.Length];
- WriteFromStackAllocatedBuffer(writeHandle, content);
+ Task readTask = ReadExactlyAsync(readHandle, readBuffer, writeBuffer.Length);
- ReadExactly(readHandle, buffer, content.Length);
- AssertExtensions.SequenceEqual(content, buffer);
+ WriteFromStackAllocatedBuffer(writeHandle, writeBuffer);
+
+ await readTask;
+ AssertExtensions.SequenceEqual(writeBuffer, readBuffer);
}
void WriteFromStackAllocatedBuffer(SafeFileHandle handle, byte[] array)
@@ -141,81 +145,33 @@ void WriteFromStackAllocatedBuffer(SafeFileHandle handle, byte[] array)
}
}
- [Fact]
- public void FileOffsetsAreIgnored_SyncWrite_SyncRead()
- {
- (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
-
- using (readHandle)
- using (writeHandle)
- {
- byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- RandomAccess.Write(writeHandle, content, fileOffset: 123);
- byte[] buffer = new byte[content.Length];
- int readFromOffset456 = RandomAccess.Read(readHandle, buffer, fileOffset: 456);
-
- Assert.InRange(readFromOffset456, 1, content.Length);
- AssertExtensions.SequenceEqual(content.AsSpan(0, readFromOffset456), buffer.AsSpan(0, readFromOffset456));
- }
- }
-
- [Fact]
- public async Task FileOffsetsAreIgnored_AsyncWrite_SyncRead()
- {
- (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
-
- using (readHandle)
- using (writeHandle)
- {
- byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- Task writeToOffset123 = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 123).AsTask();
- byte[] buffer = new byte[content.Length];
- int readFromOffset456 = RandomAccess.Read(readHandle, buffer, fileOffset: 456);
-
- Assert.InRange(readFromOffset456, 1, content.Length);
- AssertExtensions.SequenceEqual(content.AsSpan(0, readFromOffset456), buffer.AsSpan(0, readFromOffset456));
-
- await writeToOffset123;
- }
- }
-
- [Fact]
- public async Task FileOffsetsAreIgnored_AsyncRead_SyncWrite()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task FileOffsetsAreIgnored(bool asyncWrite)
{
(SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
using (readHandle)
using (writeHandle)
{
- byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- byte[] buffer = new byte[content.Length];
- Task readFromOffset456 = RandomAccess.ReadAsync(readHandle, buffer, fileOffset: 456).AsTask();
+ byte[] writeBuffer = RandomNumberGenerator.GetBytes(BufferSize);
+ byte[] readBuffer = new byte[writeBuffer.Length];
- RandomAccess.Write(writeHandle, content, fileOffset: 123);
+ ValueTask readTask = RandomAccess.ReadAsync(readHandle, readBuffer, fileOffset: 456);
- int bytesRead = await readFromOffset456;
- Assert.InRange(bytesRead, 1, content.Length);
- AssertExtensions.SequenceEqual(content.AsSpan(0, bytesRead), buffer.AsSpan(0, bytesRead));
- }
- }
-
- [Fact]
- public async Task FileOffsetsAreIgnoredAsync()
- {
- (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
-
- using (readHandle)
- using (writeHandle)
- {
- byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- Task writeToOffset123 = RandomAccess.WriteAsync(writeHandle, content, fileOffset: 123).AsTask();
- byte[] buffer = new byte[content.Length];
- Task readFromOffset456 = RandomAccess.ReadAsync(readHandle, buffer, fileOffset: 456).AsTask();
-
- await Task.WhenAll(readFromOffset456, writeToOffset123);
+ if (asyncWrite)
+ {
+ await RandomAccess.WriteAsync(writeHandle, writeBuffer, fileOffset: 123);
+ }
+ else
+ {
+ RandomAccess.Write(writeHandle, writeBuffer, fileOffset: 123);
+ }
- Assert.InRange(readFromOffset456.Result, 1, content.Length);
- AssertExtensions.SequenceEqual(content.AsSpan(0, readFromOffset456.Result), buffer.AsSpan(0, readFromOffset456.Result));
+ int readFromOffset456 = await readTask;
+ Assert.InRange(readFromOffset456, 1, writeBuffer.Length);
+ AssertExtensions.SequenceEqual(writeBuffer.AsSpan(0, readFromOffset456), readBuffer.AsSpan(0, readFromOffset456));
}
}
@@ -229,30 +185,25 @@ public async Task PartialReadsAreSupported(bool useAsync)
using (readHandle)
using (writeHandle)
{
- byte[] content = RandomNumberGenerator.GetBytes(BufferSize);
- if (useAsync)
- {
- await RandomAccess.WriteAsync(writeHandle, content, fileOffset: 0);
- }
- else
- {
- RandomAccess.Write(writeHandle, content, fileOffset: 0);
- }
+ byte[] writeBuffer = RandomNumberGenerator.GetBytes(BufferSize);
+ byte[] readBuffer = new byte[BufferSize];
- byte[] buffer = new byte[BufferSize];
+ ValueTask writeTask = RandomAccess.WriteAsync(writeHandle, writeBuffer, fileOffset: 0);
for (int i = 0; i < BufferSize; i++)
{
if (useAsync)
{
- Assert.Equal(1, await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(i, 1), fileOffset: 0));
+ Assert.Equal(1, await RandomAccess.ReadAsync(readHandle, readBuffer.AsMemory(i, 1), fileOffset: 0));
}
else
{
- Assert.Equal(1, RandomAccess.Read(readHandle, buffer.AsSpan(i, 1), fileOffset: 0));
+ Assert.Equal(1, RandomAccess.Read(readHandle, readBuffer.AsSpan(i, 1), fileOffset: 0));
}
}
- Assert.Equal(content, buffer);
+
+ await writeTask;
+ Assert.Equal(writeBuffer, readBuffer);
}
}
@@ -264,86 +215,79 @@ public async Task MultipleBuffersAreSupported_AsyncWrite_SyncReads()
using (readHandle)
using (writeHandle)
{
- ReadOnlyMemory[] vectors = GenerateReadOnlyVectors(VectorCount, BufferSize);
- Task write = RandomAccess.WriteAsync(writeHandle, vectors, fileOffset: 123).AsTask();
- byte[] buffer = new byte[VectorsByteCount];
+ ReadOnlyMemory[] writeVectors = GenerateReadOnlyVectors(VectorCount, BufferSize);
+ byte[] readBuffer = new byte[VectorsByteCount];
+
+ ValueTask writeTask = RandomAccess.WriteAsync(writeHandle, writeVectors, fileOffset: 123);
- int bytesRead = 0;
- int read;
+ int totalBytesRead = 0;
+ int bytesRead;
do
{
- read = RandomAccess.Read(readHandle, buffer.AsSpan(bytesRead), fileOffset: 456);
- Assert.InRange(read, 0, VectorsByteCount - bytesRead);
- bytesRead += read;
- } while (bytesRead != VectorsByteCount && read > 0);
+ bytesRead = RandomAccess.Read(readHandle, readBuffer.AsSpan(totalBytesRead), fileOffset: 456);
+ Assert.InRange(bytesRead, 0, VectorsByteCount - totalBytesRead);
+ totalBytesRead += bytesRead;
+ } while (totalBytesRead != VectorsByteCount && bytesRead > 0);
+ await writeTask;
AssertExtensions.SequenceEqual(
- vectors.SelectMany(vector => vector.ToArray()).ToArray().AsSpan(0, bytesRead),
- buffer.AsSpan(0, bytesRead));
-
- await write;
+ writeVectors.SelectMany(vector => vector.ToArray()).ToArray().AsSpan(0, totalBytesRead),
+ readBuffer.AsSpan(0, totalBytesRead));
}
}
- [Fact]
- public async Task MultipleBuffersAreSupported_AsyncWrite_SyncRead()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task MultipleBuffersAreSupported_AsyncWrite_ThenRead(bool asyncRead)
{
(SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
using (readHandle)
using (writeHandle)
{
- ReadOnlyMemory[] readOnlyVectors = GenerateReadOnlyVectors(VectorCount, BufferSize);
- Task write = RandomAccess.WriteAsync(writeHandle, readOnlyVectors, fileOffset: 123).AsTask();
-
- Memory[] writableVectors = GenerateVectors(VectorCount, BufferSize);
- int bytesRead = (int)RandomAccess.Read(readHandle, writableVectors, fileOffset: 456);
+ ReadOnlyMemory[] writeVectors = GenerateReadOnlyVectors(VectorCount, BufferSize);
+ Memory[] readVectors = GenerateVectors(VectorCount, BufferSize);
- Assert.InRange(bytesRead, 1, VectorsByteCount);
- AssertEqual(readOnlyVectors, writableVectors, bytesRead);
+ ValueTask writeTask = RandomAccess.WriteAsync(writeHandle, writeVectors, fileOffset: 123);
- await write;
- }
- }
-
- [Fact]
- public async Task MultipleBuffersAreSupported_AsyncWrite_AsyncRead()
- {
- (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
-
- using (readHandle)
- using (writeHandle)
- {
- ReadOnlyMemory[] readOnlyVectors = GenerateReadOnlyVectors(VectorCount, BufferSize);
- Task write = RandomAccess.WriteAsync(writeHandle, readOnlyVectors, fileOffset: 123).AsTask();
+ long bytesRead = asyncRead
+ ? await RandomAccess.ReadAsync(readHandle, readVectors, fileOffset: 456)
+ : RandomAccess.Read(readHandle, readVectors, fileOffset: 456);
- Memory[] writableVectors = GenerateVectors(VectorCount, BufferSize);
-
- long bytesRead = await RandomAccess.ReadAsync(readHandle, writableVectors, fileOffset: 456);
+ await writeTask;
Assert.InRange(bytesRead, 1, VectorsByteCount);
- AssertEqual(readOnlyVectors, writableVectors, (int)bytesRead);
-
- await write;
+ AssertEqual(writeVectors, readVectors, (int)bytesRead);
}
}
- [Fact]
- public async Task MultipleBuffersAreSupported_AsyncRead_SyncWrite()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task MultipleBuffersAreSupported_AsyncRead_ThenWrite(bool asyncWrite)
{
(SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles();
using (readHandle)
using (writeHandle)
{
- Memory[] writableVectors = GenerateVectors(VectorCount, BufferSize);
- ValueTask read = RandomAccess.ReadAsync(readHandle, writableVectors, fileOffset: 456);
+ Memory[] readVectors = GenerateVectors(VectorCount, BufferSize);
+ byte[] writeBuffer = RandomNumberGenerator.GetBytes(VectorsByteCount);
- byte[] content = RandomNumberGenerator.GetBytes(VectorsByteCount);
- RandomAccess.Write(writeHandle, content, fileOffset: 123);
+ ValueTask readTask = RandomAccess.ReadAsync(readHandle, readVectors, fileOffset: 456);
+
+ if (asyncWrite)
+ {
+ await RandomAccess.WriteAsync(writeHandle, writeBuffer, fileOffset: 123);
+ }
+ else
+ {
+ RandomAccess.Write(writeHandle, writeBuffer, fileOffset: 123);
+ }
- int bytesRead = (int)await read;
+ int bytesRead = (int)await readTask;
Assert.InRange(bytesRead, 1, VectorsByteCount);
- Assert.Equal(content.Take(bytesRead).ToArray(), writableVectors.SelectMany(vector => vector.ToArray()).Take(bytesRead).ToArray());
+ Assert.Equal(writeBuffer.Take(bytesRead).ToArray(), readVectors.SelectMany(vector => vector.ToArray()).Take(bytesRead).ToArray());
}
}
@@ -355,26 +299,26 @@ private static Memory[] GenerateVectors(int vectorCount, int bufferSize)
private static void ReadExactly(SafeFileHandle readHandle, Span buffer, int expectedByteCount)
{
- int bytesRead = 0;
- int read;
+ int totalBytesRead = 0;
+ int bytesRead;
do
{
- read = RandomAccess.Read(readHandle, buffer.Slice(bytesRead), fileOffset: 0);
- Assert.InRange(read, 0, expectedByteCount - bytesRead);
- bytesRead += read;
- } while (bytesRead != expectedByteCount && read > 0);
+ bytesRead = RandomAccess.Read(readHandle, buffer.Slice(totalBytesRead), fileOffset: 0);
+ Assert.InRange(bytesRead, 0, expectedByteCount - totalBytesRead);
+ totalBytesRead += bytesRead;
+ } while (totalBytesRead != expectedByteCount && bytesRead > 0);
}
private static async Task ReadExactlyAsync(SafeFileHandle readHandle, byte[] buffer, int expectedByteCount)
{
- int bytesRead = 0;
- int read;
+ int totalBytesRead = 0;
+ int bytesRead;
do
{
- read = await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(bytesRead), fileOffset: 0);
- Assert.InRange(read, 0, expectedByteCount - bytesRead);
- bytesRead += read;
- } while (bytesRead != expectedByteCount && read > 0);
+ bytesRead = await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(totalBytesRead), fileOffset: 0);
+ Assert.InRange(bytesRead, 0, expectedByteCount - totalBytesRead);
+ totalBytesRead += bytesRead;
+ } while (totalBytesRead != expectedByteCount && bytesRead > 0);
}
private static void AssertEqual(ReadOnlyMemory[] readOnlyVectors, Memory[] writableVectors, int byteCount)
From d0709ef993500fd350a7905be5d65a116f4d88c9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 15:10:08 +0000
Subject: [PATCH 11/19] Use GetAllowedVectorCount in ReadV/WriteV and fix doc
typos
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
src/native/libs/System.Native/pal_io.c | 8 ++++----
src/native/libs/System.Native/pal_io.h | 8 ++++----
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c
index e3d62af03f1604..8d25d7b370dbf0 100644
--- a/src/native/libs/System.Native/pal_io.c
+++ b/src/native/libs/System.Native/pal_io.c
@@ -1949,7 +1949,6 @@ int32_t SystemNative_PWrite(intptr_t fd, void* buffer, int32_t bufferSize, int64
return (int32_t)count;
}
-#if (HAVE_PREADV || HAVE_PWRITEV) && !defined(TARGET_WASM)
static int GetAllowedVectorCount(IOVector* vectors, int32_t vectorCount)
{
#if defined(IOV_MAX)
@@ -1994,7 +1993,6 @@ static int GetAllowedVectorCount(IOVector* vectors, int32_t vectorCount)
return allowedCount;
}
-#endif // (HAVE_PREADV || HAVE_PWRITEV) && !defined(TARGET_WASM)
int64_t SystemNative_ReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount)
{
@@ -2002,11 +2000,12 @@ int64_t SystemNative_ReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount)
assert(vectorCount >= 0);
int fileDescriptor = ToFileDescriptor(fd);
+ int allowedVectorCount = GetAllowedVectorCount(vectors, vectorCount);
while (1)
{
int64_t count;
- while ((count = readv(fileDescriptor, (struct iovec*)vectors, (int)vectorCount)) < 0 && errno == EINTR);
+ while ((count = readv(fileDescriptor, (struct iovec*)vectors, allowedVectorCount)) < 0 && errno == EINTR);
if (count != -1 || (errno != EAGAIN && errno != EWOULDBLOCK))
{
@@ -2081,11 +2080,12 @@ int64_t SystemNative_WriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount)
assert(vectorCount >= 0);
int fileDescriptor = ToFileDescriptor(fd);
+ int allowedVectorCount = GetAllowedVectorCount(vectors, vectorCount);
while (1)
{
int64_t count;
- while ((count = writev(fileDescriptor, (struct iovec*)vectors, (int)vectorCount)) < 0 && errno == EINTR);
+ while ((count = writev(fileDescriptor, (struct iovec*)vectors, allowedVectorCount)) < 0 && errno == EINTR);
if (count != -1 || (errno != EAGAIN && errno != EWOULDBLOCK))
{
diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h
index 3927921b5d411a..b27b45659b0a2e 100644
--- a/src/native/libs/System.Native/pal_io.h
+++ b/src/native/libs/System.Native/pal_io.h
@@ -875,27 +875,27 @@ PALEXPORT int32_t SystemNative_PWrite(intptr_t fd, void* buffer, int32_t bufferS
/**
* Reads the number of bytes specified into the provided buffers from the specified, opened file descriptor at specified offset.
*
- * Returns the number of bytes read on success; otherwise, -1 is returned an errno is set.
+ * Returns the number of bytes read on success; otherwise, -1 is returned and errno is set.
*/
PALEXPORT int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset);
/**
* Writes the number of bytes specified in the buffers into the specified, opened file descriptor at specified offset.
*
- * Returns the number of bytes written on success; otherwise, -1 is returned an errno is set.
+ * Returns the number of bytes written on success; otherwise, -1 is returned and errno is set.
*/
PALEXPORT int64_t SystemNative_PWriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset);
/**
* Reads the number of bytes specified into the provided buffers from the specified, opened file descriptor.
*
- * Returns the number of bytes read on success; otherwise, -1 is returned an errno is set.
+ * Returns the number of bytes read on success; otherwise, -1 is returned and errno is set.
*/
PALEXPORT int64_t SystemNative_ReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount);
/**
* Writes the number of bytes specified in the buffers into the specified, opened file descriptor
*
- * Returns the number of bytes written on success; otherwise, -1 is returned an errno is set.
+ * Returns the number of bytes written on success; otherwise, -1 is returned and errno is set.
*/
PALEXPORT int64_t SystemNative_WriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount);
From a674e0fbbe3c52e67a0950dcb2350f068eba54e5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 15:15:58 +0000
Subject: [PATCH 12/19] Fill pipe buffer before testing write cancellation to
avoid flaky sync completion
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../RandomAccess/NonSeekable_AsyncHandles.cs | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs
index edec0ee6f8158b..8cef00eb741a3d 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs
@@ -28,6 +28,25 @@ public async Task CancellationIsSupported(FileAccess access)
Assert.True(handle.IsAsync);
+ // For writes, fill the pipe buffer first so the next write will block
+ // (a small write to a fresh pipe can complete synchronously).
+ if (access == FileAccess.Write)
+ {
+ byte[] filler = new byte[1024 * 1024]; // 1 MB should exceed any pipe buffer
+ try
+ {
+ while (true)
+ {
+ using CancellationTokenSource fillCts = new(TimeSpan.FromMilliseconds(500));
+ await RandomAccess.WriteAsync(handle, filler, 0, fillCts.Token);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Pipe buffer is now full; next write will reliably block.
+ }
+ }
+
CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(250));
CancellationToken token = cts.Token;
byte[] buffer = new byte[1];
From 19d57c8a89ca69230ffdf53ec04c2f4ef3361e1d Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Sat, 14 Mar 2026 16:22:04 +0100
Subject: [PATCH 13/19] Revert a bug introduced by the Agent.
This reverts commit a674e0fbbe3c52e67a0950dcb2350f068eba54e5.
---
.../RandomAccess/NonSeekable_AsyncHandles.cs | 19 -------------------
1 file changed, 19 deletions(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs
index 8cef00eb741a3d..edec0ee6f8158b 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs
@@ -28,25 +28,6 @@ public async Task CancellationIsSupported(FileAccess access)
Assert.True(handle.IsAsync);
- // For writes, fill the pipe buffer first so the next write will block
- // (a small write to a fresh pipe can complete synchronously).
- if (access == FileAccess.Write)
- {
- byte[] filler = new byte[1024 * 1024]; // 1 MB should exceed any pipe buffer
- try
- {
- while (true)
- {
- using CancellationTokenSource fillCts = new(TimeSpan.FromMilliseconds(500));
- await RandomAccess.WriteAsync(handle, filler, 0, fillCts.Token);
- }
- }
- catch (OperationCanceledException)
- {
- // Pipe buffer is now full; next write will reliably block.
- }
- }
-
CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(250));
CancellationToken token = cts.Token;
byte[] buffer = new byte[1];
From d7322eee03a100681f75261846956ab8facdf237 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Sat, 14 Mar 2026 16:24:31 +0100
Subject: [PATCH 14/19] Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
.../RandomAccess/NonSeekable_AsyncHandles.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs
index edec0ee6f8158b..611760e361b17d 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs
@@ -30,7 +30,7 @@ public async Task CancellationIsSupported(FileAccess access)
CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(250));
CancellationToken token = cts.Token;
- byte[] buffer = new byte[1];
+ byte[] buffer = new byte[1024 * 1024]; // use a large buffer to ensure the async pipe write is pending
OperationCanceledException ex = await Assert.ThrowsAsync(
() => access == FileAccess.Write
From da4579d204c77b29b7e53627a99e5a6864ba9ab5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Mar 2026 19:58:56 +0000
Subject: [PATCH 15/19] Address @stephentoub feedback: foreach loop,
consolidate ReadV/WriteV calls, rename helper
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/System/ConsolePal.Unix.cs | 3 +-
.../src/System/IO/RandomAccess.Unix.cs | 41 ++++++++++++++-----
2 files changed, 31 insertions(+), 13 deletions(-)
diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
index e62ea5ca79cb57..8845acada13e2a 100644
--- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
+++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
@@ -1000,9 +1000,8 @@ private static void UpdatedCachedCursorPosition(ReadOnlySpan buffer, int c
GetWindowSize(out int width, out int height);
- for (int i = 0; i < buffer.Length; i++)
+ foreach (byte c in buffer)
{
- byte c = buffer[i];
if (c < 127 && c >= 32) // ASCII/UTF-8 characters that take up a single position
{
left++;
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 41dcdc19cee51d..0578da2dd3b9c2 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
@@ -38,7 +38,7 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer
{
// Try pread for seekable files.
result = Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset);
- if (result == -1 && NeedsNonOffsetFallback(handle))
+ if (result == -1 && ShouldFallBackToNonOffsetSyscall(handle))
{
result = Interop.Sys.Read(handle, bufPtr, buffer.Length);
}
@@ -58,7 +58,7 @@ internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnly
MemoryHandle[] handles = new MemoryHandle[buffers.Count];
Span vectors = buffers.Count <= IovStackThreshold ? stackalloc Interop.Sys.IOVector[IovStackThreshold] : new Interop.Sys.IOVector[buffers.Count];
- long result;
+ long result = -1;
try
{
int buffersCount = buffers.Count;
@@ -75,12 +75,19 @@ internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnly
if (handle.SupportsRandomAccess)
{
result = Interop.Sys.PReadV(handle, pinnedVectors, buffers.Count, fileOffset);
- if (result == -1 && NeedsNonOffsetFallback(handle))
+ if (result == -1)
{
- result = Interop.Sys.ReadV(handle, pinnedVectors, buffers.Count);
+ Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
+ if (errorInfo.Error == Interop.Error.ENXIO ||
+ errorInfo.Error == Interop.Error.ESPIPE)
+ {
+ handle.SupportsRandomAccess = false;
+ // Fall through to non-offset ReadV below.
+ }
}
}
- else
+
+ if (!handle.SupportsRandomAccess)
{
result = Interop.Sys.ReadV(handle, pinnedVectors, buffers.Count);
}
@@ -121,7 +128,7 @@ internal static unsafe void WriteAtOffset(SafeFileHandle handle, ReadOnlySpan 0)
{
- long bytesWritten;
+ long bytesWritten = -1;
Span left = vectors.Slice(buffersOffset);
fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(left))
{
if (handle.SupportsRandomAccess)
{
bytesWritten = Interop.Sys.PWriteV(handle, pinnedVectors, left.Length, fileOffset);
- if (bytesWritten == -1 && NeedsNonOffsetFallback(handle))
+ if (bytesWritten == -1)
{
- bytesWritten = Interop.Sys.WriteV(handle, pinnedVectors, left.Length);
+ Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
+ if (errorInfo.Error == Interop.Error.ENXIO ||
+ errorInfo.Error == Interop.Error.ESPIPE)
+ {
+ handle.SupportsRandomAccess = false;
+ // Fall through to non-offset WriteV below.
+ }
}
}
- else
+
+ if (!handle.SupportsRandomAccess)
{
bytesWritten = Interop.Sys.WriteV(handle, pinnedVectors, left.Length);
}
@@ -258,7 +272,12 @@ internal static ValueTask WriteAtOffsetAsync(SafeFileHandle handle, ReadOnlyMemo
private static ValueTask WriteGatherAtOffsetAsync(SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset, CancellationToken cancellationToken)
=> handle.GetThreadPoolValueTaskSource().QueueWriteGather(buffers, fileOffset, cancellationToken);
- private static bool NeedsNonOffsetFallback(SafeFileHandle handle)
+ ///
+ /// Checks the last error after a failed pread/pwrite/preadv/pwritev call
+ /// and returns true if the error indicates a non-seekable file type (ENXIO or ESPIPE),
+ /// updating to false.
+ ///
+ private static bool ShouldFallBackToNonOffsetSyscall(SafeFileHandle handle)
{
// We need to fallback to the non-offset version for certain file types
// e.g: character devices (such as /dev/tty), pipes, and sockets.
From 272996b3185067044db3f8ee2e1c06ff7282ea41 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Tue, 17 Mar 2026 11:26:46 +0100
Subject: [PATCH 16/19] try to simplify the logic responsible for falling
through to non-offset code path
---
.../src/System/IO/RandomAccess.Unix.cs | 58 ++++++-------------
1 file changed, 19 insertions(+), 39 deletions(-)
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 0578da2dd3b9c2..4829e515485854 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
@@ -38,12 +38,14 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer
{
// Try pread for seekable files.
result = Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset);
- if (result == -1 && ShouldFallBackToNonOffsetSyscall(handle))
+
+ if (result == -1 && ShouldFallBackToNonOffsetSyscall(Interop.Sys.GetLastErrorInfo()))
{
- result = Interop.Sys.Read(handle, bufPtr, buffer.Length);
+ handle.SupportsRandomAccess = false; // Fall through to non-offset Read below.
}
}
- else
+
+ if (!handle.IsAsync && !handle.SupportsRandomAccess)
{
result = Interop.Sys.Read(handle, bufPtr, buffer.Length);
}
@@ -75,15 +77,10 @@ internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnly
if (handle.SupportsRandomAccess)
{
result = Interop.Sys.PReadV(handle, pinnedVectors, buffers.Count, fileOffset);
- if (result == -1)
+
+ if (result == -1 && ShouldFallBackToNonOffsetSyscall(Interop.Sys.GetLastErrorInfo()))
{
- Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
- if (errorInfo.Error == Interop.Error.ENXIO ||
- errorInfo.Error == Interop.Error.ESPIPE)
- {
- handle.SupportsRandomAccess = false;
- // Fall through to non-offset ReadV below.
- }
+ handle.SupportsRandomAccess = false; // Fall through to non-offset ReadV below.
}
}
@@ -128,12 +125,14 @@ internal static unsafe void WriteAtOffset(SafeFileHandle handle, ReadOnlySpan
/// Checks the last error after a failed pread/pwrite/preadv/pwritev call
- /// and returns true if the error indicates a non-seekable file type (ENXIO or ESPIPE),
- /// updating to false.
+ /// and returns true if the error indicates a non-seekable file type (ENXIO or ESPIPE).
///
- private static bool ShouldFallBackToNonOffsetSyscall(SafeFileHandle handle)
- {
- // We need to fallback to the non-offset version for certain file types
- // e.g: character devices (such as /dev/tty), pipes, and sockets.
- Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
-
- if (errorInfo.Error == Interop.Error.ENXIO ||
- errorInfo.Error == Interop.Error.ESPIPE)
- {
- handle.SupportsRandomAccess = false;
- return true;
- }
-
- return false;
- }
+ private static bool ShouldFallBackToNonOffsetSyscall(Interop.ErrorInfo lastError)
+ => lastError.Error == Interop.Error.ENXIO || lastError.Error == Interop.Error.ESPIPE;
}
}
From 3b45b71f0f4773132ddc37e8fa6fab7e807b7852 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Tue, 17 Mar 2026 15:03:07 +0100
Subject: [PATCH 17/19] fix the build
---
.../System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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 4829e515485854..cf2e56021b15a9 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
@@ -29,7 +29,7 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer
// The Windows implementation uses ReadFile, which ignores the offset if the handle
// isn't seekable. We do the same manually with PRead vs Read, in order to enable
// the function to be used by FileStream for all the same situations.
- int result;
+ int result = -1;
if (handle.IsAsync)
{
result = Interop.Sys.ReadFromNonblocking(handle, bufPtr, buffer.Length);
@@ -117,7 +117,7 @@ internal static unsafe void WriteAtOffset(SafeFileHandle handle, ReadOnlySpan
Date: Tue, 17 Mar 2026 14:16:14 +0000
Subject: [PATCH 18/19] Restore NotSupportedException docs with version
qualification for Read/Write methods
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../System.Private.CoreLib/src/System/IO/RandomAccess.cs | 8 ++++++++
1 file changed, 8 insertions(+)
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 8b24c1aebd71e4..154e8b4e5f8892 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs
@@ -59,6 +59,7 @@ public static void SetLength(SafeFileHandle handle, long length)
/// is .
/// is invalid.
/// The file is closed.
+ /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket).
/// is negative.
/// was not opened for reading.
/// An I/O error occurred.
@@ -80,6 +81,7 @@ public static int Read(SafeFileHandle handle, Span buffer, long fileOffset
/// or is .
/// is invalid.
/// The file is closed.
+ /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket).
/// is negative.
/// was not opened for reading.
/// An I/O error occurred.
@@ -103,6 +105,7 @@ public static long Read(SafeFileHandle handle, IReadOnlyList> buffe
/// is .
/// is invalid.
/// The file is closed.
+ /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket).
/// is negative.
/// was not opened for reading.
/// An I/O error occurred.
@@ -130,6 +133,7 @@ public static ValueTask ReadAsync(SafeFileHandle handle, Memory buffe
/// or is .
/// is invalid.
/// The file is closed.
+ /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket).
/// is negative.
/// was not opened for reading.
/// An I/O error occurred.
@@ -156,6 +160,7 @@ public static ValueTask ReadAsync(SafeFileHandle handle, IReadOnlyList is .
/// is invalid.
/// The file is closed.
+ /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket).
/// is negative.
/// was not opened for writing.
/// An I/O error occurred.
@@ -176,6 +181,7 @@ public static void Write(SafeFileHandle handle, ReadOnlySpan buffer, long
/// or is .
/// is invalid.
/// The file is closed.
+ /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket).
/// is negative.
/// was not opened for writing.
/// An I/O error occurred.
@@ -199,6 +205,7 @@ public static void Write(SafeFileHandle handle, IReadOnlyList is .
/// is invalid.
/// The file is closed.
+ /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket).
/// is negative.
/// was not opened for writing.
/// An I/O error occurred.
@@ -226,6 +233,7 @@ public static ValueTask WriteAsync(SafeFileHandle handle, ReadOnlyMemory b
/// or is .
/// is invalid.
/// The file is closed.
+ /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket).
/// is negative.
/// was not opened for writing.
/// An I/O error occurred.
From c8572c84f498fa90ffff341f1ceb89f18c6b2f8c Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Sun, 22 Mar 2026 16:10:44 +0100
Subject: [PATCH 19/19] Apply suggestions from code review
Co-authored-by: Stephen Toub
---
.../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 cf2e56021b15a9..c16a258a0d701e 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
@@ -271,6 +271,6 @@ private static ValueTask WriteGatherAtOffsetAsync(SafeFileHandle handle, IReadOn
/// and returns true if the error indicates a non-seekable file type (ENXIO or ESPIPE).
///
private static bool ShouldFallBackToNonOffsetSyscall(Interop.ErrorInfo lastError)
- => lastError.Error == Interop.Error.ENXIO || lastError.Error == Interop.Error.ESPIPE;
+ => lastError.Error is Interop.Error.ENXIO or Interop.Error.ESPIPE;
}
}