Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files - #125512

Merged
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements
Mar 22, 2026
Merged

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files#125512
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements

Conversation

CopilotAI commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Description

Relaxes System.IO.RandomAccess to support non-seekable handles (pipes, sockets, character devices) by falling back to non-offset syscalls when the file does not support seeking. Updates affected call sites in System.Console and System.Diagnostics.Process to use RandomAccess.Read/Write, and adds comprehensive tests for non-seekable handle scenarios.

Core changes

  • Update RandomAccess validation to allow non-seekable handles for Read*/Write* methods (while keeping seek-only behavior for GetLength/SetLength)
  • Add SystemNative_ReadV/SystemNative_WriteV native exports with EAGAIN/EWOULDBLOCK poll-loop handling and GetAllowedVectorCount capping for IOV_MAX
  • Extract ShouldFallBackToNonOffsetSyscall helper for ENXIO/ESPIPE fallback logic, with consolidated control flow to avoid duplicate syscalls
  • Update XML doc comments with version-qualified behavior: "In .NET 11 and later versions, ..." for new non-seekable support, and restored NotSupportedException docs qualified with "In .NET 10 and earlier versions, ..." for backward compatibility

Console/Process call site updates

  • Replace Interop.Sys.Read/Write with RandomAccess.Read/Write in ConsolePal.Unix.cs, ConsolePal.Wasi.cs, and ConsolePal.Unix.ConsoleStream.cs (with EPIPE handling preserved)
  • Revert ConsolePal.Browser.cs changes (WASM does not support libSystem.Native dynamic linking)
  • Use foreach loop in UpdatedCachedCursorPosition instead of index-based iteration
  • Fix ProcessWaitingTests to use ReadBlock instead of Read to handle partial reads correctly

Tests

  • Add non-seekable handle tests for single/multi-buffer sync/async read/write, cancellation, and partial reads
  • Apply test improvements from PR Make RandomAccess.Read*|Write* methods work with non-seekable files #96711: AssertCanceled helper, AssertExtensions.SequenceEqual, break on read == 0
  • Fill pipe buffer before write cancellation test to avoid flaky sync completion
  • Merge PartialReads sync/async tests into a single [Theory]

Testing

  • All System.IO.FileSystem.Tests pass (9741 tests)
  • System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, default)
  • ProcessWaitingTests pass with ReadBlock fix
Original prompt

This section details on the original issue you should resolve

<issue_title>Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files</issue_title>
<issue_description>In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or separate PR, the Unix implementation of overloads that accept multiple buffers should start using readv and writev sys-calls. This should be relatively easy (just search for preadv and pwritev and reuse the patterns)

But it's going to allow to:

<agent_instructions>1. Do exactly what I did in #96711, you can even cherry-pick the git commits if you want.
2. For testing, in RandomAccess_NonSeekable.GetNamedPipeHandlesAsync don't use NamedPipeClientStream or NamedPipeServerStream just use the bran new SafeFileHandle.CreateAnonymousPipe(out readHandle, out writeHandle, bool asyncRead = false, bool asyncWrite = false) API.
3. Search the repo for places where we have SafeFileHandle and could use the RandomAccess API instead of performing direct sys-calls. Use the API there.
4. Make sure you run all the src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests test and they are passing before you send the PR.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ Tagging subscribers to this area: @dotnet/area-system-io See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
Issue Details

In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or...


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

CopilotAIand others added 3 commits March 12, 2026 22:49
…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>
…d of AnonymousPipeServerStream
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
… consistency
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
…ndling
- 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

All four review items addressed in commit 098316d. All 9741 System.IO.FileSystem.Tests pass.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception):

privatestaticunsafevoidWrite(SafeFileHandlefd,byte*bufPtr,intcount)
{
while(count>0)
{
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=Interop.Sys.GetLastErrorInfo();
if(errorInfo.Error==Interop.Error.EPIPE)
{
return;
}
else
{
throwInterop.GetIOException(errorInfo);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}

privatestaticunsafevoidWrite(SafeFileHandlefd,ReadOnlySpan<byte>buffer,boolmayChangeCursorPosition=true)
{
fixed (byte*p=buffer)
{
byte*bufPtr=p;
intcount=buffer.Length;
while(count>0)
{
intcursorVersion=mayChangeCursorPosition?Volatile.Read(refs_cursorVersion):-1;
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=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;
}
elseif(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,outInterop.PollEventstriggered);
continue;
}
else
{
// Something else... fail.
throwInterop.GetExceptionForIoErrno(errorInfo);
}
}
else
{
if(mayChangeCursorPosition)
{
UpdatedCachedCursorPosition(bufPtr,bytesWritten,cursorVersion);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}
}

use the RandomAccess.Read in following places:

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

If it does not break the build, remove related references from the project file (the list below is not complete, please verify it on your own):

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
…ack, 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception): ...

Done in commit a5d8cb2:

  • ConsolePal.Browser.cs: Replaced Interop.Sys.Write loop with RandomAccess.Write, EPIPE handled via catch (IOException) when (ConvertErrorPlatformToPal(ex.HResult) == EPIPE)
  • ConsolePal.Unix.cs: Read uses RandomAccess.Read, Write uses RandomAccess.Write with EPIPE catch and cursor tracking after the full write. UpdatedCachedCursorPosition now takes ReadOnlySpan<byte> instead of byte*.
  • ConsolePal.Wasi.cs: Both Read and Write use RandomAccess.Read/Write with EPIPE handling
  • Removed Interop.Read.cs, Interop.Write.cs, Interop.Poll.cs, Interop.Poll.Structs.cs from Browser, WASI, and Unix sections of System.Console.csproj

All 9741 System.IO.FileSystem.Tests pass, and System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, and the default).

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Console/src/System/ConsolePal.Unix.cs Outdated

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@stephentoub I believe I've addressed all your feedback, PTAL.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnik
adamsitnik enabled auto-merge (squash) March 22, 2026 15:11
@adamsitnik

Copy link
Copy Markdown
Member

/ba-g failures are unrelated (bad exit)

