Skip to content

Console.Unix: make Console.OpenStandardInput Stream aware of terminal #39192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 14, 2020
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
20 changes: 16 additions & 4 deletions src/libraries/System.Console/src/System/ConsolePal.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ internal static class ConsolePal

public static Stream OpenStandardInput()
{
return new UnixConsoleStream(SafeFileHandleHelper.Open(() => Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDIN_FILENO)), FileAccess.Read);
return new UnixConsoleStream(SafeFileHandleHelper.Open(() => Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDIN_FILENO)), FileAccess.Read,
useReadLine: !Console.IsInputRedirected);
}

public static Stream OpenStandardOutput()
Expand All @@ -68,7 +69,7 @@ public static Encoding OutputEncoding

private static SyncTextReader? s_stdInReader;

private static SyncTextReader StdInReader
internal static SyncTextReader StdInReader
{
get
{
Expand Down Expand Up @@ -1410,15 +1411,19 @@ private sealed class UnixConsoleStream : ConsoleStream
/// <summary>The file descriptor for the opened file.</summary>
private readonly SafeFileHandle _handle;

private readonly bool _useReadLine;

/// <summary>Initialize the stream.</summary>
/// <param name="handle">The file handle wrapped by this stream.</param>
/// <param name="access">FileAccess.Read or FileAccess.Write.</param>
internal UnixConsoleStream(SafeFileHandle handle, FileAccess access)
/// <param name="useReadLine">Use ReadLine API for reading.</param>
internal UnixConsoleStream(SafeFileHandle handle, FileAccess access, bool useReadLine = false)
: base(access)
{
Debug.Assert(handle != null, "Expected non-null console handle");
Debug.Assert(!handle.IsInvalid, "Expected valid console handle");
_handle = handle;
_useReadLine = useReadLine;
}

protected override void Dispose(bool disposing)
Expand All @@ -1434,7 +1439,14 @@ public override int Read(byte[] buffer, int offset, int count)
{
ValidateRead(buffer, offset, count);

return ConsolePal.Read(_handle, buffer, offset, count);
if (_useReadLine)
{
return ConsolePal.StdInReader.ReadLine(buffer, offset, count);
}
else
{
return ConsolePal.Read(_handle, buffer, offset, count);
}
}

public override void Write(byte[] buffer, int offset, int count)
Expand Down
83 changes: 65 additions & 18 deletions src/libraries/System.Console/src/System/IO/StdInReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal sealed class StdInReader : TextReader
private readonly Stack<ConsoleKeyInfo> _tmpKeys = new Stack<ConsoleKeyInfo>(); // temporary working stack; should be empty outside of ReadLine
private readonly Stack<ConsoleKeyInfo> _availableKeys = new Stack<ConsoleKeyInfo>(); // a queue of already processed key infos available for reading
private readonly Encoding _encoding;
private Encoder? _bufferReadEncoder;

private char[] _unprocessedBufferToBeRead; // Buffer that might have already been read from stdin but not yet processed.
private const int BytesToBeRead = 1024; // No. of bytes to be read from the stream at a time.
Expand Down Expand Up @@ -79,13 +80,63 @@ internal unsafe int ReadStdin(byte* buffer, int bufferSize)

public override string? ReadLine()
{
return ReadLine(consumeKeys: true);
bool isEnter = ReadLineCore(consumeKeys: true);
string? line = null;
if (isEnter || _readLineSB.Length > 0)
{
line = _readLineSB.ToString();
_readLineSB.Clear();
}
return line;
}

private string? ReadLine(bool consumeKeys)
public int ReadLine(byte[] buffer, int offset, int count)
{
if (count == 0)
{
return 0;
}

// Don't read a new line if there are remaining characters in the StringBuilder.
if (_readLineSB.Length == 0)
{
bool isEnter = ReadLineCore(consumeKeys: true);
if (isEnter)
{
_readLineSB.Append('\n');
}
}

// Encode line into buffer.
Encoder encoder = _bufferReadEncoder ??= _encoding.GetEncoder();
int bytesUsedTotal = 0;
int charsUsedTotal = 0;
Span<byte> destination = buffer.AsSpan(offset, count);
foreach (ReadOnlyMemory<char> chunk in _readLineSB.GetChunks())
{
encoder.Convert(chunk.Span, destination, flush: false, out int charsUsed, out int bytesUsed, out bool completed);
destination = destination.Slice(bytesUsed);
bytesUsedTotal += bytesUsed;
charsUsedTotal += charsUsed;

if (charsUsed == 0)
{
break;
}
}
_readLineSB.Remove(0, charsUsedTotal);
return bytesUsedTotal;
}

// Reads a line in _readLineSB when consumeKeys is true,
// or _availableKeys when consumeKeys is false.
// Returns whether the line was terminated using the Enter key.
private bool ReadLineCore(bool consumeKeys)
{
Debug.Assert(_tmpKeys.Count == 0);
string? readLineStr = null;

// Don't carry over chars from previous ReadLine call.
_readLineSB.Clear();

Interop.Sys.InitializeConsoleBeforeRead();
try
Expand All @@ -110,23 +161,15 @@ internal unsafe int ReadStdin(byte* buffer, int bufferSize)
// try to keep this very simple, at least for now.
if (keyInfo.Key == ConsoleKey.Enter)
{
readLineStr = _readLineSB.ToString();
_readLineSB.Clear();
if (!previouslyProcessed)
{
Console.WriteLine();
}
break;
return true;
}
else if (IsEol(keyInfo.KeyChar))
{
string line = _readLineSB.ToString();
_readLineSB.Clear();
if (line.Length > 0)
{
readLineStr = line;
}
break;
return false;
}
else if (keyInfo.Key == ConsoleKey.Backspace)
{
Expand Down Expand Up @@ -166,7 +209,10 @@ internal unsafe int ReadStdin(byte* buffer, int bufferSize)
}
else if (keyInfo.Key == ConsoleKey.Tab)
{
_readLineSB.Append(keyInfo.KeyChar);
if (consumeKeys)
{
_readLineSB.Append(keyInfo.KeyChar);
}
if (!previouslyProcessed)
{
Console.Write(' ');
Expand All @@ -182,7 +228,10 @@ internal unsafe int ReadStdin(byte* buffer, int bufferSize)
}
else if (keyInfo.KeyChar != '\0')
{
_readLineSB.Append(keyInfo.KeyChar);
if (consumeKeys)
{
_readLineSB.Append(keyInfo.KeyChar);
}
if (!previouslyProcessed)
{
Console.Write(keyInfo.KeyChar);
Expand All @@ -200,8 +249,6 @@ internal unsafe int ReadStdin(byte* buffer, int bufferSize)
_availableKeys.Push(_tmpKeys.Pop());
}
}

return readLineStr;
}

public override int Read() => ReadOrPeek(peek: false);
Expand All @@ -213,7 +260,7 @@ private int ReadOrPeek(bool peek)
// If there aren't any keys in our processed keys stack, read a line to populate it.
if (_availableKeys.Count == 0)
{
ReadLine(consumeKeys: false);
ReadLineCore(consumeKeys: false);
}

// Now if there are keys, use the first.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,8 @@ public bool KeyAvailable
}
}
}

