如何中断Console.ReadLine

是否可以通过编程方式停止Console.ReadLine()

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

我怎样才能做到这一点?

更新:此技术在Windows 10上不再可靠。请不要使用它。
Win10中相当繁重的实现更改使控制台更像终端。 毫无疑问,协助新的Linux子系统。 一个(非预期的?)副作用是CloseHandle()死锁,直到读完成,杀死这个方法死了。 我会留下原来的post,只是因为它可能有助于某人找到替代方案。


有可能,你必须通过关闭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); } } 

将[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()(如果使用它将引发exception)。

这个答案优于所有涉及SendKeys和Windows输入模拟器的解决方案,因为它即使当前的应用程序没有焦点也能正常工作。

我需要一个可以与Mono一起使用的解决方案,所以没有API调用。 我发布这个只是包装其他任何人处于相同的情况,或者想要一个纯粹的C#方式来做到这一点。 CreateKeyInfoFromInt()函数是棘手的部分(一些键的长度超过一个字节)。 在下面的代码中,如果从另一个线程调用ReadKeyReset(),则ReadKey()会抛出exception。 下面的代码并不完全完整,但它确实展示了使用现有的Console C#函数创建可插入的GetKey()函数的概念。

 static ManualResetEvent resetEvent = new ManualResetEvent(true); ///  /// Resets the ReadKey function from another thread. ///  public static void ReadKeyReset() { resetEvent.Set(); } ///  /// Reads a key from stdin ///  /// The ConsoleKeyInfo for the pressed key. /// Intercept the key 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; } 

目前接受的答案不再适用,所以我决定创建一个新答案。 唯一安全的方法是创建自己的ReadLine方法我可以想到许多需要这种function的场景,这里的代码实现了其中一个:

 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表达式中抛出一个exception。

同样在catch表达式中,您可以删除stringBuilder.Clear(); line和这将导致代码返回到目前为止用户输入的内容。 将此与成功或取消的标志相结合,您可以保留到目前为止输入的内容并在进一步的请求中使用它。

您可以更改的其他事项是,如果要获取超时function,可以在循环中设置除取消令牌之外的超时。

我试着尽可能干净,但这段代码可以更干净。 该方法可以变为async本身,并传入超时和取消令牌。

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