0

TL; DR:

I need to retrieve the INPUT_RECORDs* from the console input buffer in real time without preventing a parallel/the main thread from reading them and printing the characters to the console screen buffer.

All I manage to achieve is to either retrieve the INPUT_RECORDs using PeekConsoleInput (or ReadConsoleInput) OR have the main thread handle reading/printing the typed characters to the screen.

How can I reliably peek at the console input buffer and retrieve the INPUT_RECORDs while letting the main thread handle printing the typed characters to the screen?

* preferably only KEY_EVENT_RECORDs


I can use the following two lines to have my console application print to the screen whatever the user types on the keyboard (including the deletion of characters in case of backspace or delete as well as inserting characters by use of the arrow keys):

let mut line = String::new();
std::io::stdin().read_line(&mut line).unwrap();

console_input

I would like to keep that behavior while retrieving the INPUT_RECORDs from the console input buffer.**

I tried using PeekConsoleInput in a background thread and keep the above code in the main thread but the main thread seems to be faster in taking the INPUT_RECORDs out of the input buffer leaving no records for the background thread to peek at.

The attached code shows how running PeekConsoleInput in a background thread does successfully peek the INPUT_RECORDs as long as no other thread removes them (flush = true vs flush = false).

How can I reliably peek at the console input buffer and retrieve the INPUT_RECORDs while letting the main thread handle printing the typed characters to the screen?

** I can then send those INPUT_RECORDs to child processes and using WriteConsoleInput replicate the users input on multiple consoles simultaneously.


Cargo.toml

[dependencies]
tokio = {version = "1.27.0", features = ["macros", "rt-multi-thread", "time"]}

[dependencies.windows]
version = "0.44.0"
features = [
    "Win32_Foundation",
    "Win32_System_Console",
]

main.rs

use std::time::Duration;

use windows::Win32::System::Console::{
    FlushConsoleInputBuffer, GetStdHandle, PeekConsoleInputW, INPUT_RECORD, STD_INPUT_HANDLE,
};

async fn peek_console_input(nb_of_records_to_read: u32, flush: bool) {
    const NB_EVENTS: usize = 2;
    let mut nb_of_records_read = 0;
    let console_handle = unsafe { GetStdHandle(STD_INPUT_HANDLE).unwrap() };
    loop {
        let mut input_buffer: [INPUT_RECORD; NB_EVENTS] = [INPUT_RECORD::default(); NB_EVENTS];
        let mut number_of_events_read = 0;
        unsafe {
            PeekConsoleInputW(
                console_handle,
                &mut input_buffer,
                &mut number_of_events_read,
            )
            .expect("Failed to read console input");
        }
        if number_of_events_read > 0 as u32 {
            if flush {
                unsafe {
                    FlushConsoleInputBuffer(console_handle);
                };
            }
            nb_of_records_read += number_of_events_read;
            for record in input_buffer {
                let event = unsafe { record.Event.KeyEvent };
                println!(
                    "key_down: {}, unicode_char: {}",
                    event.bKeyDown.as_bool(),
                    unsafe { event.uChar.UnicodeChar }
                );
            }
        }
        if nb_of_records_read >= nb_of_records_to_read {
            break;
        }
    }
}

#[tokio::main]
async fn main() {
    let nb_of_records_to_read = 8;
    for flush in [true, false] {
        println!(
            "Spawning a thread peeking at the console input buffer \
            for the next {nb_of_records_to_read} input records \
            (key down and up count as 1 record each)"
        );
        let join_handle = tokio::spawn(peek_console_input(nb_of_records_to_read, flush));
        if flush {
            println!(
                "Putting main thread to sleep for 2 milliseconds at a time \
                while waiting for the background thread to finish."
            );
            println!("Please press any combination of keys and behold the output");
            while !join_handle.is_finished() {
                tokio::time::sleep(Duration::from_millis(2)).await;
            }
        } else {
            println!("Main thread is reading a line from stdin, please start pressing keys");
            let mut line = String::new();
            std::io::stdin().read_line(&mut line).unwrap();
            println!("read line: {}", line);
        }
        join_handle.abort();
        join_handle.await.unwrap();
    }
}
whme
  • 4,908
  • 5
  • 15
  • 28
  • 1
    This approach is doomed to failure as there's no way to synchronize the console's input buffer between the threads. You might try hooking ReadConsoleInput() and posting the events to a worker thread instead. – Luke Apr 01 '23 at 17:46
  • Thanks for sharing your insights! Do you mean using LD_PRELOAD[1]to replace ReadConsoleInput with my own function that sends the events to a worker before returning? Does something like `std::io::stdin().read_line()` call that function under the hood? [1]: https://stackoverflow.com/questions/426230/what-is-the-ld-preload-trick – whme Apr 01 '23 at 18:04
  • Why not simply use a `SetWindowsHookEx(WH_KEYBOARD)` hook to get the keystokes before the console delivers them to `ReadConsoleInput()`? – Remy Lebeau Apr 01 '23 at 18:21
  • I sort of tried that[1] but got deterred by the required GetMessage/DispatchMessage loop. I'll definitely give it another try [1]: https://stackoverflow.com/questions/75870904/how-to-correctly-set-a-wh-keyboard-hook-procedure-using-setwindowshookexw-in-rus – whme Apr 01 '23 at 18:46

0 Answers0