@adamsitnik
adamsitnik merged commit 9dd9f6f into mainMar 22, 2026
163 of 166 checks passed
@adamsitnik
adamsitnik deleted the copilot/relax-randomaccess-requirements branch March 22, 2026 19:31
eiriktsarpalis pushed a commit that referenced this pull request Mar 23, 2026
…th non-seekable files (#125512)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnikadamsitnik added this to the 11.0.0 milestone Mar 31, 2026
CopilotAI added a commit that referenced this pull request Apr 19, 2026
Reverts all source changes (ConsolePal.Unix.cs, ConsolePal.Wasi.cs,
ConsolePal.Unix.ConsoleStream.cs, System.Console.csproj) back to the
pre-#125512 state that uses Interop.Sys.Read/Write directly instead
of RandomAccess.Read/Write.
Only the new test methods (CanCopyStandardInputToStandardOutput,
UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent) and the
ConsoleHandles.cs csproj include are kept.
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ed523a85-0c0f-4eae-849f-f050c054fd96
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik added a commit that referenced this pull request Apr 20, 2026
…seekable file (#126844)
Reverts all System.Console source changes from PR #125512 (which
introduced `RandomAccess.Read`/`Write` based I/O) back to the original
`Interop.Sys.Read`/`Write` implementation. The `FileStream`-based
approach will be revisited separately.
Adds regression tests that verify `Console.OpenStandardInput().CopyTo()`
and `Console.OpenStandardOutput().Write()` work correctly when
stdin/stdout is redirected to a seekable file.
fixes#126843
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 1, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files

5 participants

@adamsitnik@stephentoub@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files - #125512

Merged
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements
Mar 22, 2026
Merged

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files#125512
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements

Conversation

CopilotAI commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Description

Relaxes System.IO.RandomAccess to support non-seekable handles (pipes, sockets, character devices) by falling back to non-offset syscalls when the file does not support seeking. Updates affected call sites in System.Console and System.Diagnostics.Process to use RandomAccess.Read/Write, and adds comprehensive tests for non-seekable handle scenarios.

Core changes

  • Update RandomAccess validation to allow non-seekable handles for Read*/Write* methods (while keeping seek-only behavior for GetLength/SetLength)
  • Add SystemNative_ReadV/SystemNative_WriteV native exports with EAGAIN/EWOULDBLOCK poll-loop handling and GetAllowedVectorCount capping for IOV_MAX
  • Extract ShouldFallBackToNonOffsetSyscall helper for ENXIO/ESPIPE fallback logic, with consolidated control flow to avoid duplicate syscalls
  • Update XML doc comments with version-qualified behavior: "In .NET 11 and later versions, ..." for new non-seekable support, and restored NotSupportedException docs qualified with "In .NET 10 and earlier versions, ..." for backward compatibility

Console/Process call site updates

  • Replace Interop.Sys.Read/Write with RandomAccess.Read/Write in ConsolePal.Unix.cs, ConsolePal.Wasi.cs, and ConsolePal.Unix.ConsoleStream.cs (with EPIPE handling preserved)
  • Revert ConsolePal.Browser.cs changes (WASM does not support libSystem.Native dynamic linking)
  • Use foreach loop in UpdatedCachedCursorPosition instead of index-based iteration
  • Fix ProcessWaitingTests to use ReadBlock instead of Read to handle partial reads correctly

Tests

  • Add non-seekable handle tests for single/multi-buffer sync/async read/write, cancellation, and partial reads
  • Apply test improvements from PR Make RandomAccess.Read*|Write* methods work with non-seekable files #96711: AssertCanceled helper, AssertExtensions.SequenceEqual, break on read == 0
  • Fill pipe buffer before write cancellation test to avoid flaky sync completion
  • Merge PartialReads sync/async tests into a single [Theory]

Testing

  • All System.IO.FileSystem.Tests pass (9741 tests)
  • System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, default)
  • ProcessWaitingTests pass with ReadBlock fix
Original prompt

This section details on the original issue you should resolve

<issue_title>Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files</issue_title>
<issue_description>In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or separate PR, the Unix implementation of overloads that accept multiple buffers should start using readv and writev sys-calls. This should be relatively easy (just search for preadv and pwritev and reuse the patterns)

But it's going to allow to:

<agent_instructions>1. Do exactly what I did in #96711, you can even cherry-pick the git commits if you want.
2. For testing, in RandomAccess_NonSeekable.GetNamedPipeHandlesAsync don't use NamedPipeClientStream or NamedPipeServerStream just use the bran new SafeFileHandle.CreateAnonymousPipe(out readHandle, out writeHandle, bool asyncRead = false, bool asyncWrite = false) API.
3. Search the repo for places where we have SafeFileHandle and could use the RandomAccess API instead of performing direct sys-calls. Use the API there.
4. Make sure you run all the src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests test and they are passing before you send the PR.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ Tagging subscribers to this area: @dotnet/area-system-io See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
Issue Details

In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or...


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

CopilotAIand others added 3 commits March 12, 2026 22:49
…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>
…d of AnonymousPipeServerStream
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
… consistency
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
…ndling
- 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

All four review items addressed in commit 098316d. All 9741 System.IO.FileSystem.Tests pass.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception):

privatestaticunsafevoidWrite(SafeFileHandlefd,byte*bufPtr,intcount)
{
while(count>0)
{
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=Interop.Sys.GetLastErrorInfo();
if(errorInfo.Error==Interop.Error.EPIPE)
{
return;
}
else
{
throwInterop.GetIOException(errorInfo);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}

privatestaticunsafevoidWrite(SafeFileHandlefd,ReadOnlySpan<byte>buffer,boolmayChangeCursorPosition=true)
{
fixed (byte*p=buffer)
{
byte*bufPtr=p;
intcount=buffer.Length;
while(count>0)
{
intcursorVersion=mayChangeCursorPosition?Volatile.Read(refs_cursorVersion):-1;
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=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;
}
elseif(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,outInterop.PollEventstriggered);
continue;
}
else
{
// Something else... fail.
throwInterop.GetExceptionForIoErrno(errorInfo);
}
}
else
{
if(mayChangeCursorPosition)
{
UpdatedCachedCursorPosition(bufPtr,bytesWritten,cursorVersion);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}
}

use the RandomAccess.Read in following places:

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

If it does not break the build, remove related references from the project file (the list below is not complete, please verify it on your own):

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
…ack, 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception): ...

Done in commit a5d8cb2:

  • ConsolePal.Browser.cs: Replaced Interop.Sys.Write loop with RandomAccess.Write, EPIPE handled via catch (IOException) when (ConvertErrorPlatformToPal(ex.HResult) == EPIPE)
  • ConsolePal.Unix.cs: Read uses RandomAccess.Read, Write uses RandomAccess.Write with EPIPE catch and cursor tracking after the full write. UpdatedCachedCursorPosition now takes ReadOnlySpan<byte> instead of byte*.
  • ConsolePal.Wasi.cs: Both Read and Write use RandomAccess.Read/Write with EPIPE handling
  • Removed Interop.Read.cs, Interop.Write.cs, Interop.Poll.cs, Interop.Poll.Structs.cs from Browser, WASI, and Unix sections of System.Console.csproj

All 9741 System.IO.FileSystem.Tests pass, and System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, and the default).

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Console/src/System/ConsolePal.Unix.cs Outdated

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@stephentoub I believe I've addressed all your feedback, PTAL.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnik
adamsitnik enabled auto-merge (squash) March 22, 2026 15:11
@adamsitnik

Copy link
Copy Markdown
Member

/ba-g failures are unrelated (bad exit)

@adamsitnik
adamsitnik merged commit 9dd9f6f into mainMar 22, 2026
163 of 166 checks passed
@adamsitnik
adamsitnik deleted the copilot/relax-randomaccess-requirements branch March 22, 2026 19:31
eiriktsarpalis pushed a commit that referenced this pull request Mar 23, 2026
…th non-seekable files (#125512)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnikadamsitnik added this to the 11.0.0 milestone Mar 31, 2026
CopilotAI added a commit that referenced this pull request Apr 19, 2026
Reverts all source changes (ConsolePal.Unix.cs, ConsolePal.Wasi.cs,
ConsolePal.Unix.ConsoleStream.cs, System.Console.csproj) back to the
pre-#125512 state that uses Interop.Sys.Read/Write directly instead
of RandomAccess.Read/Write.
Only the new test methods (CanCopyStandardInputToStandardOutput,
UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent) and the
ConsoleHandles.cs csproj include are kept.
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ed523a85-0c0f-4eae-849f-f050c054fd96
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik added a commit that referenced this pull request Apr 20, 2026
…seekable file (#126844)
Reverts all System.Console source changes from PR #125512 (which
introduced `RandomAccess.Read`/`Write` based I/O) back to the original
`Interop.Sys.Read`/`Write` implementation. The `FileStream`-based
approach will be revisited separately.
Adds regression tests that verify `Console.OpenStandardInput().CopyTo()`
and `Console.OpenStandardOutput().Write()` work correctly when
stdin/stdout is redirected to a seekable file.
fixes#126843
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 1, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files

5 participants

@adamsitnik@stephentoub@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files - #125512

Merged
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements
Mar 22, 2026
Merged

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files#125512
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements

Conversation

CopilotAI commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Description

Relaxes System.IO.RandomAccess to support non-seekable handles (pipes, sockets, character devices) by falling back to non-offset syscalls when the file does not support seeking. Updates affected call sites in System.Console and System.Diagnostics.Process to use RandomAccess.Read/Write, and adds comprehensive tests for non-seekable handle scenarios.

Core changes

  • Update RandomAccess validation to allow non-seekable handles for Read*/Write* methods (while keeping seek-only behavior for GetLength/SetLength)
  • Add SystemNative_ReadV/SystemNative_WriteV native exports with EAGAIN/EWOULDBLOCK poll-loop handling and GetAllowedVectorCount capping for IOV_MAX
  • Extract ShouldFallBackToNonOffsetSyscall helper for ENXIO/ESPIPE fallback logic, with consolidated control flow to avoid duplicate syscalls
  • Update XML doc comments with version-qualified behavior: "In .NET 11 and later versions, ..." for new non-seekable support, and restored NotSupportedException docs qualified with "In .NET 10 and earlier versions, ..." for backward compatibility

Console/Process call site updates

  • Replace Interop.Sys.Read/Write with RandomAccess.Read/Write in ConsolePal.Unix.cs, ConsolePal.Wasi.cs, and ConsolePal.Unix.ConsoleStream.cs (with EPIPE handling preserved)
  • Revert ConsolePal.Browser.cs changes (WASM does not support libSystem.Native dynamic linking)
  • Use foreach loop in UpdatedCachedCursorPosition instead of index-based iteration
  • Fix ProcessWaitingTests to use ReadBlock instead of Read to handle partial reads correctly

Tests

  • Add non-seekable handle tests for single/multi-buffer sync/async read/write, cancellation, and partial reads
  • Apply test improvements from PR Make RandomAccess.Read*|Write* methods work with non-seekable files #96711: AssertCanceled helper, AssertExtensions.SequenceEqual, break on read == 0
  • Fill pipe buffer before write cancellation test to avoid flaky sync completion
  • Merge PartialReads sync/async tests into a single [Theory]

Testing

  • All System.IO.FileSystem.Tests pass (9741 tests)
  • System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, default)
  • ProcessWaitingTests pass with ReadBlock fix
