Cómo interrumpir Console.ReadLine

¿Es posible detener Console.ReadLine() programáticamente?

Tengo una aplicación de consola: la mayor parte de la lógica se ejecuta en un hilo diferente y en el hilo principal acepto la entrada usando Console.ReadLine() . Me gustaría dejar de leer desde la consola cuando el hilo separado deje de ejecutarse.

¿Cómo puedo conseguir esto?

ACTUALIZACIÓN: esta técnica ya no es confiable en Windows 10. No la use, por favor.
Cambios de implementación bastante pesados ​​en Win10 para hacer que una consola actúe más como una terminal. Sin duda para ayudar en el nuevo subsistema Linux. Un efecto secundario (¿no intencionado?) Es que CloseHandle () se bloquea hasta que se completa una lectura, lo que elimina este enfoque. Dejaré la publicación original en su lugar, solo porque podría ayudar a alguien a encontrar una alternativa.


Es posible, tienes que sacudir la alfombra del piso cerrando la stream stdin. Este progtwig demuestra la 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); } } 

Enviar [enter] a la aplicación de la consola que se está ejecutando actualmente:

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

Este código envía [enter] al proceso actual de la consola, cancelando cualquier llamada a ReadLine () que bloquea el código no administrado en las profundidades del kernel de Windows, lo que permite que la cadena C # salga naturalmente.

Usé este código en lugar de la respuesta que implica cerrar la consola, porque cerrar la consola significa que ReadLine () y ReadKey () están permanentemente deshabilitados desde ese momento en el código (arrojará una excepción si se usa).

Esta respuesta es superior a todas las soluciones que incluyen SendKeys y Windows Input Simulator , ya que funciona incluso si la aplicación actual no tiene el foco.

Necesitaba una solución que funcionara con Mono, por lo que no llama a API. Estoy publicando esto simplemente porque alguien más está en la misma situación, o quiere una forma C # pura de hacer esto. La función CreateKeyInfoFromInt () es la parte difícil (algunas teclas tienen más de un byte de longitud). En el código siguiente, ReadKey () arroja una excepción si se llama a ReadKeyReset () desde otro subproceso. El código siguiente no está del todo completo, pero sí demuestra el concepto de utilizar las funciones existentes de Console C # para crear una función GetKey () intercalable.

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

La respuesta aceptada actual ya no funciona, así que decidí crear una nueva. La única forma segura de hacerlo es crear su propio método ReadLine Puedo pensar en muchos escenarios que requieren dicha funcionalidad y el código aquí implementa uno de ellos:

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

Coloque esta función en algún lugar de su código y esto le da una función que puede cancelarse mediante un CancellationToken también para un mejor código que he usado

 using Con = System.Console; 

Esta función devuelve una cadena vacía al cancelar (lo que era bueno para mi caso). Si lo desea, puede lanzar una excepción dentro de la expresión de catch marcada.

También en la misma expresión catch puedes eliminar el stringBuilder.Clear(); línea y que hará que el código devuelva lo que el usuario ingresó hasta ahora. Combine esto con una bandera exitosa o cancelada y puede guardar lo que utilizó ingresado hasta el momento y usarlo en futuras solicitudes.

Otra cosa que puede cambiar es que puede establecer un tiempo de espera además del token de cancelación en el ciclo si desea obtener una funcionalidad de tiempo de espera excedido.

Traté de estar todo lo limpio que necesito, pero este código puede ser más limpio. El método puede volverse async y se puede pasar el token de tiempo de espera y cancelación.

Esta es una versión modificada de la respuesta de Contango. En lugar de utilizar MainWindowhandle del proceso actual, este código usa GetForegroundWindow () para obtener MainWindowHandle de la consola si se inicia desde 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(); } } 

Opcional

Compruebe si la ventana de primer plano fue cmd. Si no fuera así, entonces el proceso actual debería iniciar la ventana de la consola, así que adelante y use eso. Esto no debería importar porque la ventana de primer plano debería ser la ventana de proceso actual de todos modos, pero esto lo ayuda a sentirse bien con una doble verificación.

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