How to interrupt Console.ReadLine

后端 未结 10 713
情歌与酒
情歌与酒 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:48

    Disclaimer: This is just a copy & paste answer.

    Thanks to Gérald Barré for providing such a great solution:
    https://www.meziantou.net/cancelling-console-read.htm

    Documentation for CancelIoEX:
    https://docs.microsoft.com/en-us/windows/win32/fileio/cancelioex-func

    I tested it on Windows 10. It works great and is way less "hacky" than the other solutions (like reimplementing Console.ReadLine, sending return via PostMessage or closing the handle as in the accepted answer)

    In case the site goes down I cite the code snippet here:

    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");
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-28 11:54

    Send [enter] to the currently running console app:

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

    This code sends [enter] into the current console process, aborting any ReadLine() calls blocking in unmanaged code deep within the windows kernel, which allows the C# thread to exit naturally.

    I used this code instead of the answer that involves closing the console, because closing the console means that ReadLine() and ReadKey() are permanently disabled from that point on in the code (it will throw an exception if its used).

    This answer is superior to all solutions that involve SendKeys and Windows Input Simulator, as it works even if the current app does not have the focus.

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

    I needed a solution that would work with Mono, so no API calls. I'm posting this just encase anyone else is in the same situation, or wants a pure C# way of doing this. The CreateKeyInfoFromInt() function is the tricky part (some keys are more than one byte in length). In the code below, ReadKey() throws an exception if ReadKeyReset() is called from another thread. The code below is not entirely complete, but it does demonstrate the concept of using existing Console C# functions to create an interuptable GetKey() function.

    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;
    }
    
    0 讨论(0)
  • 2020-11-28 11:59

    This is a modified version of Contango's answer. Instead of using the current process's MainWindowhandle, this code uses GetForegroundWindow() to get the Console's MainWindowHandle if launched from 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();
        }
    }
    

    Optional

    Check to see if the foreground window was cmd. If it wasn't, then the current process should launch the console window so go ahead and use that. This shouldn't matter because the foreground window should be the current process window anyway, but this helps you feel good about it by double checking.

        int id;
        GetWindowThreadProcessId(ConsoleWindowHnd, out id);
        if (System.Diagnostics.Process.GetProcessById(id).ProcessName != "cmd")
        {
            ConsoleWindowHnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
        }
    
    0 讨论(0)
提交回复
热议问题