Original prompt

This section details on the original issue you should resolve

<issue_title>Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files</issue_title>
<issue_description>In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or separate PR, the Unix implementation of overloads that accept multiple buffers should start using readv and writev sys-calls. This should be relatively easy (just search for preadv and pwritev and reuse the patterns)

But it's going to allow to:

<agent_instructions>1. Do exactly what I did in #96711, you can even cherry-pick the git commits if you want.
2. For testing, in RandomAccess_NonSeekable.GetNamedPipeHandlesAsync don't use NamedPipeClientStream or NamedPipeServerStream just use the bran new SafeFileHandle.CreateAnonymousPipe(out readHandle, out writeHandle, bool asyncRead = false, bool asyncWrite = false) API.
3. Search the repo for places where we have SafeFileHandle and could use the RandomAccess API instead of performing direct sys-calls. Use the API there.
4. Make sure you run all the src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests test and they are passing before you send the PR.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ Tagging subscribers to this area: @dotnet/area-system-io See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
Issue Details

In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or...


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

CopilotAIand others added 3 commits March 12, 2026 22:49
…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>
…d of AnonymousPipeServerStream
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
… consistency
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
…ndling
- 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

All four review items addressed in commit 098316d. All 9741 System.IO.FileSystem.Tests pass.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception):

privatestaticunsafevoidWrite(SafeFileHandlefd,byte*bufPtr,intcount)
{
while(count>0)
{
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=Interop.Sys.GetLastErrorInfo();
if(errorInfo.Error==Interop.Error.EPIPE)
{
return;
}
else
{
throwInterop.GetIOException(errorInfo);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}

privatestaticunsafevoidWrite(SafeFileHandlefd,ReadOnlySpan<byte>buffer,boolmayChangeCursorPosition=true)
{
fixed (byte*p=buffer)
{
byte*bufPtr=p;
intcount=buffer.Length;
while(count>0)
{
intcursorVersion=mayChangeCursorPosition?Volatile.Read(refs_cursorVersion):-1;
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=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;
}
elseif(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,outInterop.PollEventstriggered);
continue;
}
else
{
// Something else... fail.
throwInterop.GetExceptionForIoErrno(errorInfo);
}
}
else
{
if(mayChangeCursorPosition)
{
UpdatedCachedCursorPosition(bufPtr,bytesWritten,cursorVersion);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}
}

use the RandomAccess.Read in following places:

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

If it does not break the build, remove related references from the project file (the list below is not complete, please verify it on your own):

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
…ack, 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception): ...

Done in commit a5d8cb2:

  • ConsolePal.Browser.cs: Replaced Interop.Sys.Write loop with RandomAccess.Write, EPIPE handled via catch (IOException) when (ConvertErrorPlatformToPal(ex.HResult) == EPIPE)
  • ConsolePal.Unix.cs: Read uses RandomAccess.Read, Write uses RandomAccess.Write with EPIPE catch and cursor tracking after the full write. UpdatedCachedCursorPosition now takes ReadOnlySpan<byte> instead of byte*.
  • ConsolePal.Wasi.cs: Both Read and Write use RandomAccess.Read/Write with EPIPE handling
  • Removed Interop.Read.cs, Interop.Write.cs, Interop.Poll.cs, Interop.Poll.Structs.cs from Browser, WASI, and Unix sections of System.Console.csproj

All 9741 System.IO.FileSystem.Tests pass, and System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, and the default).

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Console/src/System/ConsolePal.Unix.cs Outdated

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@stephentoub I believe I've addressed all your feedback, PTAL.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnik
adamsitnik enabled auto-merge (squash) March 22, 2026 15:11
@adamsitnik

Copy link
Copy Markdown
Member

/ba-g failures are unrelated (bad exit)

@adamsitnik
adamsitnik merged commit 9dd9f6f into mainMar 22, 2026
163 of 166 checks passed
@adamsitnik
adamsitnik deleted the copilot/relax-randomaccess-requirements branch March 22, 2026 19:31
eiriktsarpalis pushed a commit that referenced this pull request Mar 23, 2026
…th non-seekable files (#125512)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnikadamsitnik added this to the 11.0.0 milestone Mar 31, 2026
CopilotAI added a commit that referenced this pull request Apr 19, 2026
Reverts all source changes (ConsolePal.Unix.cs, ConsolePal.Wasi.cs,
ConsolePal.Unix.ConsoleStream.cs, System.Console.csproj) back to the
pre-#125512 state that uses Interop.Sys.Read/Write directly instead
of RandomAccess.Read/Write.
Only the new test methods (CanCopyStandardInputToStandardOutput,
UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent) and the
ConsoleHandles.cs csproj include are kept.
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ed523a85-0c0f-4eae-849f-f050c054fd96
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik added a commit that referenced this pull request Apr 20, 2026
…seekable file (#126844)
Reverts all System.Console source changes from PR #125512 (which
introduced `RandomAccess.Read`/`Write` based I/O) back to the original
`Interop.Sys.Read`/`Write` implementation. The `FileStream`-based
approach will be revisited separately.
Adds regression tests that verify `Console.OpenStandardInput().CopyTo()`
and `Console.OpenStandardOutput().Write()` work correctly when
stdin/stdout is redirected to a seekable file.
fixes#126843
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 1, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files

5 participants

@adamsitnik@stephentoub@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files - #125512

Merged
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements
Mar 22, 2026
Merged

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files#125512
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements

Conversation

CopilotAI commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Description

Relaxes System.IO.RandomAccess to support non-seekable handles (pipes, sockets, character devices) by falling back to non-offset syscalls when the file does not support seeking. Updates affected call sites in System.Console and System.Diagnostics.Process to use RandomAccess.Read/Write, and adds comprehensive tests for non-seekable handle scenarios.

Core changes

  • Update RandomAccess validation to allow non-seekable handles for Read*/Write* methods (while keeping seek-only behavior for GetLength/SetLength)
  • Add SystemNative_ReadV/SystemNative_WriteV native exports with EAGAIN/EWOULDBLOCK poll-loop handling and GetAllowedVectorCount capping for IOV_MAX
  • Extract ShouldFallBackToNonOffsetSyscall helper for ENXIO/ESPIPE fallback logic, with consolidated control flow to avoid duplicate syscalls
  • Update XML doc comments with version-qualified behavior: "In .NET 11 and later versions, ..." for new non-seekable support, and restored NotSupportedException docs qualified with "In .NET 10 and earlier versions, ..." for backward compatibility

Console/Process call site updates

  • Replace Interop.Sys.Read/Write with RandomAccess.Read/Write in ConsolePal.Unix.cs, ConsolePal.Wasi.cs, and ConsolePal.Unix.ConsoleStream.cs (with EPIPE handling preserved)
  • Revert ConsolePal.Browser.cs changes (WASM does not support libSystem.Native dynamic linking)
  • Use foreach loop in UpdatedCachedCursorPosition instead of index-based iteration
  • Fix ProcessWaitingTests to use ReadBlock instead of Read to handle partial reads correctly

Tests

  • Add non-seekable handle tests for single/multi-buffer sync/async read/write, cancellation, and partial reads
  • Apply test improvements from PR Make RandomAccess.Read*|Write* methods work with non-seekable files #96711: AssertCanceled helper, AssertExtensions.SequenceEqual, break on read == 0
  • Fill pipe buffer before write cancellation test to avoid flaky sync completion
  • Merge PartialReads sync/async tests into a single [Theory]

Testing

  • All System.IO.FileSystem.Tests pass (9741 tests)
  • System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, default)
  • ProcessWaitingTests pass with ReadBlock fix
