Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/libraries/System.Console/src/System.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,16 @@
Link="Common\Interop\Unix\Interop.StdinReady.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.ReadStdinUnbuffered.cs"
Link="Common\Interop\Unix\Interop.ReadStdinUnbuffered.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />
<Compile Include="$(CommonPath)System\Text\EncodingHelper.Unix.cs"
Link="Common\System\Text\EncodingHelper.Unix.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Write.cs"
Link="Common\Interop\Unix\Interop.Write.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs"
Link="Common\Interop\Unix\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Errors.cs"
Expand Down Expand Up @@ -225,6 +233,10 @@
Link="Common\Interop\Unix\Interop.Open.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.OpenFlags.cs"
Link="Common\Interop\Unix\Interop.OpenFlags.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\Interop.Poll.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetEUid.cs"
Link="Common\Interop\Unix\Interop.GetEUid.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetPwUid.cs"
Expand All @@ -237,6 +249,10 @@
Link="Common\Interop\Unix\Interop.SNPrintF.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Stat.cs"
Link="Common\Interop\Unix\Interop.Stat.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
Link="Common\Interop\Unix\Interop.Read.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Write.cs"
Link="Common\Interop\Unix\Interop.Write.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetWindowWidth.cs"
Link="Common\Interop\Unix\Interop.GetWindowWidth.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.InitializeTerminalAndSignalHandling.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public override int Read(Span<byte> buffer) =>
_useReadLine ?
ConsolePal.StdInReader.ReadLine(buffer) :
#endif
RandomAccess.Read(_handle, buffer, fileOffset: 0);
ConsolePal.Read(_handle, buffer);

public override void Write(ReadOnlySpan<byte> buffer) =>
ConsolePal.WriteFromConsoleStream(_handle, buffer);
Expand Down
81 changes: 63 additions & 18 deletions src/libraries/System.Console/src/System/ConsolePal.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,20 @@ private static unsafe void EnsureInitializedCore()
}
}

/// <summary>Reads data from the file descriptor into the buffer.</summary>
/// <param name="fd">The file descriptor.</param>
/// <param name="buffer">The buffer to read into.</param>
/// <returns>The number of bytes read, or an exception if there's an error.</returns>
private static unsafe int Read(SafeFileHandle fd, Span<byte> 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<byte> buffer, SafeFileHandle? handle = null, bool mayChangeCursorPosition = true)
{
handle ??= s_terminalHandle;
Expand All @@ -964,44 +978,75 @@ internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySp
/// <param name="fd">The file descriptor.</param>
/// <param name="buffer">The buffer from which to write data.</param>
/// <param name="mayChangeCursorPosition">Writing this buffer may change the cursor position.</param>
private static void Write(SafeFileHandle fd, ReadOnlySpan<byte> buffer, bool mayChangeCursorPosition = true)
private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan<byte> 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);
Comment thread
adamsitnik marked this conversation as resolved.
continue;
}
else
{
// Something else... fail.
throw Interop.GetExceptionForIoErrno(errorInfo);
}
}
else
{
if (mayChangeCursorPosition)
{
UpdatedCachedCursorPosition(bufPtr, bytesWritten, cursorVersion);
}
Comment thread
adamsitnik marked this conversation as resolved.
}

count -= bytesWritten;
bufPtr += bytesWritten;
}
}
}

private static void UpdatedCachedCursorPosition(ReadOnlySpan<byte> 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;
}

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++;
Expand Down
59 changes: 51 additions & 8 deletions src/libraries/System.Console/src/System/ConsolePal.Wasi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,20 @@ internal static void EnsureConsoleInitialized()
{
}

/// <summary>Reads data from the file descriptor into the buffer.</summary>
/// <param name="fd">The file descriptor.</param>
/// <param name="buffer">The buffer to read into.</param>
/// <returns>The number of bytes read, or an exception if there's an error.</returns>
private static unsafe int Read(SafeFileHandle fd, Span<byte> 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<byte> buffer)
{
EnsureConsoleInitialized();
Expand All @@ -257,16 +271,45 @@ internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySp
/// <summary>Writes data from the buffer into the file descriptor.</summary>
/// <param name="fd">The file descriptor.</param>
/// <param name="buffer">The buffer from which to write data.</param>
private static void Write(SafeFileHandle fd, ReadOnlySpan<byte> buffer)
private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan<byte> 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);
Comment thread
adamsitnik marked this conversation as resolved.
continue;
}
else
{
// Something else... fail.
throw Interop.GetExceptionForIoErrno(errorInfo);
}
}

count -= bytesWritten;
bufPtr += bytesWritten;
}
}
}
}
Expand Down
64 changes: 63 additions & 1 deletion src/libraries/System.Console/tests/ConsoleHandles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics;
using System.IO;
using System.Text;
using Microsoft.DotNet.RemoteExecutor;
using Microsoft.Win32.SafeHandles;
using Xunit;
Expand Down Expand Up @@ -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))]
Comment thread
adamsitnik marked this conversation as resolved.
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))]
Comment thread
adamsitnik marked this conversation as resolved.
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.");
Comment thread
adamsitnik marked this conversation as resolved.

string output = File.ReadAllText(testFilePath, Encoding.UTF8);
Assert.Equal(outputContentPart1 + outputContentPart2, output);
}
finally
{
File.Delete(testFilePath);
}
}
Comment thread
adamsitnik marked this conversation as resolved.

[Fact]
[PlatformSpecific(TestPlatforms.Android | TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.Browser)]
public void OpenStandardInputHandle_ThrowsOnUnsupportedPlatforms()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<Compile Include="Helpers.cs" />
<Compile Include="ReadAndWrite.cs" />
<Compile Include="ConsoleKeyInfoTests.cs" />
<Compile Include="ConsoleHandles.cs" />
Comment thread
adamsitnik marked this conversation as resolved.
<Compile Include="ConsoleStream.cs" />
<Compile Include="SetError.cs" />
<Compile Include="SetIn.cs" />
Expand Down
Loading