Fix UnixConsoleStream when stdin/stdout is redirected to a seekable file#126844
Fix UnixConsoleStream when stdin/stdout is redirected to a seekable file#126844
Conversation
|
Tagging subscribers to this area: @dotnet/area-system-io |
RandomAccess.Read/Write use pread/pwrite syscalls which always operate at a specified fileOffset. Passing fileOffset:0 for seekable files (e.g., regular files redirected as stdin/stdout) caused reads/writes to always operate at position 0, resulting in infinite loops or incorrect output. The fix creates a FileStream (with bufferSize:0) wrapping the handle when the handle is seekable. FileStream uses OSFileStreamStrategy which tracks the file position and advances it after each read/write, ensuring correct sequential behavior for seekable files. For non-seekable files (pipes, terminals), the existing RandomAccess-based path is used, preserving cursor position tracking for terminals. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/0df60225-7783-4496-abcf-9917387f56e5 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
…g duplication - Replace 'float for GC' FileStream approach with explicit _useFileStreamForIo bool field; FileStream is always held and disposed deterministically - Fix string duplication in stdout test by passing outputContent as RemoteExecutor arg - Add try/catch around File.Delete in finally blocks Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/0df60225-7783-4496-abcf-9917387f56e5 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot please address my feedback
src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs
Outdated
Show resolved
Hide resolved
| const string inputContent = "Hello from seekable stdin!"; | ||
| string testFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); | ||
| try | ||
| { | ||
| File.WriteAllText(testFilePath, inputContent, Encoding.UTF8); | ||
|
|
There was a problem hiding this comment.
| const string inputContent = "Hello from seekable stdin!"; | |
| string testFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); | |
| try | |
| { | |
| File.WriteAllText(testFilePath, inputContent, Encoding.UTF8); | |
| const string inputContent = "Hello from seekable stdin!"; | |
| string testFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); | |
| File.WriteAllText(testFilePath, inputContent, Encoding.UTF8); | |
| try | |
| { |
src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs
Outdated
Show resolved
Hide resolved
…onsoleStream, simplify tests - Remove _useFileStreamForIo field and extra comments; always use FileStream for I/O - Change Write() in UnixConsoleStream to call WriteFromConsoleStream(_fileStream, buffer) - Update WriteFromConsoleStream in ConsolePal.Unix.cs and ConsolePal.Wasi.cs to accept FileStream instead of SafeFileHandle; use fs.Write(buffer) which handles both seekable and non-seekable files correctly - Rename SeekableStdinRedirection test to CanCopyStandardInputToStandardOutput and run on all non-mobile platforms (not just AnyUnix); simplify via RemoteInvokeOptions.StartInfo - Simplify SeekableStdoutRedirection test similarly; remove boilerplate try/catch Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/67a83dee-25ae-47d1-ae0f-b4aa1f18265f Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Done in adb2b69. All review feedback addressed:
|
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot address my feedback
src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs
Outdated
Show resolved
Hide resolved
- Fix XML comment in ConsolePal.Wasi.cs Write(FileStream): fd → fs - Remove _handle field from UnixConsoleStream; FileStream owns handle lifetime - Remove Flush() override from UnixConsoleStream (base is a nop; bufferSize:0) - Remove redundant [PlatformSpecific] from two new tests (RemoteExecutor handles it) - Dispose stdoutHandle before WaitForExit so File.ReadAllText can open the file Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ba3bd32e-023a-4160-bf27-f0ae05027d8d Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Done in e5c1365. All feedback addressed:
|
There was a problem hiding this comment.
@copilot this method needs to be removed, as calling RandomAccess.Write(fd, buffer, fileOffset: 0); for seekable files will introduce a bug we are trying to fix
There was a problem hiding this comment.
Done in 49df75d. Write(SafeFileHandle, ...) is removed. WriteToTerminal now uses Write(FileStream, ...) — via a cached s_terminalFileStream for the default terminal handle, and a temporary FileStream (wrapping a freshly-obtained non-owning handle) for the WriteTerminalAnsiColorString path.
…l writes - Add s_terminalFileStream cached field alongside s_terminalHandle - Initialize s_terminalFileStream in EnsureInitializedCore when s_terminalHandle is set - Update WriteToTerminal to use Write(FileStream,...) — cached s_terminalFileStream for the default terminal, temporary FileStream for explicit non-owning handles - Remove Write(SafeFileHandle, ...) which used RandomAccess.Write(fd, offset:0), a buggy pattern for seekable files (now fully eliminated from the codebase) Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/6de74f54-eefb-49ea-88ce-3ffa1c3628e8 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
| else | ||
| { | ||
| // handle is always a freshly-obtained non-owning handle; safe to wrap in a FileStream and dispose. | ||
| using FileStream fs = new FileStream(handle, FileAccess.Write, bufferSize: 0); |
There was a problem hiding this comment.
this is going to close the handle..
Description
UnixConsoleStreamusedRandomAccess.Read/WritewithfileOffset: 0, which internally callspread/pwrite— syscalls that operate at a fixed offset and never advance the file position. For seekable handles (regular files), this means every read restarts at byte 0, causing infinite loops onCopyTo, and every write overwrites from the beginning.Root Cause
RandomAccess.ReadAtOffsetusespreadwhenhandle.SupportsRandomAccessis true (i.e., the handle is seekable). PassingfileOffset: 0always reads the same bytes. Same issue applies to writes.Fix
UnixConsoleStream, wrap the handle in aFileStream(handle, access, bufferSize: 0)and use it unconditionally for all reads and writes.FileStreamcorrectly handles both seekable files (tracking and advancing_filePosition) and non-seekable files (pipes, terminals), so no conditional branching is needed.FileStreamalso owns the handle lifetime, so no separate_handlefield is required.Write()inUnixConsoleStreamdelegates toConsolePal.WriteFromConsoleStream(_fileStream, buffer).WriteFromConsoleStreaminConsolePal.Unix.csandConsolePal.Wasi.csupdated to acceptFileStreaminstead ofSafeFileHandle, callingfs.Write(buffer)which handles both seekable and non-seekable cases correctly, while preserving EPIPE handling and terminal cursor-position tracking.Flush()override has been removed fromUnixConsoleStream— the base implementation is a no-op andbufferSize: 0means no buffering.Write(SafeFileHandle fd, ...)overload inConsolePal.Unix.cs(which usedRandomAccess.Write(fd, buffer, fileOffset: 0)) has been removed entirely.WriteToTerminalnow uses a cacheds_terminalFileStream(wrappings_terminalHandle) for the default terminal, and a temporary non-owningFileStreamfor explicit handle calls, both going throughWrite(FileStream, ...).Tests
Added two regression tests to
ConsoleHandles.cs(run on all platforms supported byRemoteExecutor, including Windows):CanCopyStandardInputToStandardOutput— verifies stdin redirected to a seekable file is fully consumed (process exits, no infinite loop).UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent— verifies stdout redirected to a seekable file contains the complete expected output. The parent's copy ofstdoutHandleis disposed before reading the file to ensureFile.ReadAllTextcan open it on all platforms.ConsoleHandles.cswas not previously included inSystem.Console.Tests.csproj— this is now fixed.Changes
src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs— core fix: always useFileStreamfor all I/O; removed_handlefield andFlush()overridesrc/libraries/System.Console/src/System/ConsolePal.Unix.cs—WriteFromConsoleStreamupdated to acceptFileStream;Write(SafeFileHandle, ...)removed;WriteToTerminalupdated to use cacheds_terminalFileStream; news_terminalFileStreamfield addedsrc/libraries/System.Console/src/System/ConsolePal.Wasi.cs—WriteFromConsoleStreamupdated to acceptFileStream; fixed XML doc commentsrc/libraries/System.Console/tests/ConsoleHandles.cs— regression testssrc/libraries/System.Console/tests/System.Console.Tests.csproj— includeConsoleHandles.cs