Original prompt

This section details on the original issue you should resolve

<issue_title>Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files</issue_title>
<issue_description>In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or separate PR, the Unix implementation of overloads that accept multiple buffers should start using readv and writev sys-calls. This should be relatively easy (just search for preadv and pwritev and reuse the patterns)

But it's going to allow to:

<agent_instructions>1. Do exactly what I did in #96711, you can even cherry-pick the git commits if you want.
2. For testing, in RandomAccess_NonSeekable.GetNamedPipeHandlesAsync don't use NamedPipeClientStream or NamedPipeServerStream just use the bran new SafeFileHandle.CreateAnonymousPipe(out readHandle, out writeHandle, bool asyncRead = false, bool asyncWrite = false) API.
3. Search the repo for places where we have SafeFileHandle and could use the RandomAccess API instead of performing direct sys-calls. Use the API there.
4. Make sure you run all the src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests test and they are passing before you send the PR.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ Tagging subscribers to this area: @dotnet/area-system-io See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
Issue Details

In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or...


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

CopilotAIand others added 3 commits March 12, 2026 22:49
…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>
…d of AnonymousPipeServerStream
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
… consistency
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
…ndling
- 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

All four review items addressed in commit 098316d. All 9741 System.IO.FileSystem.Tests pass.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception):

privatestaticunsafevoidWrite(SafeFileHandlefd,byte*bufPtr,intcount)
{
while(count>0)
{
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=Interop.Sys.GetLastErrorInfo();
if(errorInfo.Error==Interop.Error.EPIPE)
{
return;
}
else
{
throwInterop.GetIOException(errorInfo);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}

privatestaticunsafevoidWrite(SafeFileHandlefd,ReadOnlySpan<byte>buffer,boolmayChangeCursorPosition=true)
{
fixed (byte*p=buffer)
{
byte*bufPtr=p;
intcount=buffer.Length;
while(count>0)
{
intcursorVersion=mayChangeCursorPosition?Volatile.Read(refs_cursorVersion):-1;
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=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;
}
elseif(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,outInterop.PollEventstriggered);
continue;
}
else
{
// Something else... fail.
throwInterop.GetExceptionForIoErrno(errorInfo);
}
}
else
{
if(mayChangeCursorPosition)
{
UpdatedCachedCursorPosition(bufPtr,bytesWritten,cursorVersion);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}
}

use the RandomAccess.Read in following places:

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

If it does not break the build, remove related references from the project file (the list below is not complete, please verify it on your own):

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
…ack, 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception): ...

Done in commit a5d8cb2:

  • ConsolePal.Browser.cs: Replaced Interop.Sys.Write loop with RandomAccess.Write, EPIPE handled via catch (IOException) when (ConvertErrorPlatformToPal(ex.HResult) == EPIPE)
  • ConsolePal.Unix.cs: Read uses RandomAccess.Read, Write uses RandomAccess.Write with EPIPE catch and cursor tracking after the full write. UpdatedCachedCursorPosition now takes ReadOnlySpan<byte> instead of byte*.
  • ConsolePal.Wasi.cs: Both Read and Write use RandomAccess.Read/Write with EPIPE handling
  • Removed Interop.Read.cs, Interop.Write.cs, Interop.Poll.cs, Interop.Poll.Structs.cs from Browser, WASI, and Unix sections of System.Console.csproj

All 9741 System.IO.FileSystem.Tests pass, and System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, and the default).

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Console/src/System/ConsolePal.Unix.cs Outdated

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@stephentoub I believe I've addressed all your feedback, PTAL.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnik
adamsitnik enabled auto-merge (squash) March 22, 2026 15:11
@adamsitnik

Copy link
Copy Markdown
Member

/ba-g failures are unrelated (bad exit)

@adamsitnik
adamsitnik merged commit 9dd9f6f into mainMar 22, 2026
163 of 166 checks passed
@adamsitnik
adamsitnik deleted the copilot/relax-randomaccess-requirements branch March 22, 2026 19:31
eiriktsarpalis pushed a commit that referenced this pull request Mar 23, 2026
…th non-seekable files (#125512)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnikadamsitnik added this to the 11.0.0 milestone Mar 31, 2026
CopilotAI added a commit that referenced this pull request Apr 19, 2026
Reverts all source changes (ConsolePal.Unix.cs, ConsolePal.Wasi.cs,
ConsolePal.Unix.ConsoleStream.cs, System.Console.csproj) back to the
pre-#125512 state that uses Interop.Sys.Read/Write directly instead
of RandomAccess.Read/Write.
Only the new test methods (CanCopyStandardInputToStandardOutput,
UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent) and the
ConsoleHandles.cs csproj include are kept.
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ed523a85-0c0f-4eae-849f-f050c054fd96
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik added a commit that referenced this pull request Apr 20, 2026
…seekable file (#126844)
Reverts all System.Console source changes from PR #125512 (which
introduced `RandomAccess.Read`/`Write` based I/O) back to the original
`Interop.Sys.Read`/`Write` implementation. The `FileStream`-based
approach will be revisited separately.
Adds regression tests that verify `Console.OpenStandardInput().CopyTo()`
and `Console.OpenStandardOutput().Write()` work correctly when
stdin/stdout is redirected to a seekable file.
fixes#126843
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 1, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files

5 participants

@adamsitnik@stephentoub@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files - #125512

Merged
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements
Mar 22, 2026
Merged

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files#125512
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements

Conversation

CopilotAI commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Description

Relaxes System.IO.RandomAccess to support non-seekable handles (pipes, sockets, character devices) by falling back to non-offset syscalls when the file does not support seeking. Updates affected call sites in System.Console and System.Diagnostics.Process to use RandomAccess.Read/Write, and adds comprehensive tests for non-seekable handle scenarios.

Core changes

  • Update RandomAccess validation to allow non-seekable handles for Read*/Write* methods (while keeping seek-only behavior for GetLength/SetLength)
  • Add SystemNative_ReadV/SystemNative_WriteV native exports with EAGAIN/EWOULDBLOCK poll-loop handling and GetAllowedVectorCount capping for IOV_MAX
  • Extract ShouldFallBackToNonOffsetSyscall helper for ENXIO/ESPIPE fallback logic, with consolidated control flow to avoid duplicate syscalls
  • Update XML doc comments with version-qualified behavior: "In .NET 11 and later versions, ..." for new non-seekable support, and restored NotSupportedException docs qualified with "In .NET 10 and earlier versions, ..." for backward compatibility

Console/Process call site updates

  • Replace Interop.Sys.Read/Write with RandomAccess.Read/Write in ConsolePal.Unix.cs, ConsolePal.Wasi.cs, and ConsolePal.Unix.ConsoleStream.cs (with EPIPE handling preserved)
  • Revert ConsolePal.Browser.cs changes (WASM does not support libSystem.Native dynamic linking)
  • Use foreach loop in UpdatedCachedCursorPosition instead of index-based iteration
  • Fix ProcessWaitingTests to use ReadBlock instead of Read to handle partial reads correctly

Tests

  • Add non-seekable handle tests for single/multi-buffer sync/async read/write, cancellation, and partial reads
  • Apply test improvements from PR Make RandomAccess.Read*|Write* methods work with non-seekable files #96711: AssertCanceled helper, AssertExtensions.SequenceEqual, break on read == 0
  • Fill pipe buffer before write cancellation test to avoid flaky sync completion
  • Merge PartialReads sync/async tests into a single [Theory]

Testing

  • All System.IO.FileSystem.Tests pass (9741 tests)
  • System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, default)
  • ProcessWaitingTests pass with ReadBlock fix
