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 @@ +