public int ReadLine(byte[] buffer, int offset, int count)
=> Inner.ReadLine(buffer, offset, count);
}
}
52 changes: 27 additions & 25 deletions src/libraries/System.Console/tests/ManualTests/ManualTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Threading.Tasks;
using System.IO;
using Xunit;

namespace System
Expand All @@ -23,6 +24,26 @@ public static void ReadLine(bool consoleIn)
AssertUserExpectedResults("the characters you typed properly echoed as you typed");
}

[ConditionalFact(nameof(ManualTestsEnabled))]
public static void ReadLineFromOpenStandardInput()
{
string expectedLine = "aab";

// Use Console.ReadLine
Console.WriteLine($"Please type 'a' 3 times, press 'Backspace' to erase 1, then type a single 'b' and press 'Enter'.");
string result = Console.ReadLine();
Assert.Equal(expectedLine, result);
AssertUserExpectedResults("the characters you typed properly echoed as you typed");

// ReadLine from Console.OpenStandardInput
Console.WriteLine($"Please type 'a' 3 times, press 'Backspace' to erase 1, then type a single 'b' and press 'Enter'.");
using Stream inputStream = Console.OpenStandardInput();
using StreamReader reader = new StreamReader(inputStream);
result = reader.ReadLine();
Assert.Equal(expectedLine, result);
AssertUserExpectedResults("the characters you typed properly echoed as you typed");
}

