From: Tom Deseyn Date: Fri, 14 Aug 2020 15:24:32 +0000 (+0200) Subject: Console.Unix: make Console.OpenStandardInput Stream aware of terminal (#39192) X-Git-Tag: submit/tizen/20210909.063632~5981 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=1e6e8d9c8faa9a0dd51c2de67fcf1f5e0a4a1ed5;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Console.Unix: make Console.OpenStandardInput Stream aware of terminal (#39192) * Console.Unix: make Console.OpenStandardInput Stream aware of terminal When performing OpenStandardInput against a terminal, perform Reads on a line-by-line basis and perform appropriate processing and echoing. * Add test * fix manual tests for Windows Co-authored-by: Eirik Tsarpalis --- diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs index 098601b..017c55e 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs @@ -43,7 +43,8 @@ namespace System 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() @@ -68,7 +69,7 @@ namespace System private static SyncTextReader? s_stdInReader; - private static SyncTextReader StdInReader + internal static SyncTextReader StdInReader { get { @@ -1410,15 +1411,19 @@ namespace System /// The file descriptor for the opened file. private readonly SafeFileHandle _handle; + private readonly bool _useReadLine; + /// Initialize the stream. /// The file handle wrapped by this stream. /// FileAccess.Read or FileAccess.Write. - internal UnixConsoleStream(SafeFileHandle handle, FileAccess access) + /// Use ReadLine API for reading. + 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) @@ -1434,7 +1439,14 @@ namespace System { 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) diff --git a/src/libraries/System.Console/src/System/IO/StdInReader.cs b/src/libraries/System.Console/src/System/IO/StdInReader.cs index 99ebbd3..6396176 100644 --- a/src/libraries/System.Console/src/System/IO/StdInReader.cs +++ b/src/libraries/System.Console/src/System/IO/StdInReader.cs @@ -22,6 +22,7 @@ namespace System.IO private readonly Stack _tmpKeys = new Stack(); // temporary working stack; should be empty outside of ReadLine private readonly Stack _availableKeys = new Stack(); // 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. @@ -79,13 +80,63 @@ namespace System.IO 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 destination = buffer.AsSpan(offset, count); + foreach (ReadOnlyMemory 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 @@ -110,23 +161,15 @@ namespace System.IO // 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) { @@ -166,7 +209,10 @@ namespace System.IO } else if (keyInfo.Key == ConsoleKey.Tab) { - _readLineSB.Append(keyInfo.KeyChar); + if (consumeKeys) + { + _readLineSB.Append(keyInfo.KeyChar); + } if (!previouslyProcessed) { Console.Write(' '); @@ -182,7 +228,10 @@ namespace System.IO } else if (keyInfo.KeyChar != '\0') { - _readLineSB.Append(keyInfo.KeyChar); + if (consumeKeys) + { + _readLineSB.Append(keyInfo.KeyChar); + } if (!previouslyProcessed) { Console.Write(keyInfo.KeyChar); @@ -200,8 +249,6 @@ namespace System.IO _availableKeys.Push(_tmpKeys.Pop()); } } - - return readLineStr; } public override int Read() => ReadOrPeek(peek: false); @@ -213,7 +260,7 @@ namespace System.IO // 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. diff --git a/src/libraries/System.Console/src/System/IO/SyncTextReader.Unix.cs b/src/libraries/System.Console/src/System/IO/SyncTextReader.Unix.cs index f98499d..9e68d74 100644 --- a/src/libraries/System.Console/src/System/IO/SyncTextReader.Unix.cs +++ b/src/libraries/System.Console/src/System/IO/SyncTextReader.Unix.cs @@ -39,5 +39,8 @@ namespace System.IO } } } + + public int ReadLine(byte[] buffer, int offset, int count) + => Inner.ReadLine(buffer, offset, count); } } diff --git a/src/libraries/System.Console/tests/ManualTests/ManualTests.cs b/src/libraries/System.Console/tests/ManualTests/ManualTests.cs index 9bbec55..5928b7a 100644 --- a/src/libraries/System.Console/tests/ManualTests/ManualTests.cs +++ b/src/libraries/System.Console/tests/ManualTests/ManualTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using System.IO; using Xunit; namespace System @@ -24,6 +25,26 @@ namespace System } [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() { Console.WriteLine("Please press 'a' until it wraps to the next terminal line, then press 'Backspace' until the input is erased, and then type a single 'a' and press 'Enter'."); @@ -36,6 +57,7 @@ namespace System } [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:"); @@ -91,19 +113,11 @@ namespace System public static IEnumerable 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) { @@ -118,18 +132,6 @@ namespace System } [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() { Console.Out.WriteLine("abcdefghijklmnopqrstuvwxyz"); @@ -216,7 +218,7 @@ namespace System } } - 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))]