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" />
+
+
+
+
+
+
+
+
buffer) =>
_useReadLine ?
ConsolePal.StdInReader.ReadLine(buffer) :
#endif
- RandomAccess.Read(_handle, buffer, fileOffset: 0);
+ ConsolePal.Read(_handle, buffer);
public override void Write(ReadOnlySpan buffer) =>
ConsolePal.WriteFromConsoleStream(_handle, buffer);
diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
index 054830289c3201..5ddf6b1e737353 100644
--- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
+++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
@@ -939,6 +939,20 @@ private static unsafe void EnsureInitializedCore()
}
}
+ /// 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;
@@ -964,35 +978,65 @@ internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySp
/// The file descriptor.
/// 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)
+ 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)
{
- 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;
- }
+ 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;
@@ -1000,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++;
diff --git a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs
index 53d20868d0d028..70aec828947c03 100644
--- a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs
+++ b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs
@@ -244,6 +244,20 @@ internal static void EnsureConsoleInitialized()
{
}
+ /// 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();
@@ -257,16 +271,45 @@ internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySp
/// 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 unsafe void Write(SafeFileHandle fd, ReadOnlySpan buffer)
{
- try
- {
- RandomAccess.Write(fd, buffer, fileOffset: 0);
- }
- 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;
+ }
}
}
}
diff --git a/src/libraries/System.Console/tests/ConsoleHandles.cs b/src/libraries/System.Console/tests/ConsoleHandles.cs
index b09c94b71497c8..3930c6c6875dd5 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,71 @@ 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))]
+ public void CanCopyStandardInputToStandardOutput()
+ {
+ const string inputContent = "Hello from seekable stdin!";
+ string testFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ File.WriteAllText(testFilePath, inputContent, Encoding.UTF8);
+
+ try
+ {
+ 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 { StartInfo = new ProcessStartInfo { RedirectStandardOutput = true, StandardInputHandle = stdinHandle } });
+
+ 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
+ {
+ File.Delete(testFilePath);
+ }
+ }
+
+ [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ public void UnixConsoleStream_SeekableStdoutRedirection_WritesAllContent()
+ {
+ 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 (part1, part2) =>
+ {
+ Stream stdout = Console.OpenStandardOutput();
+ stdout.Write(Encoding.UTF8.GetBytes(part1));
+ stdout.Write(Encoding.UTF8.GetBytes(part2));
+ return RemoteExecutor.SuccessExitCode;
+ },
+ 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(outputContentPart1 + outputContentPart2, 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 @@
+