Original prompt

This section details on the original issue you should resolve

<issue_title>Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files</issue_title>
<issue_description>In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or separate PR, the Unix implementation of overloads that accept multiple buffers should start using readv and writev sys-calls. This should be relatively easy (just search for preadv and pwritev and reuse the patterns)

But it's going to allow to:

<agent_instructions>1. Do exactly what I did in #96711, you can even cherry-pick the git commits if you want.
2. For testing, in RandomAccess_NonSeekable.GetNamedPipeHandlesAsync don't use NamedPipeClientStream or NamedPipeServerStream just use the bran new SafeFileHandle.CreateAnonymousPipe(out readHandle, out writeHandle, bool asyncRead = false, bool asyncWrite = false) API.
3. Search the repo for places where we have SafeFileHandle and could use the RandomAccess API instead of performing direct sys-calls. Use the API there.
4. Make sure you run all the src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests test and they are passing before you send the PR.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ Tagging subscribers to this area: @dotnet/area-system-io See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
Issue Details

In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or...


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

CopilotAIand others added 3 commits March 12, 2026 22:49
…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>
…d of AnonymousPipeServerStream
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
… consistency
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
…ndling
- 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

All four review items addressed in commit 098316d. All 9741 System.IO.FileSystem.Tests pass.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception):

privatestaticunsafevoidWrite(SafeFileHandlefd,byte*bufPtr,intcount)
{
while(count>0)
{
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=Interop.Sys.GetLastErrorInfo();
if(errorInfo.Error==Interop.Error.EPIPE)
{
return;
}
else
{
throwInterop.GetIOException(errorInfo);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}

privatestaticunsafevoidWrite(SafeFileHandlefd,ReadOnlySpan<byte>buffer,boolmayChangeCursorPosition=true)
{
fixed (byte*p=buffer)
{
byte*bufPtr=p;
intcount=buffer.Length;
while(count>0)
{
intcursorVersion=mayChangeCursorPosition?Volatile.Read(refs_cursorVersion):-1;
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=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;
}
elseif(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,outInterop.PollEventstriggered);
continue;
}
else
{
// Something else... fail.
throwInterop.GetExceptionForIoErrno(errorInfo);
}
}
else
{
if(mayChangeCursorPosition)
{
UpdatedCachedCursorPosition(bufPtr,bytesWritten,cursorVersion);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}
}

use the RandomAccess.Read in following places:

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

If it does not break the build, remove related references from the project file (the list below is not complete, please verify it on your own):

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
…ack, 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception): ...

Done in commit a5d8cb2:

  • ConsolePal.Browser.cs: Replaced Interop.Sys.Write loop with RandomAccess.Write, EPIPE handled via catch (IOException) when (ConvertErrorPlatformToPal(ex.HResult) == EPIPE)
  • ConsolePal.Unix.cs: Read uses RandomAccess.Read, Write uses RandomAccess.Write with EPIPE catch and cursor tracking after the full write. UpdatedCachedCursorPosition now takes ReadOnlySpan<byte> instead of byte*.
  • ConsolePal.Wasi.cs: Both Read and Write use RandomAccess.Read/Write with EPIPE handling
  • Removed Interop.Read.cs, Interop.Write.cs, Interop.Poll.cs, Interop.Poll.Structs.cs from Browser, WASI, and Unix sections of System.Console.csproj

All 9741 System.IO.FileSystem.Tests pass, and System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, and the default).

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Console/src/System/ConsolePal.Unix.cs Outdated

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@stephentoub I believe I've addressed all your feedback, PTAL.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnik
adamsitnik enabled auto-merge (squash) March 22, 2026 15:11
@adamsitnik

Copy link
Copy Markdown
Member

/ba-g failures are unrelated (bad exit)

@adamsitnik
adamsitnik merged commit 9dd9f6f into mainMar 22, 2026
163 of 166 checks passed
@adamsitnik
adamsitnik deleted the copilot/relax-randomaccess-requirements branch March 22, 2026 19:31
eiriktsarpalis pushed a commit that referenced this pull request Mar 23, 2026
…th non-seekable files (#125512)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnikadamsitnik added this to the 11.0.0 milestone Mar 31, 2026
CopilotAI added a commit that referenced this pull request Apr 19, 2026
Reverts all source changes (ConsolePal.Unix.cs, ConsolePal.Wasi.cs,
ConsolePal.Unix.ConsoleStream.cs, System.Console.csproj) back to the
pre-#125512 state that uses Interop.Sys.Read/Write directly instead
of RandomAccess.Read/Write.
Only the new test methods (CanCopyStandardInputToStandardOutput,
UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent) and the
ConsoleHandles.cs csproj include are kept.
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ed523a85-0c0f-4eae-849f-f050c054fd96
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik added a commit that referenced this pull request Apr 20, 2026
…seekable file (#126844)
Reverts all System.Console source changes from PR #125512 (which
introduced `RandomAccess.Read`/`Write` based I/O) back to the original
`Interop.Sys.Read`/`Write` implementation. The `FileStream`-based
approach will be revisited separately.
Adds regression tests that verify `Console.OpenStandardInput().CopyTo()`
and `Console.OpenStandardOutput().Write()` work correctly when
stdin/stdout is redirected to a seekable file.
fixes#126843
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 1, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files

5 participants

@adamsitnik@stephentoub@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files - #125512

Merged
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements
Mar 22, 2026
Merged

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files#125512
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements

Conversation

CopilotAI commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Description

Relaxes System.IO.RandomAccess to support non-seekable handles (pipes, sockets, character devices) by falling back to non-offset syscalls when the file does not support seeking. Updates affected call sites in System.Console and System.Diagnostics.Process to use RandomAccess.Read/Write, and adds comprehensive tests for non-seekable handle scenarios.

Core changes

  • Update RandomAccess validation to allow non-seekable handles for Read*/Write* methods (while keeping seek-only behavior for GetLength/SetLength)
  • Add SystemNative_ReadV/SystemNative_WriteV native exports with EAGAIN/EWOULDBLOCK poll-loop handling and GetAllowedVectorCount capping for IOV_MAX
  • Extract ShouldFallBackToNonOffsetSyscall helper for ENXIO/ESPIPE fallback logic, with consolidated control flow to avoid duplicate syscalls
  • Update XML doc comments with version-qualified behavior: "In .NET 11 and later versions, ..." for new non-seekable support, and restored NotSupportedException docs qualified with "In .NET 10 and earlier versions, ..." for backward compatibility

Console/Process call site updates

  • Replace Interop.Sys.Read/Write with RandomAccess.Read/Write in ConsolePal.Unix.cs, ConsolePal.Wasi.cs, and ConsolePal.Unix.ConsoleStream.cs (with EPIPE handling preserved)
  • Revert ConsolePal.Browser.cs changes (WASM does not support libSystem.Native dynamic linking)
  • Use foreach loop in UpdatedCachedCursorPosition instead of index-based iteration
  • Fix ProcessWaitingTests to use ReadBlock instead of Read to handle partial reads correctly

Tests

  • Add non-seekable handle tests for single/multi-buffer sync/async read/write, cancellation, and partial reads
  • Apply test improvements from PR Make RandomAccess.Read*|Write* methods work with non-seekable files #96711: AssertCanceled helper, AssertExtensions.SequenceEqual, break on read == 0
  • Fill pipe buffer before write cancellation test to avoid flaky sync completion
  • Merge PartialReads sync/async tests into a single [Theory]

Testing

  • All System.IO.FileSystem.Tests pass (9741 tests)
  • System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, default)
  • ProcessWaitingTests pass with ReadBlock fix