[ConditionalFact(nameof(ManualTestsEnabled))]
public static void ReadLine_BackSpaceCanMoveAccrossWrappedLines()
{
Expand All @@ -36,6 +57,7 @@ public static void ReadLine_BackSpaceCanMoveAccrossWrappedLines()
}

[ConditionalFact(nameof(ManualTestsEnabled))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/40735", TestPlatforms.Windows)]
public static void InPeek()
{
Console.WriteLine("Please type \"peek\" (without the quotes). You should see it as you type:");
Expand Down Expand Up @@ -91,19 +113,11 @@ static string RenderKeyChord(ConsoleKeyInfo key)

public static IEnumerable<object[]> GetKeyChords()
{
yield return MkConsoleKeyInfo('\x01', ConsoleKey.A, ConsoleModifiers.Control);
yield return MkConsoleKeyInfo('\x01', ConsoleKey.A, ConsoleModifiers.Control | ConsoleModifiers.Alt);
yield return MkConsoleKeyInfo('\x02', ConsoleKey.B, ConsoleModifiers.Control);
yield return MkConsoleKeyInfo(OperatingSystem.IsWindows() ? '\x00' : '\x02', ConsoleKey.B, ConsoleModifiers.Control | ConsoleModifiers.Alt);
yield return MkConsoleKeyInfo('\r', ConsoleKey.Enter, (ConsoleModifiers)0);

if (OperatingSystem.IsWindows())
{
// windows will report '\n' as 'Ctrl+Enter', which is typically not picked up by Unix terminals
yield return MkConsoleKeyInfo('\n', ConsoleKey.Enter, ConsoleModifiers.Control);
}
else
{
yield return MkConsoleKeyInfo('\n', ConsoleKey.J, ConsoleModifiers.Control);
}
// windows will report '\n' as 'Ctrl+Enter', which is typically not picked up by Unix terminals
yield return MkConsoleKeyInfo('\n', OperatingSystem.IsWindows() ? ConsoleKey.Enter : ConsoleKey.J, ConsoleModifiers.Control);

static object[] MkConsoleKeyInfo (char keyChar, ConsoleKey consoleKey, ConsoleModifiers modifiers)
{
Expand All @@ -117,18 +131,6 @@ static object[] MkConsoleKeyInfo (char keyChar, ConsoleKey consoleKey, ConsoleMo
}
}

[ConditionalFact(nameof(ManualTestsEnabled))]
public static void OpenStandardInput()
{
Console.WriteLine("Please type \"console\" (without the quotes). You shouldn't see it as you type:");
var stream = Console.OpenStandardInput();
var textReader = new System.IO.StreamReader(stream);
var result = textReader.ReadLine();

Assert.Equal("console", result);
AssertUserExpectedResults("\"console\" correctly not echoed as you typed it");
}

[ConditionalFact(nameof(ManualTestsEnabled))]
public static void ConsoleOutWriteLine()
{
Expand Down Expand Up @@ -216,7 +218,7 @@ public static void CursorPositionAndArrowKeys()
}
}

AssertUserExpectedResults("the arrow keys move around the screen as expected with no other bad artificts");
AssertUserExpectedResults("the arrow keys move around the screen as expected with no other bad artifacts");
}

[ConditionalFact(nameof(ManualTestsEnabled))]
Expand Down