Как прервать Console.ReadLine

Можно ли программно остановить программный код Console.ReadLine()?

У меня консольное приложение: большая часть логики работает в другом потоке, а в основном потоке я принимаю вход с помощью Console.ReadLine(). Я бы хотел прекратить чтение с консоли, когда выделенный поток остановился.

Как я могу это достичь?

Ответ 1

ОБНОВЛЕНИЕ: эта техника больше не надежна в Windows 10. Не используйте ее, пожалуйста.
Довольно серьезные изменения в реализации в Win10, чтобы консоль действовала как терминал. Без сомнения, чтобы помочь в новой подсистеме Linux. Один (непреднамеренный?) Побочный эффект заключается в том, что CloseHandle() блокируется до завершения чтения, убивая этот подход мертвым. Я оставлю оригинальное сообщение на месте только потому, что оно может помочь кому-то найти альтернативу.

ОБНОВЛЕНИЕ 2: Посмотрите на ответ Wischi для достойной альтернативы.


Возможно, вам придется дернуть коврик, закрыв поток stdin. Эта программа демонстрирует идею:

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);
    }
}

Ответ 2

Отправьте [enter] в текущее запущенное консольное приложение:

    class Program
    {
        [DllImport("User32.Dll", EntryPoint = "PostMessageA")]
        private static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam);

        const int VK_RETURN = 0x0D;
        const int WM_KEYDOWN = 0x100;

        static void Main(string[] args)
        {
            Console.Write("Switch focus to another window now.\n");

            ThreadPool.QueueUserWorkItem((o) =>
            {
                Thread.Sleep(4000);

                var hWnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
                PostMessage(hWnd, WM_KEYDOWN, VK_RETURN, 0);
            });

            Console.ReadLine();

            Console.Write("ReadLine() successfully aborted by background thread.\n");
            Console.Write("[any key to exit]");
            Console.ReadKey();
        }
    }

Этот код отправляет [enter] в текущий консольный процесс, прерывая любые блокировки ReadLine() в неуправляемом коде глубоко внутри ядра Windows, что позволяет потоку С# естественным образом завершать работу.

Я использовал этот код вместо ответа, который включает закрытие консоли, потому что закрытие консоли означает, что ReadLine() и ReadKey() навсегда отключены с этого момента в коде (это вызовет исключение, если оно используется).

Этот ответ превосходит все решения, использующие SendKeys и Windows Input Simulator, поскольку он работает, даже если текущее приложение не имеет фокуса.

Ответ 3

Мне нужно решение, которое будет работать с Mono, поэтому API не вызывает. Я публикую это только для того, чтобы кто-либо еще находился в той же ситуации или хочет, чтобы это было сделано с помощью С#. Функция CreateKeyInfoFromInt() является сложной частью (некоторые ключи имеют длину более одного байта). В приведенном ниже коде ReadKey() выдает исключение, если ReadKeyReset() вызывается из другого потока. Код ниже не полностью завершен, но он демонстрирует концепцию использования существующих функций Console С# для создания взаимозаменяемой функции GetKey().

static ManualResetEvent resetEvent = new ManualResetEvent(true);

/// <summary>
/// Resets the ReadKey function from another thread.
/// </summary>
public static void ReadKeyReset()
{
    resetEvent.Set();
}

/// <summary>
/// Reads a key from stdin
/// </summary>
/// <returns>The ConsoleKeyInfo for the pressed key.</returns>
/// <param name='intercept'>Intercept the key</param>
public static ConsoleKeyInfo ReadKey(bool intercept = false)
{
    resetEvent.Reset();
    while (!Console.KeyAvailable)
    {
        if (resetEvent.WaitOne(50))
            throw new GetKeyInteruptedException();
    }
    int x = CursorX, y = CursorY;
    ConsoleKeyInfo result = CreateKeyInfoFromInt(Console.In.Read(), false);
    if (intercept)
    {
        // Not really an intercept, but it works with mono at least
        if (result.Key != ConsoleKey.Backspace)
        {
            Write(x, y, " ");
            SetCursorPosition(x, y);
        }
        else
        {
            if ((x == 0) && (y > 0))
            {
                y--;
                x = WindowWidth - 1;
            }
            SetCursorPosition(x, y);
        }
    }
    return result;
}

Ответ 4

Текущий принятый ответ больше не работает, поэтому я решил создать новый. Единственный безопасный способ сделать это - создать свой собственный метод ReadLine, я могу придумать много сценариев, требующих такой функциональности, и здесь код реализует один из них:

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();
}

Поместите эту функцию где-нибудь в свой код, и это даст вам функцию, которая может быть отменена с помощью CancellationToken также для лучшего кода, который я использовал

using Con = System.Console;

Эта функция возвращает пустую строку при аннулировании (что было хорошо для моего случая), вы можете выбросить исключение внутри отмеченного выражения catch, если хотите.

Также в том же выражении catch вы можете удалить строку stringBuilder.Clear();, и это приведет к тому, что код вернет введенный вами пользователь. Объедините это с успешным или отмененным флагом, и вы можете сохранить использованное до сих пор и использовать его в последующих запросах.

