Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b1a3fd8
Initial plan
Copilot Mar 12, 2026
5d43e89
Relax RandomAccess type requirements - make Read/Write work with non-…
Copilot Mar 12, 2026
ea20ad6
Fix FlushToDisk test to use SafeFileHandle.CreateAnonymousPipe instea…
Copilot Mar 12, 2026
62659a4
Fix test: use bytesRead local instead of readFromOffset456.Result for…
Copilot Mar 12, 2026
098316d
Add PReadV/PWriteV -> ReadV/WriteV fallback and EAGAIN/EWOULDBLOCK ha…
Copilot Mar 13, 2026
a5d8cb2
Extract NeedsNonOffsetFallback helper, improve tests per jozkee feedb…
Copilot Mar 13, 2026
8982b12
Replace ConsolePal.Read with RandomAccess.Read, merge PartialReads te…
Copilot Mar 13, 2026
1d5307c
Fix ProcessWaitingTests: use ReadBlock instead of Read to ensure all …
Copilot Mar 13, 2026
1dd370b
Address reviewer feedback: revert Browser ConsolePal changes, restore…
Copilot Mar 13, 2026
38df64c
fix test bugs introduced by Opus when it was moving my old tests to t…
adamsitnik Mar 13, 2026
d0709ef
Use GetAllowedVectorCount in ReadV/WriteV and fix doc typos
Copilot Mar 14, 2026
a674e0f
Fill pipe buffer before testing write cancellation to avoid flaky syn…
Copilot Mar 14, 2026
19d57c8
Revert a bug introduced by the Agent.
adamsitnik Mar 14, 2026
d7322ee
Apply suggestions from code review
adamsitnik Mar 14, 2026
da4579d
Address @stephentoub feedback: foreach loop, consolidate ReadV/WriteV…
Copilot Mar 16, 2026
272996b
try to simplify the logic responsible for falling through to non-offs…
adamsitnik Mar 17, 2026
3b45b71
fix the build
adamsitnik Mar 17, 2026
ddab128
Restore NotSupportedException docs with version qualification for Rea…
Copilot Mar 17, 2026
c6926fc
Merge branch 'main' into copilot/relax-randomaccess-requirements
adamsitnik Mar 18, 2026
7c37673
Merge branch 'main' into copilot/relax-randomaccess-requirements
adamsitnik Mar 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
16 changes: 0 additions & 16 deletions src/libraries/System.Console/src/System.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,8 @@
Link="Common\Interop\Unix\Interop.StdinReady.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.ReadStdinUnbuffered.cs"
Link="Common\Interop\Unix\Interop.ReadStdinUnbuffered.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />
<Compile Include="$(CommonPath)System\Text\EncodingHelper.Unix.cs"
Link="Common\System\Text\EncodingHelper.Unix.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Write.cs"
Link="Common\Interop\Unix\Interop.Write.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs"
Link="Common\Interop\Unix\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Errors.cs"
Expand Down Expand Up @@ -233,10 +225,6 @@
Link="Common\Interop\Unix\Interop.Open.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.OpenFlags.cs"
Link="Common\Interop\Unix\Interop.OpenFlags.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetEUid.cs"
Link="Common\Interop\Unix\Interop.GetEUid.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetPwUid.cs"
Expand All @@ -249,10 +237,6 @@
Link="Common\Interop\Unix\Interop.SNPrintF.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Stat.cs"
Link="Common\Interop\Unix\Interop.Stat.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Write.cs"
Link="Common\Interop\Unix\Interop.Write.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetWindowWidth.cs"
Link="Common\Interop\Unix\Interop.GetWindowWidth.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.InitializeTerminalAndSignalHandling.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public override int Read(Span<byte> buffer) =>
_useReadLine ?
ConsolePal.StdInReader.ReadLine(buffer) :
#endif
ConsolePal.Read(_handle, buffer);
RandomAccess.Read(_handle, buffer, fileOffset: 0);

public override void Write(ReadOnlySpan<byte> buffer) =>
ConsolePal.WriteFromConsoleStream(_handle, buffer);
Expand Down
81 changes: 18 additions & 63 deletions src/libraries/System.Console/src/System/ConsolePal.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -939,20 +939,6 @@ private static unsafe void EnsureInitializedCore()
}
}

