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);