Why is .NET Core handling ReadKey differently on R

2019-06-16 02:56发布

问题:

I'm writing a .NET Core console application. I wanted to limit console input to a certain number of maximum characters for each input. I have some code that does this by building a string with Console.ReadKey() instead of Console.ReadLine() Everything worked perfectly testing it on Windows. Then, when I deployed to a Raspberry Pi 3 running Raspbian, I quickly encountered all sorts of problems. I remembered that Linux handles line endings differently from Windows, and it seems backspaces are handled differently as well. I changed the way I handled those, going off the ConsoleKey instead of the character, and the newline problem went away, but backspaces only sometimes register. Also, sometimes characters get outputted to the console outside of my input box, even though I set the ReadKey to not output to the console on its own. Am I missing something about how Linux handles console input?

//I replaced my calls to Console.ReadLine() with this. The limit is the
//max number of characters that can be entered in the console.
public static string ReadChars(int limit)
{
    string str = string.Empty; //all the input so far
    int left = Console.CursorLeft; //store cursor position for re-outputting
    int top = Console.CursorTop;
    while (true) //keep checking for key events
    {
        if (Console.KeyAvailable)
        {
            //true to intercept input and not output to console
            //normally. This sometimes fails and outputs anyway.
            ConsoleKeyInfo c = Console.ReadKey(true);
            if (c.Key == ConsoleKey.Enter) //stop input on Enter key
                break;
            if (c.Key == ConsoleKey.Backspace) //remove last char on Backspace
            {
                if (str != "")
                {
                     tr = str.Substring(0, str.Length - 1);
                }
            }
            else if (c.Key != ConsoleKey.Tab && str.Length < limit)
            {
                //don't allow tabs or exceeding the max size
                str += c.KeyChar;
            }
            else
            {
                //ignore tabs and when the limit is exceeded
                continue;
            }
            Console.SetCursorPosition(left, top);
            string padding = ""; //padding clears unused chars in field
            for (int i = 0; i < limit - str.Length; i++)
            {
                padding += " ";
            }
            //output this way instead
            Console.Write(str + padding);
        }
    }
    return str;
}

回答1:

I think the fundamental issue is exposed by Stephen Toub's comment in this GitHub issue:

You may be thinking of the fact that we now only disable echo during a ReadKey(intercept: true) call, so in a race between the user typing and you calling ReadKey(intercept: true), the key might be echo'd even when you were hoping it wouldn't be, but you won't lose the keystroke.

Which is cold comfort, but accurate. This is a race that is very hard to win. The core problem is that a Linux terminal works very differently from the Windows console. It operates much more like a teletype did back in the 1970s. You banged away on the keyboard, regardless if the computer was paying any attention to what you type, the teletype just echo-ed what you typed banging it out on paper. Until you press the Enter key, then the computer started cranking away at the text.

Very different from the Windows console, it requires the program to have an active Read call to echo any typed text.

So this is a pretty fundamental mismatch with the console api. It needs an Echo property to give you any hope of doing this correctly. So you can set it to false before you start accepting input and take care of the echo yourself. It is still a race, but at least you have a shot at clearing any pre-typed text.

The only half-decent workaround you have now is to disable echo before you start your program. Requiring you to do all input through your method.



回答2:

I tested and found out that Console.ReadKey(true) indeed has some bugs where the key actually gets echoed to the console when the typing is fast or repeat keys are fired. This is something that you don't expect, but why it happens I have no idea.

If you are interested in Debugging it, you can look at the below source code

https://referencesource.microsoft.com/#mscorlib/system/console.cs,1476

I chose to put a workaround for the issue. So there are few issues in your approach. The Left Arrow and Right Arrow keys should be handled or they should not be allowed. I chose the later one by adding below code

if (c.Key == ConsoleKey.LeftArrow || c.Key == ConsoleKey.RightArrow) {
   continue;
}

When you type the characters using below

Console.Write(str + padding);

You basically disturb the cursor position also, which is not correct. So you need to set the cursor position after this using below

Console.CursorLeft = str.Length;

Now comes the part of handling the leaky keys which is probably a .NET bug, I added below code

else
{
    //ignore tabs and when the ilimit is exceeded
    if (Console.CursorLeft > str.Length){

        var delta = Console.CursorLeft - str.Length;
        Console.CursorLeft = str.Length;
        Console.Write(new String(' ',delta));
        Console.CursorLeft = str.Length;
    }
    continue;
}

So we check for any unseen reason something was echoed then erase it. Then stress tested it

$ docker run -it console
Please enter some text:
tarun6686e
You entered: tarun6686e

Below is the final code that I had used

using System;

namespace ConsoleTest
{
    public class Program {
        public static string tr="";
        //I replaced my calls to Console.ReadLine() with this. The limit is the
        //max number of characters that can be entered in the console.
        public static string ReadChars(int limit)
        {
            string str = string.Empty; //all the input so far
            int left = Console.CursorLeft; //store cursor position for re-outputting
            int top = Console.CursorTop;

            while (true) //keep checking for key events
            {
                if (Console.KeyAvailable)
                {
                    //true to intercept input and not output to console
                    //normally. This sometimes fails and outputs anyway.
                    ConsoleKeyInfo c = Console.ReadKey(true);
                    string name = Enum.GetName(typeof(ConsoleKey), c.Key);
                    var key = c.KeyChar;
                    // Console.WriteLine(String.Format("Name={0}, Key={1}, KeyAscii={2}", name, key,(int)key));
                    if (c.Key == ConsoleKey.Enter) //stop input on Enter key
                        {
                            Console.WriteLine();
                            break;
                        }
                    if (c.Key == ConsoleKey.LeftArrow || c.Key == ConsoleKey.RightArrow) {
                        continue;
                    }

                    if (c.Key == ConsoleKey.Backspace) //remove last char on Backspace
                    {
                        if (str != "")
                        {
                            str = str.Substring(0, str.Length - 1);
                        }
                    }
                    else if (c.Key != ConsoleKey.Tab && str.Length < limit)
                    {
                        //don't allow tabs or exceeding the max size
                        str += c.KeyChar;
                    }
                    else
                    {
                        //ignore tabs and when the ilimit is exceeded
                        if (Console.CursorLeft > str.Length){

                            var delta = Console.CursorLeft - str.Length;
                            Console.CursorLeft = str.Length;
                            Console.Write(new String(' ',delta));
                            Console.CursorLeft = str.Length;
                        }
                        continue;
                    }
                    Console.SetCursorPosition(left, top);
                    string padding = ""; //padding clears unused chars in field
                    for (int i = 0; i < limit - str.Length; i++)
                    {
                        padding += " ";
                    }
                    //output this way instead
                    Console.Write(str + padding);
                    Console.CursorLeft = str.Length;
                }
            }
            return str;
        }

        public static void Main(string[] args) {
            Console.WriteLine("Please enter some text: ");
            var text = ReadChars(10);

            Console.WriteLine("You entered: " + text);
        }
    }
}