How to interrupt Console.ReadLine

后端 未结 10 712
情歌与酒
情歌与酒 2020-11-28 11:34

Is it possible to stop the Console.ReadLine() programmatically?

I have a console application: the much of the logic runs on a different thread and in th

相关标签:
10条回答
  • 2020-11-28 11:34

    I was also looking for a way to stop reading from console in certain conditions. the solution i came up with was to make a non blocking version of read line with these two methods.

    static IEnumerator<Task<string>> AsyncConsoleInput()
    {
        var e = loop(); e.MoveNext(); return e;
        IEnumerator<Task<string>> loop()
        {
            while (true) yield return Task.Run(() => Console.ReadLine());
        }
    }
    
    static Task<string> ReadLine(this IEnumerator<Task<string>> console)
    {
        if (console.Current.IsCompleted) console.MoveNext();
        return console.Current;
    }
    

    this allows us to have ReadLine on separate thread and we can wait for it or use it in other places conditionally.

    var console = AsyncConsoleInput();
    
    var task = Task.Run(() =>
    {
         // your task on separate thread
    });
    
    if (Task.WaitAny(console.ReadLine(), task) == 0) // if ReadLine finished first
    {
        task.Wait();
        var x = console.Current.Result; // last user input (await instead of Result in async method)
    }
    else // task finished first 
    {
        var x = console.ReadLine(); // this wont issue another read line because user did not input anything yet. 
    }
    
    0 讨论(0)
  • 2020-11-28 11:38

    The current accepted answer don't work any longer so I decided to create a new one. The only safe way to do this is to create your own ReadLine method I can think of many many scenarios requiring such functionality and the code here implements one of them:

    public static string CancellableReadLine(CancellationToken cancellationToken)
    {
        StringBuilder stringBuilder = new StringBuilder();
        Task.Run(() =>
        {
            try
            {
                ConsoleKeyInfo keyInfo;
                var startingLeft = Con.CursorLeft;
                var startingTop = Con.CursorTop;
                var currentIndex = 0;
                do
                {
                    var previousLeft = Con.CursorLeft;
                    var previousTop = Con.CursorTop;
                    while (!Con.KeyAvailable)
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                        Thread.Sleep(50);
                    }
                    keyInfo = Con.ReadKey();
                    switch (keyInfo.Key)
                    {
                        case ConsoleKey.A:
                        case ConsoleKey.B:
                        case ConsoleKey.C:
                        case ConsoleKey.D:
                        case ConsoleKey.E:
                        case ConsoleKey.F:
                        case ConsoleKey.G:
                        case ConsoleKey.H:
                        case ConsoleKey.I:
                        case ConsoleKey.J:
                        case ConsoleKey.K:
                        case ConsoleKey.L:
                        case ConsoleKey.M:
                        case ConsoleKey.N:
                        case ConsoleKey.O:
                        case ConsoleKey.P:
                        case ConsoleKey.Q:
                        case ConsoleKey.R:
                        case ConsoleKey.S:
                        case ConsoleKey.T:
                        case ConsoleKey.U:
                        case ConsoleKey.V:
                        case ConsoleKey.W:
                        case ConsoleKey.X:
                        case ConsoleKey.Y:
                        case ConsoleKey.Z:
                        case ConsoleKey.Spacebar:
                        case ConsoleKey.Decimal:
                        case ConsoleKey.Add:
                        case ConsoleKey.Subtract:
                        case ConsoleKey.Multiply:
                        case ConsoleKey.Divide:
                        case ConsoleKey.D0:
                        case ConsoleKey.D1:
                        case ConsoleKey.D2:
                        case ConsoleKey.D3:
                        case ConsoleKey.D4:
                        case ConsoleKey.D5:
                        case ConsoleKey.D6:
                        case ConsoleKey.D7:
                        case ConsoleKey.D8:
                        case ConsoleKey.D9:
                        case ConsoleKey.NumPad0:
                        case ConsoleKey.NumPad1:
                        case ConsoleKey.NumPad2:
                        case ConsoleKey.NumPad3:
                        case ConsoleKey.NumPad4:
                        case ConsoleKey.NumPad5:
                        case ConsoleKey.NumPad6:
                        case ConsoleKey.NumPad7:
                        case ConsoleKey.NumPad8:
                        case ConsoleKey.NumPad9:
                        case ConsoleKey.Oem1:
                        case ConsoleKey.Oem102:
                        case ConsoleKey.Oem2:
                        case ConsoleKey.Oem3:
                        case ConsoleKey.Oem4:
                        case ConsoleKey.Oem5:
                        case ConsoleKey.Oem6:
                        case ConsoleKey.Oem7:
                        case ConsoleKey.Oem8:
                        case ConsoleKey.OemComma:
                        case ConsoleKey.OemMinus:
                        case ConsoleKey.OemPeriod:
                        case ConsoleKey.OemPlus:
                            stringBuilder.Insert(currentIndex, keyInfo.KeyChar);
                            currentIndex++;
                            if (currentIndex < stringBuilder.Length)
                            {
                                var left = Con.CursorLeft;
                                var top = Con.CursorTop;
                                Con.Write(stringBuilder.ToString().Substring(currentIndex));
                                Con.SetCursorPosition(left, top);
                            }
                            break;
                        case ConsoleKey.Backspace:
                            if (currentIndex > 0)
                            {
                                currentIndex--;
                                stringBuilder.Remove(currentIndex, 1);
                                var left = Con.CursorLeft;
                                var top = Con.CursorTop;
                                if (left == previousLeft)
                                {
                                    left = Con.BufferWidth - 1;
                                    top--;
                                    Con.SetCursorPosition(left, top);
                                }
                                Con.Write(stringBuilder.ToString().Substring(currentIndex) + " ");
                                Con.SetCursorPosition(left, top);
                            }
                            else
                            {
                                Con.SetCursorPosition(startingLeft, startingTop);
                            }
                            break;
                        case ConsoleKey.Delete:
                            if (stringBuilder.Length > currentIndex)
                            {
                                stringBuilder.Remove(currentIndex, 1);
                                Con.SetCursorPosition(previousLeft, previousTop);
                                Con.Write(stringBuilder.ToString().Substring(currentIndex) + " ");
                                Con.SetCursorPosition(previousLeft, previousTop);
                            }
                            else
                                Con.SetCursorPosition(previousLeft, previousTop);
                            break;
                        case ConsoleKey.LeftArrow:
                            if (currentIndex > 0)
                            {
                                currentIndex--;
                                var left = Con.CursorLeft - 2;
                                var top = Con.CursorTop;
                                if (left < 0)
                                {
                                    left = Con.BufferWidth + left;
                                    top--;
                                }
                                Con.SetCursorPosition(left, top);
                                if (currentIndex < stringBuilder.Length - 1)
                                {
                                    Con.Write(stringBuilder[currentIndex].ToString() + stringBuilder[currentIndex + 1]);
                                    Con.SetCursorPosition(left, top);
                                }
                            }
                            else
                            {
                                Con.SetCursorPosition(startingLeft, startingTop);
                                if (stringBuilder.Length > 0)
                                    Con.Write(stringBuilder[0]);
                                Con.SetCursorPosition(startingLeft, startingTop);
                            }
                            break;
                        case ConsoleKey.RightArrow:
                            if (currentIndex < stringBuilder.Length)
                            {
                                Con.SetCursorPosition(previousLeft, previousTop);
                                Con.Write(stringBuilder[currentIndex]);
                                currentIndex++;
                            }
                            else
                            {
                                Con.SetCursorPosition(previousLeft, previousTop);
                            }
                            break;
                        case ConsoleKey.Home:
                            if (stringBuilder.Length > 0 && currentIndex != stringBuilder.Length)
                            {
                                Con.SetCursorPosition(previousLeft, previousTop);
                                Con.Write(stringBuilder[currentIndex]);
                            }
                            Con.SetCursorPosition(startingLeft, startingTop);
                            currentIndex = 0;
                            break;
                        case ConsoleKey.End:
                            if (currentIndex < stringBuilder.Length)
                            {
                                Con.SetCursorPosition(previousLeft, previousTop);
                                Con.Write(stringBuilder[currentIndex]);
                                var left = previousLeft + stringBuilder.Length - currentIndex;
                                var top = previousTop;
                                while (left > Con.BufferWidth)
                                {
                                    left -= Con.BufferWidth;
                                    top++;
                                }
                                currentIndex = stringBuilder.Length;
                                Con.SetCursorPosition(left, top);
                            }
                            else
                                Con.SetCursorPosition(previousLeft, previousTop);
                            break;
                        default:
                            Con.SetCursorPosition(previousLeft, previousTop);
                            break;
                    }
                } while (keyInfo.Key != ConsoleKey.Enter);
                Con.WriteLine();
            }
            catch
            {
                //MARK: Change this based on your need. See description below.
                stringBuilder.Clear();
            }
        }).Wait();
        return stringBuilder.ToString();
    }
    

    Place this function somewhere in your code and this gives you a function that can be cancelled via a CancellationToken also for better code I have used

    using Con = System.Console;
    

    This function returns an empty string upon cancellation (which was good for my case) you can throw an exception inside the marked catch expression above if you wish.

    Also in the same catch expression you can remove the stringBuilder.Clear(); line and that will cause the code to return what user entered so far. Combine this with a successful or canceled flag and you can keep what used entered so far and use it in further requests.

    Other thing you can change is that you can set a timeout besides the cancellation token in the loop if you want to get a timeout functionality.

    I tried to be as clean as I need but this code can be cleaner. The method can become async itself and timeout and cancellation token passed in.

    0 讨论(0)
  • 2020-11-28 11:39

    UPDATE: this technique is no longer reliable on Windows 10. Don't use it please.
    Fairly heavy implementation changes in Win10 to make a console act more like a terminal. No doubt to assist in the new Linux sub-system. One (unintended?) side-effect is that CloseHandle() deadlocks until a read is completed, killing this approach dead. I'll leave the original post in place, only because it might help somebody to find an alternative.

    UPDATE2: Look at wischi's answer for a decent alternative.


    It's possible, you have to jerk the floor mat by closing the stdin stream. This program demonstrates the idea:

    using System;
    using System.Threading;
    using System.Runtime.InteropServices;
    
    namespace ConsoleApplication2 {
        class Program {
            static void Main(string[] args) {
                ThreadPool.QueueUserWorkItem((o) => {
                    Thread.Sleep(1000);
                    IntPtr stdin = GetStdHandle(StdHandle.Stdin);
                    CloseHandle(stdin);
                });
                Console.ReadLine();
            }
    
            // P/Invoke:
            private enum StdHandle { Stdin = -10, Stdout = -11, Stderr = -12 };
            [DllImport("kernel32.dll")]
            private static extern IntPtr GetStdHandle(StdHandle std);
            [DllImport("kernel32.dll")]
            private static extern bool CloseHandle(IntPtr hdl);
        }
    }
    
    0 讨论(0)
  • 2020-11-28 11:40

    So here is a Solution working on Windows 10 and not using any fancy threading or dllimport magic. It worked fine for me I hope it helps.

    I basically create a stream reader sitting on the standard input. Reading it kinda "async" and just dispose the stream reader if I want to cancel the readline.

    Here is my Code:

        private System.IO.StreamReader stdinsr = new System.IO.StreamReader(Console.OpenStandardInput());
        [DebuggerHidden]
        private string ReadLine() {
            return stdinsr.ReadLineAsync().Result;
        }
    
        protected override void OnExit(ExitEventArgs e) {
            base.OnExit(e);
    
            commandLooper.Abort();
            stdinsr.Dispose();
        }
    

    NOTE: Yes I read async but I wait for the task result so its basically still waiting for user input.

    0 讨论(0)
  • 2020-11-28 11:43

    This will process a Ctrl+C in a seperate thread while your app is waiting for a Console.Readline():

    Console.CancelKeyPress += (_, e) =>
    {
        e.Cancel = true;
        Environment.Exit(0);
    };
    
    0 讨论(0)
  • 2020-11-28 11:47

    I just stumbled upon this little library on GitHub: https://github.com/tonerdo/readline

    ReadLine is a GNU Readline like library built in pure C#. It can serve as a drop in replacement for the inbuilt Console.ReadLine() and brings along with it some of the terminal goodness you get from unix shells, like command history navigation and tab auto completion.

    It is cross platform and runs anywhere .NET is supported, targeting netstandard1.3 means that it can be used with .NET Core as well as the full .NET Framework.

    Although this library doesn't support interrupting input at the time of writing, it should be trivial to update it to do so. Alternatively, it can be an interesting example of writing a custom solution to counter the limitations of Console.ReadLine.

    0 讨论(0)
提交回复
热议问题