Original prompt

This section details on the original issue you should resolve

<issue_title>Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files</issue_title>
<issue_description>In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or separate PR, the Unix implementation of overloads that accept multiple buffers should start using readv and writev sys-calls. This should be relatively easy (just search for preadv and pwritev and reuse the patterns)

But it's going to allow to:

<agent_instructions>1. Do exactly what I did in #96711, you can even cherry-pick the git commits if you want.
2. For testing, in RandomAccess_NonSeekable.GetNamedPipeHandlesAsync don't use NamedPipeClientStream or NamedPipeServerStream just use the bran new SafeFileHandle.CreateAnonymousPipe(out readHandle, out writeHandle, bool asyncRead = false, bool asyncWrite = false) API.
3. Search the repo for places where we have SafeFileHandle and could use the RandomAccess API instead of performing direct sys-calls. Use the API there.
4. Make sure you run all the src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests test and they are passing before you send the PR.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ Tagging subscribers to this area: @dotnet/area-system-io See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
Issue Details

In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or...


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

CopilotAIand others added 3 commits March 12, 2026 22:49
…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>
…d of AnonymousPipeServerStream
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
… consistency
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
…ndling
- 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

All four review items addressed in commit 098316d. All 9741 System.IO.FileSystem.Tests pass.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception):

privatestaticunsafevoidWrite(SafeFileHandlefd,byte*bufPtr,intcount)
{
while(count>0)
{
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=Interop.Sys.GetLastErrorInfo();
if(errorInfo.Error==Interop.Error.EPIPE)
{
return;
}
else
{
throwInterop.GetIOException(errorInfo);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}

privatestaticunsafevoidWrite(SafeFileHandlefd,ReadOnlySpan<byte>buffer,boolmayChangeCursorPosition=true)
{
fixed (byte*p=buffer)
{
byte*bufPtr=p;
intcount=buffer.Length;
while(count>0)
{
intcursorVersion=mayChangeCursorPosition?Volatile.Read(refs_cursorVersion):-1;
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=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;
}
elseif(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,outInterop.PollEventstriggered);
continue;
}
else
{
// Something else... fail.
throwInterop.GetExceptionForIoErrno(errorInfo);
}
}
else
{
if(mayChangeCursorPosition)
{
UpdatedCachedCursorPosition(bufPtr,bytesWritten,cursorVersion);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}
}

use the RandomAccess.Read in following places:

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

If it does not break the build, remove related references from the project file (the list below is not complete, please verify it on your own):

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
…ack, 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception): ...

Done in commit a5d8cb2:

  • ConsolePal.Browser.cs: Replaced Interop.Sys.Write loop with RandomAccess.Write, EPIPE handled via catch (IOException) when (ConvertErrorPlatformToPal(ex.HResult) == EPIPE)
  • ConsolePal.Unix.cs: Read uses RandomAccess.Read, Write uses RandomAccess.Write with EPIPE catch and cursor tracking after the full write. UpdatedCachedCursorPosition now takes ReadOnlySpan<byte> instead of byte*.
  • ConsolePal.Wasi.cs: Both Read and Write use RandomAccess.Read/Write with EPIPE handling
  • Removed Interop.Read.cs, Interop.Write.cs, Interop.Poll.cs, Interop.Poll.Structs.cs from Browser, WASI, and Unix sections of System.Console.csproj

All 9741 System.IO.FileSystem.Tests pass, and System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, and the default).

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Console/src/System/ConsolePal.Unix.cs Outdated

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@stephentoub I believe I've addressed all your feedback, PTAL.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnik
adamsitnik enabled auto-merge (squash) March 22, 2026 15:11
@adamsitnik

Copy link
Copy Markdown
Member

/ba-g failures are unrelated (bad exit)

@adamsitnik
adamsitnik merged commit 9dd9f6f into mainMar 22, 2026
163 of 166 checks passed
@adamsitnik
adamsitnik deleted the copilot/relax-randomaccess-requirements branch March 22, 2026 19:31
eiriktsarpalis pushed a commit that referenced this pull request Mar 23, 2026
…th non-seekable files (#125512)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnikadamsitnik added this to the 11.0.0 milestone Mar 31, 2026
CopilotAI added a commit that referenced this pull request Apr 19, 2026
Reverts all source changes (ConsolePal.Unix.cs, ConsolePal.Wasi.cs,
ConsolePal.Unix.ConsoleStream.cs, System.Console.csproj) back to the
pre-#125512 state that uses Interop.Sys.Read/Write directly instead
of RandomAccess.Read/Write.
Only the new test methods (CanCopyStandardInputToStandardOutput,
UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent) and the
ConsoleHandles.cs csproj include are kept.
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ed523a85-0c0f-4eae-849f-f050c054fd96
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik added a commit that referenced this pull request Apr 20, 2026
…seekable file (#126844)
Reverts all System.Console source changes from PR #125512 (which
introduced `RandomAccess.Read`/`Write` based I/O) back to the original
`Interop.Sys.Read`/`Write` implementation. The `FileStream`-based
approach will be revisited separately.
Adds regression tests that verify `Console.OpenStandardInput().CopyTo()`
and `Console.OpenStandardOutput().Write()` work correctly when
stdin/stdout is redirected to a seekable file.
fixes#126843
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 1, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files

5 participants

@adamsitnik@stephentoub@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files - #125512

Merged
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements
Mar 22, 2026
Merged

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files#125512
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements

Conversation

CopilotAI commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Description

Relaxes System.IO.RandomAccess to support non-seekable handles (pipes, sockets, character devices) by falling back to non-offset syscalls when the file does not support seeking. Updates affected call sites in System.Console and System.Diagnostics.Process to use RandomAccess.Read/Write, and adds comprehensive tests for non-seekable handle scenarios.

Core changes

  • Update RandomAccess validation to allow non-seekable handles for Read*/Write* methods (while keeping seek-only behavior for GetLength/SetLength)
  • Add SystemNative_ReadV/SystemNative_WriteV native exports with EAGAIN/EWOULDBLOCK poll-loop handling and GetAllowedVectorCount capping for IOV_MAX
  • Extract ShouldFallBackToNonOffsetSyscall helper for ENXIO/ESPIPE fallback logic, with consolidated control flow to avoid duplicate syscalls
  • Update XML doc comments with version-qualified behavior: "In .NET 11 and later versions, ..." for new non-seekable support, and restored NotSupportedException docs qualified with "In .NET 10 and earlier versions, ..." for backward compatibility

Console/Process call site updates

  • Replace Interop.Sys.Read/Write with RandomAccess.Read/Write in ConsolePal.Unix.cs, ConsolePal.Wasi.cs, and ConsolePal.Unix.ConsoleStream.cs (with EPIPE handling preserved)
  • Revert ConsolePal.Browser.cs changes (WASM does not support libSystem.Native dynamic linking)
  • Use foreach loop in UpdatedCachedCursorPosition instead of index-based iteration
  • Fix ProcessWaitingTests to use ReadBlock instead of Read to handle partial reads correctly

Tests

  • Add non-seekable handle tests for single/multi-buffer sync/async read/write, cancellation, and partial reads
  • Apply test improvements from PR Make RandomAccess.Read*|Write* methods work with non-seekable files #96711: AssertCanceled helper, AssertExtensions.SequenceEqual, break on read == 0
  • Fill pipe buffer before write cancellation test to avoid flaky sync completion
  • Merge PartialReads sync/async tests into a single [Theory]

Testing

  • All System.IO.FileSystem.Tests pass (9741 tests)
  • System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, default)
  • ProcessWaitingTests pass with ReadBlock fix
