Console.Unix: make Console.OpenStandardInput Stream aware of terminal (#39192)
authorTom Deseyn <tom.deseyn@gmail.com>
Fri, 14 Aug 2020 15:24:32 +0000 (17:24 +0200)
committerGitHub <noreply@github.com>
Fri, 14 Aug 2020 15:24:32 +0000 (17:24 +0200)
* 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 <eirik.tsarpalis@gmail.com>
src/libraries/System.Console/src/System/ConsolePal.Unix.cs
src/libraries/System.Console/src/System/IO/StdInReader.cs
src/libraries/System.Console/src/System/IO/SyncTextReader.Unix.cs
src/libraries/System.Console/tests/ManualTests/ManualTests.cs

index 098601b..017c55e 100644 (file)
@@ -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
             /// <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)
@@ -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)
index 99ebbd3..6396176 100644 (file)
@@ -22,6 +22,7 @@ namespace System.IO
         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.
@@ -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<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
@@ -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.
index f98499d..9e68d74 100644 (file)
@@ -39,5 +39,8 @@ namespace System.IO
                 }
             }
         }
+
+        public int ReadLine(byte[] buffer, int offset, int count)
+            => Inner.ReadLine(buffer, offset, count);
     }
 }
index 9bbec55..5928b7a 100644 (file)
@@ -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<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)
             {
@@ -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))]