如何中断 Console.ReadLine

30

如何在编程时停止Console.ReadLine()?

我有一个控制台应用程序:大部分逻辑在不同的线程上运行,而在主线程中,我使用Console.ReadLine()接受输入。 我想在分离的线程停止运行时停止从控制台读取。

我该如何实现这个功能?


请将另一个正在执行Console.ReadLine的线程中止的方法在.NET中实现。 - dice
好的,这个问题很老了,但自从上次在这里提问和回答以来有一些变化。如果你像我一样遇到了这个问题,请查看下面的答案,可能会对你有所帮助。 - Emad
11个回答

19

更新:这种技术在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
这个功能非常出色。但是,如果我想再次使用Console.ReadLine(),C#会抛出一个错误 - 有没有重新启用Console.ReadLine()的方法? - Contango
2
把它作为一个问题来问,这并不是琐碎的事情。 - Hans Passant
非常感谢您的快速回复。我已经完成了这个操作,请参见https://dev59.com/UmHVa4cB1Zd3GeqPlk81 - Contango
使用WPF和AllocConsole,同时运行Console.ReadLine会导致IOException:"句柄无效"。 - Grault
当然,这就是你知道地垫被抽动了而不使用返回值的方法。所以只需捕获它并离开即可。 - Hans Passant
目前最好的解决方案是使用WinAPI的CancelIoEX,请参见https://www.meziantou.net/cancelling-console-read.htm和我下面的答案。 - wischi

16

向当前正在运行的控制台应用程序发送 [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],中止在Windows内核深处阻塞的任何ReadLine()调用,在C#线程自然退出时允许该操作。我使用了这段代码,而不是关闭控制台的答案,因为关闭控制台意味着从那时起在代码中永久禁用ReadLine()和ReadKey() (如果使用它将抛出异常) 。这个答案比涉及SendKeys和Windows Input Simulator的所有解决方案都更好,因为它即使当前应用程序没有焦点也能正常工作。

我能否使用PostMessage()将文本发送到控制台?如何实现? - einsteinsci
@einsteinsci 如果你将消息转换为一系列单独的按键,那么是可以的。 - Contango
这很好,但如果控制台窗口不是当前进程,则无法工作。我在下面发布了一个修改后的解决方案,如果从cmd启动,它将获取控制台窗口句柄。 - u8it
关于在控制台输入消息的问题,只需使用Write()或WriteLine()或SendKeys.Send(),然后再使用PostMessage()。Postmessage()仅需要捕获回车键。此外,这也可以使用Windows输入模拟器完成,因为它只是一个封装和库,用于PostMessage(),但更容易输入消息。它非常容易安装为nuget:Install-Package InputSimulator。正如最后一句话所建议的那样,在使用InputSimulator时仍需要考虑窗口焦点。 - u8it
3
这在Windows 10和.NET Core中已经不再起作用。虽然PostMessage成功了,但是ReadKey没有被解除阻塞。 - ygoe

12

免责声明:这只是一个复制粘贴的答案。

感谢 Gérald Barré 提供如此出色的解决方案:
https://www.meziantou.net/cancelling-console-read.htm

CancelIoEX 的文档:
https://learn.microsoft.com/en-us/windows/win32/fileio/cancelioex-func

我在 Windows 10 上测试了它。它非常好用,而且比其他解决方案(例如重新实现 Console.ReadLine、通过 PostMessage 发送返回或像被接受的答案中那样关闭句柄)更少“hacky”。

如果网站崩溃,我在这里引用代码片段:

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

1
这很棒。 - Andy
使用此解决方案会中断 Concole.ReadKey(),但对于 Concole.ReadLine() 则会出现 ERROR_NOT_FOUND 错误。 - Mx.Wolf
这个解决方案在Windows 7上会无限期地阻塞。 - user3700562

5

我需要一个能够在Mono上使用的解决方案,因此不能使用API调用。我发帖只是为了防止其他人遇到同样的问题,或者想要纯C#方式来完成这个任务。CreateKeyInfoFromInt()函数是棘手的部分(某些键超过一个字节的长度)。在下面的代码中,如果从另一个线程调用ReadKeyReset(),则ReadKey()将抛出异常。下面的代码并不完全完整,但它确实演示了使用现有的Console C#函数创建可中断的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;
}

为什么不直接将 Console.KeyAvailableConsole.ReadKey 结合起来使用?另一个问题是:相比让 ReadKey 等待下一个输入事件,这 50 毫秒的自旋会占用更多的资源吗? - ygoe
1
正如你所说,这段代码还不完整,但是给了我灵感。因此我点了个赞。也许我完成后会在这里发布我的代码。 - Emad

