From f37fc587f99b9ba66f8cc3e47f88fae7ede8dd75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:27:05 +0000 Subject: [PATCH 01/10] Initial plan From fb33b419d2610a86eab71b649fc8becee3349d50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:15:07 +0000 Subject: [PATCH 02/10] Fix UnixConsoleStream to handle seekable files correctly 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> --- .../System/ConsolePal.Unix.ConsoleStream.cs | 45 +++++++++- .../System.Console/tests/ConsoleHandles.cs | 82 ++++++++++++++++++- .../tests/System.Console.Tests.csproj | 1 + 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs index fe0384b6a8160c..eeffbf0a2d0956 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs @@ -15,6 +15,13 @@ private sealed class UnixConsoleStream : ConsoleStream /// The file descriptor for the opened file. private readonly SafeFileHandle _handle; + /// + /// A FileStream wrapping the handle when it's a seekable file (e.g., a regular file). + /// RandomAccess.Read/Write use pread/pwrite which always read/write at a fixed offset; + /// for seekable files we need a FileStream to properly track the file position. + /// + private readonly FileStream? _fileStream; + private readonly bool _useReadLine; /// Initialize the stream. @@ -28,12 +35,29 @@ internal UnixConsoleStream(SafeFileHandle handle, FileAccess access, bool useRea Debug.Assert(!handle.IsInvalid, "Expected valid console handle"); _handle = handle; _useReadLine = useReadLine; + + // Create a FileStream to determine if the handle is seekable and to use for + // reads/writes on seekable files. RandomAccess.Read/Write use pread/pwrite which + // always operate at a specified offset; passing fileOffset:0 would cause them to + // read/write at position 0 rather than advancing the file position, which produces + // incorrect results for seekable files like regular files. + // For non-seekable files (e.g., pipes, terminals), FileStream.CanSeek is false + // and we fall back to the original RandomAccess-based path. + FileStream fs = new FileStream(handle, access, bufferSize: 0); + if (fs.CanSeek) + { + _fileStream = fs; + } + // else: fs is not seekable; let it be GC'd. Its finalizer calls Dispose(false) + // which does NOT close the handle (OSFileStreamStrategy.Dispose skips the handle + // close when disposing=false), so _handle remains valid. } protected override void Dispose(bool disposing) { if (disposing) { + _fileStream?.Dispose(); _handle.Dispose(); } base.Dispose(disposing); @@ -44,10 +68,25 @@ public override int Read(Span buffer) => _useReadLine ? ConsolePal.StdInReader.ReadLine(buffer) : #endif - RandomAccess.Read(_handle, buffer, fileOffset: 0); + _fileStream is not null ? + _fileStream.Read(buffer) : + RandomAccess.Read(_handle, buffer, fileOffset: 0); - public override void Write(ReadOnlySpan buffer) => - ConsolePal.WriteFromConsoleStream(_handle, buffer); + public override void Write(ReadOnlySpan buffer) + { + if (_fileStream is not null) + { + ConsolePal.EnsureConsoleInitialized(); + lock (Console.Out) + { + _fileStream.Write(buffer); + } + } + else + { + ConsolePal.WriteFromConsoleStream(_handle, buffer); + } + } public override void Flush() { diff --git a/src/libraries/System.Console/tests/ConsoleHandles.cs b/src/libraries/System.Console/tests/ConsoleHandles.cs index b09c94b71497c8..8b9ad04d75e4e4 100644 --- a/src/libraries/System.Console/tests/ConsoleHandles.cs +++ b/src/libraries/System.Console/tests/ConsoleHandles.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; +using System.Text; using Microsoft.DotNet.RemoteExecutor; using Microsoft.Win32.SafeHandles; using Xunit; @@ -82,10 +83,89 @@ public void OpenStandardHandles_CanBeUsedWithStream() // Verify the output was written string output = child.Process.StandardOutput.ReadLine(); Assert.Equal("Test output", output); - + child.Process.WaitForExit(); } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public void UnixConsoleStream_SeekableStdinRedirection_ReadsAllContent() + { + // Regression test: UnixConsoleStream was using RandomAccess.Read with fileOffset=0, + // which caused it to always read from the beginning of seekable files (like regular files), + // resulting in an infinite loop when copying seekable stdin to stdout. + const string inputContent = "Hello from seekable stdin!"; + string testFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + try + { + File.WriteAllText(testFilePath, inputContent, Encoding.UTF8); + + Process process = null; + using (RemoteInvokeHandle handle = RemoteExecutor.Invoke( + static () => + { + Console.OpenStandardInput().CopyTo(Console.OpenStandardOutput()); + return RemoteExecutor.SuccessExitCode; + }, + new RemoteInvokeOptions { Start = false, StartInfo = new ProcessStartInfo { RedirectStandardOutput = true } })) + { + process = handle.Process; + handle.Process = null; + } + + using SafeFileHandle stdinHandle = File.OpenHandle(testFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + process.StartInfo.StandardInputHandle = stdinHandle; + process.Start(); + + string output = process.StandardOutput.ReadToEnd(); + Assert.True(process.WaitForExit(30_000), "Process did not exit in time — possible infinite loop when reading seekable stdin."); + Assert.Equal(inputContent, output); + } + finally + { + File.Delete(testFilePath); + } + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public void UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent() + { + // Regression test: UnixConsoleStream was using RandomAccess.Write with fileOffset=0, + // which caused it to always write to the beginning of seekable files (like regular files), + // overwriting previously written data and producing incorrect output. + const string outputContent = "Hello seekable stdout!"; + string testFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + try + { + Process process = null; + using (RemoteInvokeHandle handle = RemoteExecutor.Invoke( + static () => + { + byte[] bytes = Encoding.UTF8.GetBytes("Hello seekable stdout!"); + Console.OpenStandardOutput().Write(bytes); + return RemoteExecutor.SuccessExitCode; + }, + new RemoteInvokeOptions { Start = false })) + { + process = handle.Process; + handle.Process = null; + } + + using SafeFileHandle stdoutHandle = File.OpenHandle(testFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); + process.StartInfo.StandardOutputHandle = stdoutHandle; + process.Start(); + Assert.True(process.WaitForExit(30_000), "Process did not exit in time."); + + string output = File.ReadAllText(testFilePath, Encoding.UTF8); + Assert.Equal(outputContent, output); + } + finally + { + File.Delete(testFilePath); + } + } + [Fact] [PlatformSpecific(TestPlatforms.Android | TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.Browser)] public void OpenStandardInputHandle_ThrowsOnUnsupportedPlatforms() diff --git a/src/libraries/System.Console/tests/System.Console.Tests.csproj b/src/libraries/System.Console/tests/System.Console.Tests.csproj index 27331007d4bfc3..29169fd5ee9e0a 100644 --- a/src/libraries/System.Console/tests/System.Console.Tests.csproj +++ b/src/libraries/System.Console/tests/System.Console.Tests.csproj @@ -11,6 +11,7 @@ + From da4e00d232a85a04da230b576c3818a4241a05c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:17:52 +0000 Subject: [PATCH 03/10] Address code review: deterministic FileStream cleanup, fix test string 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> --- .../System/ConsolePal.Unix.ConsoleStream.cs | 41 +++++++++---------- .../System.Console/tests/ConsoleHandles.cs | 10 ++--- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs index eeffbf0a2d0956..5b6a15d63623d5 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs @@ -16,11 +16,18 @@ private sealed class UnixConsoleStream : ConsoleStream private readonly SafeFileHandle _handle; /// - /// A FileStream wrapping the handle when it's a seekable file (e.g., a regular file). - /// RandomAccess.Read/Write use pread/pwrite which always read/write at a fixed offset; - /// for seekable files we need a FileStream to properly track the file position. + /// A FileStream wrapping the handle, used to perform reads/writes when the handle is + /// seekable. RandomAccess.Read/Write use pread/pwrite which always read/write at a + /// fixed offset; passing fileOffset:0 would keep reading/writing at position 0 rather + /// than advancing the position, producing incorrect results for seekable files. /// - private readonly FileStream? _fileStream; + private readonly FileStream _fileStream; + + /// + /// True if the file handle is seekable (e.g. a regular file) and + /// should be used for I/O instead of RandomAccess. + /// + private readonly bool _useFileStreamForIo; private readonly bool _useReadLine; @@ -36,28 +43,18 @@ internal UnixConsoleStream(SafeFileHandle handle, FileAccess access, bool useRea _handle = handle; _useReadLine = useReadLine; - // Create a FileStream to determine if the handle is seekable and to use for - // reads/writes on seekable files. RandomAccess.Read/Write use pread/pwrite which - // always operate at a specified offset; passing fileOffset:0 would cause them to - // read/write at position 0 rather than advancing the file position, which produces - // incorrect results for seekable files like regular files. - // For non-seekable files (e.g., pipes, terminals), FileStream.CanSeek is false - // and we fall back to the original RandomAccess-based path. - FileStream fs = new FileStream(handle, access, bufferSize: 0); - if (fs.CanSeek) - { - _fileStream = fs; - } - // else: fs is not seekable; let it be GC'd. Its finalizer calls Dispose(false) - // which does NOT close the handle (OSFileStreamStrategy.Dispose skips the handle - // close when disposing=false), so _handle remains valid. + // Create a FileStream wrapper so we can check whether the handle is seekable and, + // for seekable handles, use it for reads/writes to properly advance the file position. + // The FileStream is always kept alive (never floated for GC) and disposed in Dispose(). + _fileStream = new FileStream(handle, access, bufferSize: 0); + _useFileStreamForIo = _fileStream.CanSeek; } protected override void Dispose(bool disposing) { if (disposing) { - _fileStream?.Dispose(); + _fileStream.Dispose(); _handle.Dispose(); } base.Dispose(disposing); @@ -68,13 +65,13 @@ public override int Read(Span buffer) => _useReadLine ? ConsolePal.StdInReader.ReadLine(buffer) : #endif - _fileStream is not null ? + _useFileStreamForIo ? _fileStream.Read(buffer) : RandomAccess.Read(_handle, buffer, fileOffset: 0); public override void Write(ReadOnlySpan buffer) { - if (_fileStream is not null) + if (_useFileStreamForIo) { ConsolePal.EnsureConsoleInitialized(); lock (Console.Out) diff --git a/src/libraries/System.Console/tests/ConsoleHandles.cs b/src/libraries/System.Console/tests/ConsoleHandles.cs index 8b9ad04d75e4e4..8e29b127a52f00 100644 --- a/src/libraries/System.Console/tests/ConsoleHandles.cs +++ b/src/libraries/System.Console/tests/ConsoleHandles.cs @@ -123,7 +123,7 @@ public void UnixConsoleStream_SeekableStdinRedirection_ReadsAllContent() } finally { - File.Delete(testFilePath); + try { File.Delete(testFilePath); } catch { } } } @@ -140,12 +140,12 @@ public void UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent() { Process process = null; using (RemoteInvokeHandle handle = RemoteExecutor.Invoke( - static () => + static (data) => { - byte[] bytes = Encoding.UTF8.GetBytes("Hello seekable stdout!"); - Console.OpenStandardOutput().Write(bytes); + Console.OpenStandardOutput().Write(Encoding.UTF8.GetBytes(data)); return RemoteExecutor.SuccessExitCode; }, + outputContent, new RemoteInvokeOptions { Start = false })) { process = handle.Process; @@ -162,7 +162,7 @@ public void UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent() } finally { - File.Delete(testFilePath); + try { File.Delete(testFilePath); } catch { } } } From adb2b696ad14459a5dc8a49b46f415d7cfe1067a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 06:53:04 +0000 Subject: [PATCH 04/10] Address review feedback: simplify FileStream usage, update WriteFromConsoleStream, 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> --- .../System/ConsolePal.Unix.ConsoleStream.cs | 38 ++----------- .../src/System/ConsolePal.Unix.cs | 25 ++++++++- .../src/System/ConsolePal.Wasi.cs | 8 +-- .../System.Console/tests/ConsoleHandles.cs | 53 ++++++------------- 4 files changed, 46 insertions(+), 78 deletions(-) diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs index 5b6a15d63623d5..be3ee7a176ec9d 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs @@ -15,20 +15,8 @@ private sealed class UnixConsoleStream : ConsoleStream /// The file descriptor for the opened file. private readonly SafeFileHandle _handle; - /// - /// A FileStream wrapping the handle, used to perform reads/writes when the handle is - /// seekable. RandomAccess.Read/Write use pread/pwrite which always read/write at a - /// fixed offset; passing fileOffset:0 would keep reading/writing at position 0 rather - /// than advancing the position, producing incorrect results for seekable files. - /// private readonly FileStream _fileStream; - /// - /// True if the file handle is seekable (e.g. a regular file) and - /// should be used for I/O instead of RandomAccess. - /// - private readonly bool _useFileStreamForIo; - private readonly bool _useReadLine; /// Initialize the stream. @@ -42,12 +30,7 @@ internal UnixConsoleStream(SafeFileHandle handle, FileAccess access, bool useRea Debug.Assert(!handle.IsInvalid, "Expected valid console handle"); _handle = handle; _useReadLine = useReadLine; - - // Create a FileStream wrapper so we can check whether the handle is seekable and, - // for seekable handles, use it for reads/writes to properly advance the file position. - // The FileStream is always kept alive (never floated for GC) and disposed in Dispose(). _fileStream = new FileStream(handle, access, bufferSize: 0); - _useFileStreamForIo = _fileStream.CanSeek; } protected override void Dispose(bool disposing) @@ -65,25 +48,10 @@ public override int Read(Span buffer) => _useReadLine ? ConsolePal.StdInReader.ReadLine(buffer) : #endif - _useFileStreamForIo ? - _fileStream.Read(buffer) : - RandomAccess.Read(_handle, buffer, fileOffset: 0); + _fileStream.Read(buffer); - public override void Write(ReadOnlySpan buffer) - { - if (_useFileStreamForIo) - { - ConsolePal.EnsureConsoleInitialized(); - lock (Console.Out) - { - _fileStream.Write(buffer); - } - } - else - { - ConsolePal.WriteFromConsoleStream(_handle, buffer); - } - } + public override void Write(ReadOnlySpan buffer) => + ConsolePal.WriteFromConsoleStream(_fileStream, buffer); public override void Flush() { diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs index 054830289c3201..b63e3092cb0b31 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs @@ -950,13 +950,13 @@ internal static void WriteToTerminal(ReadOnlySpan buffer, SafeFileHandle? } } - internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySpan buffer) + internal static void WriteFromConsoleStream(FileStream fs, ReadOnlySpan buffer) { EnsureConsoleInitialized(); lock (Console.Out) // synchronize with other writers { - Write(fd, buffer); + Write(fs, buffer); } } @@ -985,6 +985,27 @@ private static void Write(SafeFileHandle fd, ReadOnlySpan buffer, bool may } } + private static void Write(FileStream fs, ReadOnlySpan buffer, bool mayChangeCursorPosition = true) + { + int cursorVersion = mayChangeCursorPosition ? Volatile.Read(ref s_cursorVersion) : -1; + + try + { + fs.Write(buffer); + } + catch (IOException ex) when (Interop.Sys.ConvertErrorPlatformToPal(ex.HResult) == Interop.Error.EPIPE) + { + // Broken pipe... likely due to being redirected to a program + // that ended, so simply pretend we were successful. + return; + } + + if (mayChangeCursorPosition) + { + UpdatedCachedCursorPosition(buffer, cursorVersion); + } + } + private static void UpdatedCachedCursorPosition(ReadOnlySpan buffer, int cursorVersion) { lock (Console.Out) diff --git a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs index 53d20868d0d028..4b7da9cc43cc09 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs @@ -244,24 +244,24 @@ internal static void EnsureConsoleInitialized() { } - internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySpan buffer) + internal static unsafe void WriteFromConsoleStream(FileStream fs, ReadOnlySpan buffer) { EnsureConsoleInitialized(); lock (Console.Out) // synchronize with other writers { - Write(fd, buffer); + Write(fs, buffer); } } /// Writes data from the buffer into the file descriptor. /// The file descriptor. /// The buffer from which to write data. - private static void Write(SafeFileHandle fd, ReadOnlySpan buffer) + private static void Write(FileStream fs, ReadOnlySpan buffer) { try { - RandomAccess.Write(fd, buffer, fileOffset: 0); + fs.Write(buffer); } catch (IOException ex) when (Interop.Sys.ConvertErrorPlatformToPal(ex.HResult) == Interop.Error.EPIPE) { diff --git a/src/libraries/System.Console/tests/ConsoleHandles.cs b/src/libraries/System.Console/tests/ConsoleHandles.cs index 8e29b127a52f00..71e8f2e5d1fee3 100644 --- a/src/libraries/System.Console/tests/ConsoleHandles.cs +++ b/src/libraries/System.Console/tests/ConsoleHandles.cs @@ -88,81 +88,60 @@ public void OpenStandardHandles_CanBeUsedWithStream() } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [PlatformSpecific(TestPlatforms.AnyUnix)] - public void UnixConsoleStream_SeekableStdinRedirection_ReadsAllContent() + [PlatformSpecific(TestPlatforms.Any & ~TestPlatforms.Browser & ~TestPlatforms.iOS & ~TestPlatforms.tvOS & ~TestPlatforms.Android)] + public void CanCopyStandardInputToStandardOutput() { - // Regression test: UnixConsoleStream was using RandomAccess.Read with fileOffset=0, - // which caused it to always read from the beginning of seekable files (like regular files), - // resulting in an infinite loop when copying seekable stdin to stdout. const string inputContent = "Hello from seekable stdin!"; string testFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + File.WriteAllText(testFilePath, inputContent, Encoding.UTF8); + try { - File.WriteAllText(testFilePath, inputContent, Encoding.UTF8); - - Process process = null; - using (RemoteInvokeHandle handle = RemoteExecutor.Invoke( + using SafeFileHandle stdinHandle = File.OpenHandle(testFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + using RemoteInvokeHandle handle = RemoteExecutor.Invoke( static () => { Console.OpenStandardInput().CopyTo(Console.OpenStandardOutput()); return RemoteExecutor.SuccessExitCode; }, - new RemoteInvokeOptions { Start = false, StartInfo = new ProcessStartInfo { RedirectStandardOutput = true } })) - { - process = handle.Process; - handle.Process = null; - } + new RemoteInvokeOptions { StartInfo = new ProcessStartInfo { RedirectStandardOutput = true, StandardInputHandle = stdinHandle } }); - using SafeFileHandle stdinHandle = File.OpenHandle(testFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); - process.StartInfo.StandardInputHandle = stdinHandle; - process.Start(); - - string output = process.StandardOutput.ReadToEnd(); - Assert.True(process.WaitForExit(30_000), "Process did not exit in time — possible infinite loop when reading seekable stdin."); + string output = handle.Process.StandardOutput.ReadToEnd(); + Assert.True(handle.Process.WaitForExit(30_000), "Process did not exit in time — possible infinite loop when reading seekable stdin."); Assert.Equal(inputContent, output); } finally { - try { File.Delete(testFilePath); } catch { } + File.Delete(testFilePath); } } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [PlatformSpecific(TestPlatforms.AnyUnix)] + [PlatformSpecific(TestPlatforms.Any & ~TestPlatforms.Browser & ~TestPlatforms.iOS & ~TestPlatforms.tvOS & ~TestPlatforms.Android)] public void UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent() { - // Regression test: UnixConsoleStream was using RandomAccess.Write with fileOffset=0, - // which caused it to always write to the beginning of seekable files (like regular files), - // overwriting previously written data and producing incorrect output. const string outputContent = "Hello seekable stdout!"; string testFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); try { - Process process = null; - using (RemoteInvokeHandle handle = RemoteExecutor.Invoke( + using SafeFileHandle stdoutHandle = File.OpenHandle(testFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); + using RemoteInvokeHandle handle = RemoteExecutor.Invoke( static (data) => { Console.OpenStandardOutput().Write(Encoding.UTF8.GetBytes(data)); return RemoteExecutor.SuccessExitCode; }, outputContent, - new RemoteInvokeOptions { Start = false })) - { - process = handle.Process; - handle.Process = null; - } + new RemoteInvokeOptions { StartInfo = new ProcessStartInfo { StandardOutputHandle = stdoutHandle } }); - using SafeFileHandle stdoutHandle = File.OpenHandle(testFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); - process.StartInfo.StandardOutputHandle = stdoutHandle; - process.Start(); - Assert.True(process.WaitForExit(30_000), "Process did not exit in time."); + Assert.True(handle.Process.WaitForExit(30_000), "Process did not exit in time."); string output = File.ReadAllText(testFilePath, Encoding.UTF8); Assert.Equal(outputContent, output); } finally { - try { File.Delete(testFilePath); } catch { } + File.Delete(testFilePath); } } From e5c1365568def986d4c4a481ba013943e711acd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:27:49 +0000 Subject: [PATCH 05/10] Address second round of review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- .../src/System/ConsolePal.Unix.ConsoleStream.cs | 14 -------------- .../System.Console/src/System/ConsolePal.Wasi.cs | 4 ++-- .../System.Console/tests/ConsoleHandles.cs | 3 +-- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs index be3ee7a176ec9d..dbd1a2e4616802 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs @@ -12,9 +12,6 @@ internal static partial class ConsolePal /// Provides a stream to use for Unix console input or output. private sealed class UnixConsoleStream : ConsoleStream { - /// The file descriptor for the opened file. - private readonly SafeFileHandle _handle; - private readonly FileStream _fileStream; private readonly bool _useReadLine; @@ -28,7 +25,6 @@ internal UnixConsoleStream(SafeFileHandle handle, FileAccess access, bool useRea { Debug.Assert(handle != null, "Expected non-null console handle"); Debug.Assert(!handle.IsInvalid, "Expected valid console handle"); - _handle = handle; _useReadLine = useReadLine; _fileStream = new FileStream(handle, access, bufferSize: 0); } @@ -38,7 +34,6 @@ protected override void Dispose(bool disposing) if (disposing) { _fileStream.Dispose(); - _handle.Dispose(); } base.Dispose(disposing); } @@ -52,15 +47,6 @@ public override int Read(Span buffer) => public override void Write(ReadOnlySpan buffer) => ConsolePal.WriteFromConsoleStream(_fileStream, buffer); - - public override void Flush() - { - if (_handle.IsClosed) - { - throw Error.GetFileNotOpen(); - } - base.Flush(); - } } } } diff --git a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs index 4b7da9cc43cc09..c614fccd025a24 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs @@ -254,8 +254,8 @@ internal static unsafe void WriteFromConsoleStream(FileStream fs, ReadOnlySpanWrites data from the buffer into the file descriptor. - /// The file descriptor. + /// Writes data from the buffer into the file stream. + /// The file stream. /// The buffer from which to write data. private static void Write(FileStream fs, ReadOnlySpan buffer) { diff --git a/src/libraries/System.Console/tests/ConsoleHandles.cs b/src/libraries/System.Console/tests/ConsoleHandles.cs index 71e8f2e5d1fee3..56b046ec39a944 100644 --- a/src/libraries/System.Console/tests/ConsoleHandles.cs +++ b/src/libraries/System.Console/tests/ConsoleHandles.cs @@ -88,7 +88,6 @@ public void OpenStandardHandles_CanBeUsedWithStream() } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [PlatformSpecific(TestPlatforms.Any & ~TestPlatforms.Browser & ~TestPlatforms.iOS & ~TestPlatforms.tvOS & ~TestPlatforms.Android)] public void CanCopyStandardInputToStandardOutput() { const string inputContent = "Hello from seekable stdin!"; @@ -117,7 +116,6 @@ public void CanCopyStandardInputToStandardOutput() } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [PlatformSpecific(TestPlatforms.Any & ~TestPlatforms.Browser & ~TestPlatforms.iOS & ~TestPlatforms.tvOS & ~TestPlatforms.Android)] public void UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent() { const string outputContent = "Hello seekable stdout!"; @@ -134,6 +132,7 @@ public void UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent() outputContent, new RemoteInvokeOptions { StartInfo = new ProcessStartInfo { StandardOutputHandle = stdoutHandle } }); + stdoutHandle.Dispose(); Assert.True(handle.Process.WaitForExit(30_000), "Process did not exit in time."); string output = File.ReadAllText(testFilePath, Encoding.UTF8); From 49df75d7b7c113318ebede740389c3746acd96fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:40:32 +0000 Subject: [PATCH 06/10] Remove Write(SafeFileHandle) overload; use FileStream for all terminal writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- .../src/System/ConsolePal.Unix.cs | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs index b63e3092cb0b31..c10ab1a150b19d 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs @@ -38,6 +38,7 @@ internal static partial class ConsolePal private static int s_windowHeight; // Cached WindowHeight, invalid when s_windowWidth == -1. private static int s_invalidateCachedSettings = 1; // Tracks whether we should invalidate the cached settings. private static SafeFileHandle? s_terminalHandle; // Tracks the handle used for writing to the terminal. + private static FileStream? s_terminalFileStream; // FileStream wrapping s_terminalHandle for use with Write(FileStream, ...). /// Gets the lazily-initialized terminal information for the terminal. public static TerminalFormatStrings TerminalFormatStringsInstance { get { return s_terminalFormatStringsInstance.Value; } } @@ -900,6 +901,7 @@ private static unsafe void EnsureInitializedCore() s_terminalHandle = !Console.IsOutputRedirected ? OpenStandardOutputHandle() : !Console.IsInputRedirected ? OpenStandardInputHandle() : null; + s_terminalFileStream = s_terminalHandle != null ? new FileStream(s_terminalHandle, FileAccess.Write, bufferSize: 0) : null; // Provide the native lib with the correct code from the terminfo to transition us into // "application mode". This will both transition it immediately, as well as allow @@ -941,12 +943,19 @@ private static unsafe void EnsureInitializedCore() internal static void WriteToTerminal(ReadOnlySpan buffer, SafeFileHandle? handle = null, bool mayChangeCursorPosition = true) { - handle ??= s_terminalHandle; - Debug.Assert(handle is not null); - lock (Console.Out) // synchronize with other writers { - Write(handle, buffer, mayChangeCursorPosition); + if (handle is null) + { + Debug.Assert(s_terminalFileStream is not null); + Write(s_terminalFileStream, buffer, mayChangeCursorPosition); + } + 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); + Write(fs, buffer, mayChangeCursorPosition); + } } } @@ -960,31 +969,10 @@ internal static void WriteFromConsoleStream(FileStream fs, ReadOnlySpan bu } } - /// Writes data from the buffer into the file descriptor. - /// The file descriptor. + /// Writes data from the buffer into the file stream. + /// The file stream. /// The buffer from which to write data. /// Writing this buffer may change the cursor position. - private static void Write(SafeFileHandle fd, ReadOnlySpan buffer, bool mayChangeCursorPosition = true) - { - int cursorVersion = mayChangeCursorPosition ? Volatile.Read(ref s_cursorVersion) : -1; - - try - { - RandomAccess.Write(fd, buffer, fileOffset: 0); - } - catch (IOException ex) when (Interop.Sys.ConvertErrorPlatformToPal(ex.HResult) == Interop.Error.EPIPE) - { - // Broken pipe... likely due to being redirected to a program - // that ended, so simply pretend we were successful. - return; - } - - if (mayChangeCursorPosition) - { - UpdatedCachedCursorPosition(buffer, cursorVersion); - } - } - private static void Write(FileStream fs, ReadOnlySpan buffer, bool mayChangeCursorPosition = true) { int cursorVersion = mayChangeCursorPosition ? Volatile.Read(ref s_cursorVersion) : -1; From 1bcaaec48ee607ed7e01d2b329ca44074fcf9829 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:40:12 +0000 Subject: [PATCH 07/10] Replace temporary FileStream in WriteToTerminal with cached s_stdoutFileStream Address review feedback: the `using FileStream fs = new FileStream(handle, ...)` in WriteToTerminal can close the handle and is expensive (per-call allocation). - Add cached `s_stdoutFileStream` initialized alongside `s_terminalFileStream` - Change `WriteToTerminal` and `WriteTerminalAnsiString` parameters from `SafeFileHandle? handle` to `bool useStdout` to select between the two cached FileStreams - `WriteTerminalAnsiColorString` passes `useStdout: true` instead of creating a new SafeFileHandle via `OpenStandardOutputHandle()` each call Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d049facb-2b18-4073-b081-e0c924a03ad4 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/System/ConsolePal.Unix.cs | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs index c10ab1a150b19d..4895511692f9c1 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs @@ -39,6 +39,7 @@ internal static partial class ConsolePal private static int s_invalidateCachedSettings = 1; // Tracks whether we should invalidate the cached settings. private static SafeFileHandle? s_terminalHandle; // Tracks the handle used for writing to the terminal. private static FileStream? s_terminalFileStream; // FileStream wrapping s_terminalHandle for use with Write(FileStream, ...). + private static FileStream? s_stdoutFileStream; // Cached FileStream for stdout, used by WriteTerminalAnsiColorString. /// Gets the lazily-initialized terminal information for the terminal. public static TerminalFormatStrings TerminalFormatStringsInstance { get { return s_terminalFormatStringsInstance.Value; } } @@ -902,6 +903,7 @@ private static unsafe void EnsureInitializedCore() !Console.IsInputRedirected ? OpenStandardInputHandle() : null; s_terminalFileStream = s_terminalHandle != null ? new FileStream(s_terminalHandle, FileAccess.Write, bufferSize: 0) : null; + s_stdoutFileStream = new FileStream(OpenStandardOutputHandle(), FileAccess.Write, bufferSize: 0); // Provide the native lib with the correct code from the terminfo to transition us into // "application mode". This will both transition it immediately, as well as allow @@ -941,21 +943,12 @@ private static unsafe void EnsureInitializedCore() } } - internal static void WriteToTerminal(ReadOnlySpan buffer, SafeFileHandle? handle = null, bool mayChangeCursorPosition = true) + internal static void WriteToTerminal(ReadOnlySpan buffer, bool useStdout = false, bool mayChangeCursorPosition = true) { lock (Console.Out) // synchronize with other writers { - if (handle is null) - { - Debug.Assert(s_terminalFileStream is not null); - Write(s_terminalFileStream, buffer, mayChangeCursorPosition); - } - 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); - Write(fs, buffer, mayChangeCursorPosition); - } + FileStream fs = useStdout ? s_stdoutFileStream! : s_terminalFileStream!; + Write(fs, buffer, mayChangeCursorPosition); } } @@ -1080,13 +1073,13 @@ private static void InvalidateTerminalSettings() // FORCE_COLOR is set, or when DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is set. // In all cases, they are written to stdout. internal static void WriteTerminalAnsiColorString(string? value) - => WriteTerminalAnsiString(value, OpenStandardOutputHandle(), mayChangeCursorPosition: false); + => WriteTerminalAnsiString(value, useStdout: true, mayChangeCursorPosition: false); /// Writes a terminfo-based ANSI escape string to stdout. /// The string to write. - /// Handle to use instead of s_terminalHandle. + /// Whether to write to stdout instead of the terminal handle. /// Writing this value may change the cursor position. - internal static void WriteTerminalAnsiString(string? value, SafeFileHandle? handle = null, bool mayChangeCursorPosition = true) + internal static void WriteTerminalAnsiString(string? value, bool useStdout = false, bool mayChangeCursorPosition = true) { if (string.IsNullOrEmpty(value)) return; @@ -1104,7 +1097,7 @@ internal static void WriteTerminalAnsiString(string? value, SafeFileHandle? hand } EnsureConsoleInitialized(); - WriteToTerminal(data, handle, mayChangeCursorPosition); + WriteToTerminal(data, useStdout, mayChangeCursorPosition); } } } From b19f10a47e090f14dc71b4ee1a93fb618d294025 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 12:01:18 +0000 Subject: [PATCH 08/10] Revert System.Console source changes from PR #125512, keep only tests 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> --- .../System.Console/src/System.Console.csproj | 16 +++ .../System/ConsolePal.Unix.ConsoleStream.cs | 20 ++- .../src/System/ConsolePal.Unix.cs | 115 ++++++++++++------ .../src/System/ConsolePal.Wasi.cs | 67 ++++++++-- 4 files changed, 165 insertions(+), 53 deletions(-) diff --git a/src/libraries/System.Console/src/System.Console.csproj b/src/libraries/System.Console/src/System.Console.csproj index 34c35a2f62cd64..341c9947b9c829 100644 --- a/src/libraries/System.Console/src/System.Console.csproj +++ b/src/libraries/System.Console/src/System.Console.csproj @@ -81,8 +81,16 @@ Link="Common\Interop\Unix\Interop.StdinReady.cs" /> + + + + + + + + Provides a stream to use for Unix console input or output. private sealed class UnixConsoleStream : ConsoleStream { - private readonly FileStream _fileStream; + /// The file descriptor for the opened file. + private readonly SafeFileHandle _handle; private readonly bool _useReadLine; @@ -25,15 +26,15 @@ internal UnixConsoleStream(SafeFileHandle handle, FileAccess access, bool useRea { Debug.Assert(handle != null, "Expected non-null console handle"); Debug.Assert(!handle.IsInvalid, "Expected valid console handle"); + _handle = handle; _useReadLine = useReadLine; - _fileStream = new FileStream(handle, access, bufferSize: 0); } protected override void Dispose(bool disposing) { if (disposing) { - _fileStream.Dispose(); + _handle.Dispose(); } base.Dispose(disposing); } @@ -43,10 +44,19 @@ public override int Read(Span buffer) => _useReadLine ? ConsolePal.StdInReader.ReadLine(buffer) : #endif - _fileStream.Read(buffer); + ConsolePal.Read(_handle, buffer); public override void Write(ReadOnlySpan buffer) => - ConsolePal.WriteFromConsoleStream(_fileStream, buffer); + ConsolePal.WriteFromConsoleStream(_handle, buffer); + + public override void Flush() + { + if (_handle.IsClosed) + { + throw Error.GetFileNotOpen(); + } + base.Flush(); + } } } } diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs index 4895511692f9c1..57d4ef11fd62e3 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs @@ -38,8 +38,6 @@ internal static partial class ConsolePal private static int s_windowHeight; // Cached WindowHeight, invalid when s_windowWidth == -1. private static int s_invalidateCachedSettings = 1; // Tracks whether we should invalidate the cached settings. private static SafeFileHandle? s_terminalHandle; // Tracks the handle used for writing to the terminal. - private static FileStream? s_terminalFileStream; // FileStream wrapping s_terminalHandle for use with Write(FileStream, ...). - private static FileStream? s_stdoutFileStream; // Cached FileStream for stdout, used by WriteTerminalAnsiColorString. /// Gets the lazily-initialized terminal information for the terminal. public static TerminalFormatStrings TerminalFormatStringsInstance { get { return s_terminalFormatStringsInstance.Value; } } @@ -902,8 +900,6 @@ private static unsafe void EnsureInitializedCore() s_terminalHandle = !Console.IsOutputRedirected ? OpenStandardOutputHandle() : !Console.IsInputRedirected ? OpenStandardInputHandle() : null; - s_terminalFileStream = s_terminalHandle != null ? new FileStream(s_terminalHandle, FileAccess.Write, bufferSize: 0) : null; - s_stdoutFileStream = new FileStream(OpenStandardOutputHandle(), FileAccess.Write, bufferSize: 0); // Provide the native lib with the correct code from the terminfo to transition us into // "application mode". This will both transition it immediately, as well as allow @@ -943,58 +939,104 @@ private static unsafe void EnsureInitializedCore() } } - internal static void WriteToTerminal(ReadOnlySpan buffer, bool useStdout = false, bool mayChangeCursorPosition = true) + /// Reads data from the file descriptor into the buffer. + /// The file descriptor. + /// The buffer to read into. + /// The number of bytes read, or an exception if there's an error. + private static unsafe int Read(SafeFileHandle fd, Span buffer) { + fixed (byte* bufPtr = buffer) + { + int result = Interop.CheckIo(Interop.Sys.Read(fd, bufPtr, buffer.Length)); + Debug.Assert(result <= buffer.Length); + return result; + } + } + + internal static void WriteToTerminal(ReadOnlySpan buffer, SafeFileHandle? handle = null, bool mayChangeCursorPosition = true) + { + handle ??= s_terminalHandle; + Debug.Assert(handle is not null); + lock (Console.Out) // synchronize with other writers { - FileStream fs = useStdout ? s_stdoutFileStream! : s_terminalFileStream!; - Write(fs, buffer, mayChangeCursorPosition); + Write(handle, buffer, mayChangeCursorPosition); } } - internal static void WriteFromConsoleStream(FileStream fs, ReadOnlySpan buffer) + internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySpan buffer) { EnsureConsoleInitialized(); lock (Console.Out) // synchronize with other writers { - Write(fs, buffer); + Write(fd, buffer); } } - /// Writes data from the buffer into the file stream. - /// The file stream. + /// Writes data from the buffer into the file descriptor. + /// The file descriptor. /// The buffer from which to write data. /// Writing this buffer may change the cursor position. - private static void Write(FileStream fs, ReadOnlySpan buffer, bool mayChangeCursorPosition = true) + private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan buffer, bool mayChangeCursorPosition = true) { - int cursorVersion = mayChangeCursorPosition ? Volatile.Read(ref s_cursorVersion) : -1; - - try + fixed (byte* p = buffer) { - fs.Write(buffer); - } - catch (IOException ex) when (Interop.Sys.ConvertErrorPlatformToPal(ex.HResult) == Interop.Error.EPIPE) - { - // Broken pipe... likely due to being redirected to a program - // that ended, so simply pretend we were successful. - return; - } + byte* bufPtr = p; + int count = buffer.Length; + while (count > 0) + { + int cursorVersion = mayChangeCursorPosition ? Volatile.Read(ref s_cursorVersion) : -1; - if (mayChangeCursorPosition) - { - UpdatedCachedCursorPosition(buffer, cursorVersion); + int bytesWritten = Interop.Sys.Write(fd, bufPtr, count); + if (bytesWritten < 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (errorInfo.Error == Interop.Error.EPIPE) + { + // Broken pipe... likely due to being redirected to a program + // that ended, so simply pretend we were successful. + return; + } + else if (errorInfo.Error == Interop.Error.EAGAIN) // aka EWOULDBLOCK + { + // May happen if the file handle is configured as non-blocking. + // In that case, we need to wait to be able to write and then + // try again. We poll, but don't actually care about the result, + // only the blocking behavior, and thus ignore any poll errors + // and loop around to do another write (which may correctly fail + // if something else has gone wrong). + Interop.Sys.Poll(fd, Interop.PollEvents.POLLOUT, Timeout.Infinite, out Interop.PollEvents triggered); + continue; + } + else + { + // Something else... fail. + throw Interop.GetExceptionForIoErrno(errorInfo); + } + } + else + { + if (mayChangeCursorPosition) + { + UpdatedCachedCursorPosition(bufPtr, bytesWritten, cursorVersion); + } + } + + count -= bytesWritten; + bufPtr += bytesWritten; + } } } - private static void UpdatedCachedCursorPosition(ReadOnlySpan buffer, int cursorVersion) + private static unsafe void UpdatedCachedCursorPosition(byte* bufPtr, int count, int cursorVersion) { lock (Console.Out) { int left, top; if (cursorVersion != s_cursorVersion || // the cursor was changed during the write by another operation !TryGetCachedCursorPosition(out left, out top) || // we don't have a cursor position - buffer.Length > InteractiveBufferSize) // limit the amount of bytes we are willing to inspect + count > InteractiveBufferSize) // limit the amount of bytes we are willing to inspect { InvalidateCachedCursorPosition(); return; @@ -1002,8 +1044,9 @@ private static void UpdatedCachedCursorPosition(ReadOnlySpan buffer, int c GetWindowSize(out int width, out int height); - foreach (byte c in buffer) + for (int i = 0; i < count; i++) { + byte c = bufPtr[i]; if (c < 127 && c >= 32) // ASCII/UTF-8 characters that take up a single position { left++; @@ -1069,17 +1112,17 @@ private static void InvalidateTerminalSettings() Volatile.Write(ref s_invalidateCachedSettings, 1); } - // ANSI colors are enabled when stdout is a terminal, when - // FORCE_COLOR is set, or when DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is set. - // In all cases, they are written to stdout. + // Ansi colors are enabled when stdout is a terminal or when + // DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is set. + // In both cases, they are written to stdout. internal static void WriteTerminalAnsiColorString(string? value) - => WriteTerminalAnsiString(value, useStdout: true, mayChangeCursorPosition: false); + => WriteTerminalAnsiString(value, OpenStandardOutputHandle(), mayChangeCursorPosition: false); /// Writes a terminfo-based ANSI escape string to stdout. /// The string to write. - /// Whether to write to stdout instead of the terminal handle. + /// Handle to use instead of s_terminalHandle. /// Writing this value may change the cursor position. - internal static void WriteTerminalAnsiString(string? value, bool useStdout = false, bool mayChangeCursorPosition = true) + internal static void WriteTerminalAnsiString(string? value, SafeFileHandle? handle = null, bool mayChangeCursorPosition = true) { if (string.IsNullOrEmpty(value)) return; @@ -1097,7 +1140,7 @@ internal static void WriteTerminalAnsiString(string? value, bool useStdout = fal } EnsureConsoleInitialized(); - WriteToTerminal(data, useStdout, mayChangeCursorPosition); + WriteToTerminal(data, handle, mayChangeCursorPosition); } } } diff --git a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs index c614fccd025a24..70aec828947c03 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs @@ -244,29 +244,72 @@ internal static void EnsureConsoleInitialized() { } - internal static unsafe void WriteFromConsoleStream(FileStream fs, ReadOnlySpan buffer) + /// Reads data from the file descriptor into the buffer. + /// The file descriptor. + /// The buffer to read into. + /// The number of bytes read, or an exception if there's an error. + private static unsafe int Read(SafeFileHandle fd, Span buffer) + { + fixed (byte* bufPtr = buffer) + { + int result = Interop.CheckIo(Interop.Sys.Read(fd, bufPtr, buffer.Length)); + Debug.Assert(result <= buffer.Length); + return result; + } + } + + internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySpan buffer) { EnsureConsoleInitialized(); lock (Console.Out) // synchronize with other writers { - Write(fs, buffer); + Write(fd, buffer); } } - /// Writes data from the buffer into the file stream. - /// The file stream. + /// Writes data from the buffer into the file descriptor. + /// The file descriptor. /// The buffer from which to write data. - private static void Write(FileStream fs, ReadOnlySpan buffer) + private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan buffer) { - try - { - fs.Write(buffer); - } - catch (IOException ex) when (Interop.Sys.ConvertErrorPlatformToPal(ex.HResult) == Interop.Error.EPIPE) + fixed (byte* p = buffer) { - // Broken pipe... likely due to being redirected to a program - // that ended, so simply pretend we were successful. + byte* bufPtr = p; + int count = buffer.Length; + while (count > 0) + { + int bytesWritten = Interop.Sys.Write(fd, bufPtr, count); + if (bytesWritten < 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (errorInfo.Error == Interop.Error.EPIPE) + { + // Broken pipe... likely due to being redirected to a program + // that ended, so simply pretend we were successful. + return; + } + else if (errorInfo.Error == Interop.Error.EAGAIN) // aka EWOULDBLOCK + { + // May happen if the file handle is configured as non-blocking. + // In that case, we need to wait to be able to write and then + // try again. We poll, but don't actually care about the result, + // only the blocking behavior, and thus ignore any poll errors + // and loop around to do another write (which may correctly fail + // if something else has gone wrong). + Interop.Sys.Poll(fd, Interop.PollEvents.POLLOUT, Timeout.Infinite, out Interop.PollEvents triggered); + continue; + } + else + { + // Something else... fail. + throw Interop.GetExceptionForIoErrno(errorInfo); + } + } + + count -= bytesWritten; + bufPtr += bytesWritten; + } } } } From fdd0a2ef4ade0370386f325955b1524af5a0bd6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:44:28 +0000 Subject: [PATCH 09/10] Split stdout test into two writes to better cover seekable offset regression Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8c4bae7d-c5a8-4e7d-9567-c22ad8c8ff76 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../System.Console/tests/ConsoleHandles.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Console/tests/ConsoleHandles.cs b/src/libraries/System.Console/tests/ConsoleHandles.cs index 56b046ec39a944..3930c6c6875dd5 100644 --- a/src/libraries/System.Console/tests/ConsoleHandles.cs +++ b/src/libraries/System.Console/tests/ConsoleHandles.cs @@ -118,25 +118,29 @@ public void CanCopyStandardInputToStandardOutput() [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public void UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent() { - const string outputContent = "Hello seekable stdout!"; + const string outputContentPart1 = "Hello seekable "; + const string outputContentPart2 = "stdout!"; string testFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); try { using SafeFileHandle stdoutHandle = File.OpenHandle(testFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); using RemoteInvokeHandle handle = RemoteExecutor.Invoke( - static (data) => + static (part1, part2) => { - Console.OpenStandardOutput().Write(Encoding.UTF8.GetBytes(data)); + Stream stdout = Console.OpenStandardOutput(); + stdout.Write(Encoding.UTF8.GetBytes(part1)); + stdout.Write(Encoding.UTF8.GetBytes(part2)); return RemoteExecutor.SuccessExitCode; }, - outputContent, + outputContentPart1, + outputContentPart2, new RemoteInvokeOptions { StartInfo = new ProcessStartInfo { StandardOutputHandle = stdoutHandle } }); stdoutHandle.Dispose(); Assert.True(handle.Process.WaitForExit(30_000), "Process did not exit in time."); string output = File.ReadAllText(testFilePath, Encoding.UTF8); - Assert.Equal(outputContent, output); + Assert.Equal(outputContentPart1 + outputContentPart2, output); } finally { From 5107f24449b951bba928ff0bdca46ced4142dc9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:50:13 +0000 Subject: [PATCH 10/10] Restore PR #124492 FORCE_COLOR comment that was accidentally reverted Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ee38cb6a-7c51-4290-b71e-c815838eb480 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/libraries/System.Console/src/System/ConsolePal.Unix.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs index 57d4ef11fd62e3..5ddf6b1e737353 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs @@ -1112,9 +1112,9 @@ private static void InvalidateTerminalSettings() Volatile.Write(ref s_invalidateCachedSettings, 1); } - // Ansi colors are enabled when stdout is a terminal or when - // DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is set. - // In both cases, they are written to stdout. + // ANSI colors are enabled when stdout is a terminal, when + // FORCE_COLOR is set, or when DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is set. + // In all cases, they are written to stdout. internal static void WriteTerminalAnsiColorString(string? value) => WriteTerminalAnsiString(value, OpenStandardOutputHandle(), mayChangeCursorPosition: false);