Другая вещь, которую вы можете изменить, заключается в том, что вы можете установить таймаут, кроме токена отмены в цикле, если вы хотите получить тайм-аут.

Я старался быть таким же чистым, насколько мне нужно, но этот код может быть чище. Этот метод может стать async, а токен и маркер отмены переданы.

Ответ 5

Я также искал способ прекратить чтение с консоли в определенных условиях. Решение, которое я придумал, состояло в том, чтобы сделать неблокирующую версию строки чтения этими двумя методами.

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;
}

это позволяет нам иметь ReadLine в отдельном потоке, и мы можем ждать его или использовать его в других местах условно.

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. 
}

Ответ 6

Отказ от ответственности: это просто копия & вставить ответ.

Спасибо Жеральду Барре за то, что он предоставил такое замечательное решение:
https://www.meziantou.net/cancelling-console-read.htm

Документация для CancelIoEX:
https://docs.microsoft.com/en-us/windows/win32/fileio/cancelioex-func

Я протестировал его на Windows 10. Он отлично работает и менее "хакерский", чем другие решения (например, переопределение Console.ReadLine, отправка возврата через PostMessage или закрытие дескриптора, как в принятом ответе)

Если сайт закрывается, я привожу здесь фрагмент кода:

class Program
{
    const int STD_INPUT_HANDLE = -10;

    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern IntPtr GetStdHandle(int nStdHandle);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool CancelIoEx(IntPtr handle, IntPtr lpOverlapped);

    static void Main(string[] args)
    {
        // Start the timeout
        var read = false;
        Task.Delay(10000).ContinueWith(_ =>
        {
            if (!read)
            {
                // Timeout => cancel the console read
                var handle = GetStdHandle(STD_INPUT_HANDLE);
                CancelIoEx(handle, IntPtr.Zero);
            }
        });

        try
        {
            // Start reading from the console
            Console.WriteLine("Do you want to continue [Y/n] (10 seconds remaining):");
            var key = Console.ReadKey();
            read = true;
            Console.WriteLine("Key read");
        }
        // Handle the exception when the operation is canceled
        catch (InvalidOperationException)
        {
            Console.WriteLine("Operation canceled");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Operation canceled");
        }
    }
}

Ответ 7

Это измененная версия ответа Contango. Вместо использования текущего процесса MainWindowhandle этот код использует GetForegroundWindow(), чтобы получить консоль MainWindowHandle, если она запущена из cmd.

using System;
using System.Runtime.InteropServices;

public class Temp
{
    //Just need this
    //==============================
    static IntPtr ConsoleWindowHnd = GetForegroundWindow();
    [DllImport("user32.dll")]
    static extern IntPtr GetForegroundWindow();
    [DllImport("User32.Dll")]
    private static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam);
    const int VK_RETURN = 0x0D;
    const int WM_KEYDOWN = 0x100;
    //==============================

    public static void Main(string[] args)
    {
        System.Threading.Tasks.Task.Run(() =>
        {
            System.Threading.Thread.Sleep(2000);

            //And use like this
            //===================================================
            PostMessage(ConsoleWindowHnd, WM_KEYDOWN, VK_RETURN, 0);
            //===================================================

        });
        Console.WriteLine("Waiting");
        Console.ReadLine();
        Console.WriteLine("Waiting Done");
        Console.Write("Press any key to continue . . .");
        Console.ReadKey();
    }
}

Дополнительно

Проверьте, было ли окно переднего плана cmd. Если это не так, то текущий процесс должен запустить консольное окно, чтобы продолжить и использовать его. Это не имеет значения, потому что окно переднего плана должно быть текущим окном процесса в любом случае, но это помогает вам чувствовать себя хорошо, дважды проверяя.

    int id;
    GetWindowThreadProcessId(ConsoleWindowHnd, out id);
    if (System.Diagnostics.Process.GetProcessById(id).ProcessName != "cmd")
    {
        ConsoleWindowHnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
    }

Ответ 8

Это обработает Ctrl + C в отдельном потоке, пока ваше приложение ожидает Console.Readline():

Console.CancelKeyPress += (_, e) =>
{
    e.Cancel = true;
    Environment.Exit(0);
};

Ответ 9

Я просто наткнулся на эту маленькую библиотеку на GitHub: https://github.com/tonerdo/readline

ReadLine - это библиотека GNU Readline, подобная библиотеке, построенной на чистом С#. Он может служить заменой встроенного Console.ReadLine() и приносит с собой некоторые преимущества терминала, которые вы получаете из оболочек Unix, такие как навигация по истории команд и автоматическое завершение вкладок.

Он кроссплатформенный и работает везде, где поддерживается .NET, нацеливание на netstandard1.3 означает, что его можно использовать как с .NET Core, так и с полной .NET Framework.

Хотя эта библиотека не поддерживает прерывание ввода во время написания, обновление должно быть тривиальным, чтобы сделать это. В качестве альтернативы, это может быть интересным примером написания собственного решения, чтобы противостоять ограничениям Console.ReadLine.