Original prompt

This section details on the original issue you should resolve

<issue_title>Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files</issue_title>
<issue_description>In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or separate PR, the Unix implementation of overloads that accept multiple buffers should start using readv and writev sys-calls. This should be relatively easy (just search for preadv and pwritev and reuse the patterns)

But it's going to allow to:

<agent_instructions>1. Do exactly what I did in #96711, you can even cherry-pick the git commits if you want.
2. For testing, in RandomAccess_NonSeekable.GetNamedPipeHandlesAsync don't use NamedPipeClientStream or NamedPipeServerStream just use the bran new SafeFileHandle.CreateAnonymousPipe(out readHandle, out writeHandle, bool asyncRead = false, bool asyncWrite = false) API.
3. Search the repo for places where we have SafeFileHandle and could use the RandomAccess API instead of performing direct sys-calls. Use the API there.
4. Make sure you run all the src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests test and they are passing before you send the PR.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ Tagging subscribers to this area: @dotnet/area-system-io See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
Issue Details

In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or...


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

CopilotAIand others added 3 commits March 12, 2026 22:49
…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>
…d of AnonymousPipeServerStream
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
… consistency
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
…ndling
- 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

All four review items addressed in commit 098316d. All 9741 System.IO.FileSystem.Tests pass.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception):

privatestaticunsafevoidWrite(SafeFileHandlefd,byte*bufPtr,intcount)
{
while(count>0)
{
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=Interop.Sys.GetLastErrorInfo();
if(errorInfo.Error==Interop.Error.EPIPE)
{
return;
}
else
{
throwInterop.GetIOException(errorInfo);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}

privatestaticunsafevoidWrite(SafeFileHandlefd,ReadOnlySpan<byte>buffer,boolmayChangeCursorPosition=true)
{
fixed (byte*p=buffer)
{
byte*bufPtr=p;
intcount=buffer.Length;
while(count>0)
{
intcursorVersion=mayChangeCursorPosition?Volatile.Read(refs_cursorVersion):-1;
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=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;
}
elseif(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,outInterop.PollEventstriggered);
continue;
}
else
{
// Something else... fail.
throwInterop.GetExceptionForIoErrno(errorInfo);
}
}
else
{
if(mayChangeCursorPosition)
{
UpdatedCachedCursorPosition(bufPtr,bytesWritten,cursorVersion);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}
}

use the RandomAccess.Read in following places:

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

If it does not break the build, remove related references from the project file (the list below is not complete, please verify it on your own):

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
…ack, 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception): ...

Done in commit a5d8cb2:

  • ConsolePal.Browser.cs: Replaced Interop.Sys.Write loop with RandomAccess.Write, EPIPE handled via catch (IOException) when (ConvertErrorPlatformToPal(ex.HResult) == EPIPE)
  • ConsolePal.Unix.cs: Read uses RandomAccess.Read, Write uses RandomAccess.Write with EPIPE catch and cursor tracking after the full write. UpdatedCachedCursorPosition now takes ReadOnlySpan<byte> instead of byte*.
  • ConsolePal.Wasi.cs: Both Read and Write use RandomAccess.Read/Write with EPIPE handling
  • Removed Interop.Read.cs, Interop.Write.cs, Interop.Poll.cs, Interop.Poll.Structs.cs from Browser, WASI, and Unix sections of System.Console.csproj

All 9741 System.IO.FileSystem.Tests pass, and System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, and the default).

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Console/src/System/ConsolePal.Unix.cs Outdated

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@stephentoub I believe I've addressed all your feedback, PTAL.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnik
adamsitnik enabled auto-merge (squash) March 22, 2026 15:11
@adamsitnik

Copy link
Copy Markdown
Member

/ba-g failures are unrelated (bad exit)

@adamsitnik
adamsitnik merged commit 9dd9f6f into mainMar 22, 2026
163 of 166 checks passed
@adamsitnik
adamsitnik deleted the copilot/relax-randomaccess-requirements branch March 22, 2026 19:31
eiriktsarpalis pushed a commit that referenced this pull request Mar 23, 2026
…th non-seekable files (#125512)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnikadamsitnik added this to the 11.0.0 milestone Mar 31, 2026
CopilotAI added a commit that referenced this pull request Apr 19, 2026
Reverts all source changes (ConsolePal.Unix.cs, ConsolePal.Wasi.cs,
ConsolePal.Unix.ConsoleStream.cs, System.Console.csproj) back to the
pre-#125512 state that uses Interop.Sys.Read/Write directly instead
of RandomAccess.Read/Write.
Only the new test methods (CanCopyStandardInputToStandardOutput,
UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent) and the
ConsoleHandles.cs csproj include are kept.
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ed523a85-0c0f-4eae-849f-f050c054fd96
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik added a commit that referenced this pull request Apr 20, 2026
…seekable file (#126844)
Reverts all System.Console source changes from PR #125512 (which
introduced `RandomAccess.Read`/`Write` based I/O) back to the original
`Interop.Sys.Read`/`Write` implementation. The `FileStream`-based
approach will be revisited separately.
Adds regression tests that verify `Console.OpenStandardInput().CopyTo()`
and `Console.OpenStandardOutput().Write()` work correctly when
stdin/stdout is redirected to a seekable file.
fixes#126843
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 1, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files

5 participants

@adamsitnik@stephentoub@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files - #125512

Merged
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements
Mar 22, 2026
Merged

Relax RandomAccess type requirements: make Read/Write methods work with non-seekable files#125512
adamsitnik merged 23 commits into
mainfrom
copilot/relax-randomaccess-requirements

Conversation

CopilotAI commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Description

Relaxes System.IO.RandomAccess to support non-seekable handles (pipes, sockets, character devices) by falling back to non-offset syscalls when the file does not support seeking. Updates affected call sites in System.Console and System.Diagnostics.Process to use RandomAccess.Read/Write, and adds comprehensive tests for non-seekable handle scenarios.

Core changes

  • Update RandomAccess validation to allow non-seekable handles for Read*/Write* methods (while keeping seek-only behavior for GetLength/SetLength)
  • Add SystemNative_ReadV/SystemNative_WriteV native exports with EAGAIN/EWOULDBLOCK poll-loop handling and GetAllowedVectorCount capping for IOV_MAX
  • Extract ShouldFallBackToNonOffsetSyscall helper for ENXIO/ESPIPE fallback logic, with consolidated control flow to avoid duplicate syscalls
  • Update XML doc comments with version-qualified behavior: "In .NET 11 and later versions, ..." for new non-seekable support, and restored NotSupportedException docs qualified with "In .NET 10 and earlier versions, ..." for backward compatibility

Console/Process call site updates

  • Replace Interop.Sys.Read/Write with RandomAccess.Read/Write in ConsolePal.Unix.cs, ConsolePal.Wasi.cs, and ConsolePal.Unix.ConsoleStream.cs (with EPIPE handling preserved)
  • Revert ConsolePal.Browser.cs changes (WASM does not support libSystem.Native dynamic linking)
  • Use foreach loop in UpdatedCachedCursorPosition instead of index-based iteration
  • Fix ProcessWaitingTests to use ReadBlock instead of Read to handle partial reads correctly

Tests

  • Add non-seekable handle tests for single/multi-buffer sync/async read/write, cancellation, and partial reads
  • Apply test improvements from PR Make RandomAccess.Read*|Write* methods work with non-seekable files #96711: AssertCanceled helper, AssertExtensions.SequenceEqual, break on read == 0
  • Fill pipe buffer before write cancellation test to avoid flaky sync completion
  • Merge PartialReads sync/async tests into a single [Theory]

Testing

  • All System.IO.FileSystem.Tests pass (9741 tests)
  • System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, default)
  • ProcessWaitingTests pass with ReadBlock fix