/// <summary>Reads data from the file descriptor into the buffer.</summary>
/// <param name="fd">The file descriptor.</param>
/// <param name="buffer">The buffer to read into.</param>
/// <returns>The number of bytes read, or an exception if there's an error.</returns>
private static unsafe int Read(SafeFileHandle fd, Span<byte> buffer)
{
fixed (byte* bufPtr = buffer)
{
int result = Interop.CheckIo(Interop.Sys.Read(fd, bufPtr, buffer.Length));
Debug.Assert(result <= buffer.Length);
return result;
}
}

internal static void WriteToTerminal(ReadOnlySpan<byte> buffer, SafeFileHandle? handle = null, bool mayChangeCursorPosition = true)
{
handle ??= s_terminalHandle;
Expand All @@ -978,75 +964,44 @@ internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySp
/// <param name="fd">The file descriptor.</param>
/// <param name="buffer">The buffer from which to write data.</param>
/// <param name="mayChangeCursorPosition">Writing this buffer may change the cursor position.</param>
private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan<byte> buffer, bool mayChangeCursorPosition = true)
private static void Write(SafeFileHandle fd, ReadOnlySpan<byte> 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<byte> 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;
}

GetWindowSize(out int width, out int height);

for (int i = 0; i < count; i++)
foreach (byte c in buffer)
{
byte c = bufPtr[i];
if (c < 127 && c >= 32) // ASCII/UTF-8 characters that take up a single position
{
left++;
Expand Down
59 changes: 8 additions & 51 deletions src/libraries/System.Console/src/System/ConsolePal.Wasi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,20 +244,6 @@ internal static void EnsureConsoleInitialized()
{
}

/// <summary>Reads data from the file descriptor into the buffer.</summary>
/// <param name="fd">The file descriptor.</param>
/// <param name="buffer">The buffer to read into.</param>
/// <returns>The number of bytes read, or an exception if there's an error.</returns>
private static unsafe int Read(SafeFileHandle fd, Span<byte> buffer)
{
fixed (byte* bufPtr = buffer)
{
int result = Interop.CheckIo(Interop.Sys.Read(fd, bufPtr, buffer.Length));
Debug.Assert(result <= buffer.Length);
return result;
}
}

internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySpan<byte> buffer)
{
EnsureConsoleInitialized();
Expand All @@ -271,45 +257,16 @@ internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySp
/// <summary>Writes data from the buffer into the file descriptor.</summary>
/// <param name="fd">The file descriptor.</param>
/// <param name="buffer">The buffer from which to write data.</param>
private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan<byte> buffer)
private static void Write(SafeFileHandle fd, ReadOnlySpan<byte> 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.
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To other reviewers: this change was needed because we emulate partial writes in RandomAccess non-optimized builds for testing purposes:

private static int GetNumberOfBytesToWrite(int byteCount)
{
#if DEBUG
// In debug only, to assist with testing, simulate writing fewer than the requested number of bytes.
if (byteCount > 1 && // ensure we don't turn the read into a zero-byte read
byteCount < 512) // avoid on larger buffers that might have a length used to meet an alignment requirement
{
byteCount /= 2;
}
#endif
return byteCount;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't this WaitForPeerProcess always wrong, and we were just getting lucky because of implementation detail? i.e. regardless of that debug condition, there's no guarantee Read would always return as much as was requested

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, more or less we were always lucky so far. I wanted to be clear why it was not needed before (to show it's not a bug now)

Assert.Equal("Signal", new string(output)); // wait for the signal before killing the peer

child1.Kill();
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2557,6 +2557,9 @@
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.PRead.cs">
<Link>Common\Interop\Unix\System.Native\Interop.PRead.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.ReadV.cs">
<Link>Common\Interop\Unix\System.Native\Interop.ReadV.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.PReadV.cs">
<Link>Common\Interop\Unix\System.Native\Interop.PReadV.cs</Link>
</Compile>
Expand All @@ -2569,6 +2572,9 @@
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs">
<Link>Common\Interop\Unix\System.Native\Interop.Read.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.WriteV.cs">
<Link>Common\Interop\Unix\System.Native\Interop.WriteV.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.ReadDir.cs">
<Link>Common\Interop\Unix\System.Native\Interop.ReadDir.cs</Link>
</Compile>
Expand Down
Loading
Loading