5

我也在寻找一种在特定条件下停止从控制台读取的方法。我想到的解决方法是使用这两种方法创建一个非阻塞版本的read line。

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

3

当前被接受的答案已经不再适用,因此我决定创建一个新的答案。我所能想到的唯一安全的方法是创建自己的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();这行代码,这将导致代码返回用户输入的内容。结合成功或已取消的标志,您可以保留用户迄今为止输入的内容并在后续请求中使用它。

您还可以更改的另一件事是,如果您想获得超时功能,可以在循环中设置超时时间以外的取消标记。

我尽可能清晰简洁,但这段代码可能可以更简洁。该方法本身可以变成异步方式,并传递超时和取消标记。


3
我知道这个问题很老而且早于.NET Core,但我认为增加一种更现代的方法会很有用。
我已经在.NET 6中测试了这种方法。我创建了一个简单的异步ReadLine方法,它接受一个取消令牌,可以用来中断它。
关键是将 Console.ReadLine() 包装在一个任务中。显然,Console.ReadLine() 调用无法被打断,因此需要使用 Task.WhenAny 来与 Task.Delay 结合使用以使取消令牌生效。
为了不丢失任何输入,读取任务要保持在方法外部,这样如果操作被取消,就可以在下一次调用时等待它。
Task<string?>? readTask = null;

async Task<string?> ReadLineAsync(CancellationToken cancellationToken = default)
{
    readTask ??= Task.Run(() => Console.ReadLine());

    await Task.WhenAny(readTask, Task.Delay(-1, cancellationToken));

    cancellationToken.ThrowIfCancellationRequested();

    string? result = await readTask;
    readTask = null;

    return result;
}

一个调用示例会很好,await ReadLineAsync(token);await Task.Run(() => ReadLineAsync(token)) 都不起作用,因为线程在 Console.In.ReadLineAsync() 上停止... - MoonKillCZ
@MoonKillCZ await ReadLineAsync(token) 应该可以正常工作。在底层,会创建一个新任务来调用输入流的 ReadLine() 方法。Console.In.ReadLineAsync() 应该立即返回此任务而不阻塞线程。如果这样做不起作用,您可以尝试通过 Task.Run(() => Console.ReadLine()) 替换 Console.In.ReadLineAsync() 看看会发生什么。我已经进行了很多测试,所以不知道为什么对您不起作用。您使用的是哪个平台? - RdJNL
Win 10 最新版 + .NET 6.0。当我使用调试器逐步执行代码时,线程会停在 readTask ??= Console.In.ReadLineAsync(); 这一行。无论如何,我通过使用这个 NuGet 包解决了我的问题:ReadLine.Reboot - MoonKillCZ
@MoonKillCZ 可能是在 Console.In 上阻塞了。它是什么类型的应用程序?是常规控制台应用程序还是其他类型的应用程序? - RdJNL
我和 @MoonKillCZ 遇到了同样的问题,我发现将其包装在 Task.Run() 中解决了这个问题(完整代码行:readTask ??= Task.Run(() => Console.In.ReadLineAsync()))。 - glenn223
可能使用readTask ??= Task.Run(() => Console.ReadLine())会更有效率一些,但差别可能很小。我会更新我的答案。 - RdJNL

2

以下是一种在Windows 10上工作且不使用任何高级线程或dllimport技巧的解决方案。我希望它对您有所帮助。

我基本上创建了一个坐在标准输入上的流读取器。以“异步”的方式读取它,如果我想取消readline,则只需处理掉流读取器即可。

以下是我的代码:

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

注意:是的,我已经阅读了async,但我仍在等待任务结果,因此基本上仍在等待用户输入。


1
这是Contango答案的修改版。该代码使用GetForegroundWindow()获取控制台的MainWindowHandle(如果从cmd启动),而不是使用当前进程的MainWindowhandle。
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;
    }

0

我在GitHub上偶然发现了这个小库:https://github.com/tonerdo/readline

ReadLine是一个纯C#编写的GNU Readline类库。它可以作为内置Console.ReadLine()的替代品,并带有一些类似于Unix shell的终端好处,如命令历史记录导航和选项卡自动完成。

它是跨平台的,在任何支持.NET的地方运行,针对netstandard1.3意味着它可以与.NET Core以及完整的.NET Framework一起使用。

尽管该库在编写时不支持中断输入,但更新以实现此功能应该很容易。或者,它可以成为编写自定义解决方案以克服Console.ReadLine限制的有趣示例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接