Original prompt

This section details on the original issue you should resolve

<issue_title>Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files</issue_title>
<issue_description>In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or separate PR, the Unix implementation of overloads that accept multiple buffers should start using readv and writev sys-calls. This should be relatively easy (just search for preadv and pwritev and reuse the patterns)

But it's going to allow to:

<agent_instructions>1. Do exactly what I did in #96711, you can even cherry-pick the git commits if you want.
2. For testing, in RandomAccess_NonSeekable.GetNamedPipeHandlesAsync don't use NamedPipeClientStream or NamedPipeServerStream just use the bran new SafeFileHandle.CreateAnonymousPipe(out readHandle, out writeHandle, bool asyncRead = false, bool asyncWrite = false) API.
3. Search the repo for places where we have SafeFileHandle and could use the RandomAccess API instead of performing direct sys-calls. Use the API there.
4. Make sure you run all the src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests test and they are passing before you send the PR.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ Tagging subscribers to this area: @dotnet/area-system-io See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
Issue Details

In .NET 6 we have introduced new type called RandomAccess that allows for reading and writing to specific file offset.

As of today, all it's Read* and Write* methods throw when given handle points to a non-seekable file like socket or pipe:

ThrowHelper.ThrowNotSupportedException_UnseekableStream();

But it's not a problem for it's internal implementation (used by FileStream):

// 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.
intresult=handle.CanSeek?
Interop.Sys.PRead(handle,bufPtr,buffer.Length,fileOffset):
Interop.Sys.Read(handle,bufPtr,buffer.Length);

And we use it's internal API surface to workaround this limitation is CoreLib (mind the call to RandomAccess.WriteAtOffset (internal), not RandomAccess.Write (public):

RandomAccess.WriteAtOffset(sfh,bytes,0);

We should relax the public requirements and make RandomAccess work with non-seekable files.

This is going to require:

  • a breaking change doc
  • implementation change (stop throwing exception)
  • test changes (this test shows how to easily create a handle that points to non-seekable pipe)

Everything motioned above should be a single PR. In the same or...


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

CopilotAIand others added 3 commits March 12, 2026 22:49
…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>
…d of AnonymousPipeServerStream
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
… consistency
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
Comment threadsrc/native/libs/System.Native/pal_io.c Outdated
…ndling
- 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

All four review items addressed in commit 098316d. All 9741 System.IO.FileSystem.Tests pass.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception):

privatestaticunsafevoidWrite(SafeFileHandlefd,byte*bufPtr,intcount)
{
while(count>0)
{
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=Interop.Sys.GetLastErrorInfo();
if(errorInfo.Error==Interop.Error.EPIPE)
{
return;
}
else
{
throwInterop.GetIOException(errorInfo);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}

privatestaticunsafevoidWrite(SafeFileHandlefd,ReadOnlySpan<byte>buffer,boolmayChangeCursorPosition=true)
{
fixed (byte*p=buffer)
{
byte*bufPtr=p;
intcount=buffer.Length;
while(count>0)
{
intcursorVersion=mayChangeCursorPosition?Volatile.Read(refs_cursorVersion):-1;
intbytesWritten=Interop.Sys.Write(fd,bufPtr,count);
if(bytesWritten<0)
{
Interop.ErrorInfoerrorInfo=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;
}
elseif(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,outInterop.PollEventstriggered);
continue;
}
else
{
// Something else... fail.
throwInterop.GetExceptionForIoErrno(errorInfo);
}
}
else
{
if(mayChangeCursorPosition)
{
UpdatedCachedCursorPosition(bufPtr,bytesWritten,cursorVersion);
}
}
count-=bytesWritten;
bufPtr+=bytesWritten;
}
}
}

use the RandomAccess.Read in following places:

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

privatestaticunsafeintRead(SafeFileHandlefd,Span<byte>buffer)
{
fixed (byte*bufPtr=buffer)
{
intresult=Interop.CheckIo(Interop.Sys.Read(fd,bufPtr,buffer.Length));
Debug.Assert(result<=buffer.Length);
returnresult;
}
}

If it does not break the build, remove related references from the project file (the list below is not complete, please verify it on your own):

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />

<CompileInclude="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<CompileInclude="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
…ack, 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>

CopilotAI commented Mar 13, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please address my feedback

@copilot use the RandomAccess.Write in following places (respect the current EPIPE handling by swallowing specific exception): ...

Done in commit a5d8cb2:

  • ConsolePal.Browser.cs: Replaced Interop.Sys.Write loop with RandomAccess.Write, EPIPE handled via catch (IOException) when (ConvertErrorPlatformToPal(ex.HResult) == EPIPE)
  • ConsolePal.Unix.cs: Read uses RandomAccess.Read, Write uses RandomAccess.Write with EPIPE catch and cursor tracking after the full write. UpdatedCachedCursorPosition now takes ReadOnlySpan<byte> instead of byte*.
  • ConsolePal.Wasi.cs: Both Read and Write use RandomAccess.Read/Write with EPIPE handling
  • Removed Interop.Read.cs, Interop.Write.cs, Interop.Poll.cs, Interop.Poll.Structs.cs from Browser, WASI, and Unix sections of System.Console.csproj

All 9741 System.IO.FileSystem.Tests pass, and System.Console builds for all 8 targets (unix, windows, browser, wasi, android, ios, tvos, and the default).

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot please address my feedback

Comment threadsrc/libraries/System.Console/src/System/ConsolePal.Unix.cs Outdated

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@stephentoub I believe I've addressed all your feedback, PTAL.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs Outdated
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnik
adamsitnik enabled auto-merge (squash) March 22, 2026 15:11
@adamsitnik

Copy link
Copy Markdown
Member

/ba-g failures are unrelated (bad exit)

@adamsitnik
adamsitnik merged commit 9dd9f6f into mainMar 22, 2026
163 of 166 checks passed
@adamsitnik
adamsitnik deleted the copilot/relax-randomaccess-requirements branch March 22, 2026 19:31
eiriktsarpalis pushed a commit that referenced this pull request Mar 23, 2026
…th non-seekable files (#125512)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@adamsitnikadamsitnik added this to the 11.0.0 milestone Mar 31, 2026
CopilotAI added a commit that referenced this pull request Apr 19, 2026
Reverts all source changes (ConsolePal.Unix.cs, ConsolePal.Wasi.cs,
ConsolePal.Unix.ConsoleStream.cs, System.Console.csproj) back to the
pre-#125512 state that uses Interop.Sys.Read/Write directly instead
of RandomAccess.Read/Write.
Only the new test methods (CanCopyStandardInputToStandardOutput,
UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent) and the
ConsoleHandles.cs csproj include are kept.
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ed523a85-0c0f-4eae-849f-f050c054fd96
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik added a commit that referenced this pull request Apr 20, 2026
…seekable file (#126844)
Reverts all System.Console source changes from PR #125512 (which
introduced `RandomAccess.Read`/`Write` based I/O) back to the original
`Interop.Sys.Read`/`Write` implementation. The `FileStream`-based
approach will be revisited separately.
Adds regression tests that verify `Console.OpenStandardInput().CopyTo()`
and `Console.OpenStandardOutput().Write()` work correctly when
stdin/stdout is redirected to a seekable file.
fixes#126843
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 1, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relax RandomAccess type requirements, make all Read*|Write* methods work with non-seekable files

5 participants

@adamsitnik@stephentoub@jkotas