I want to read a line from stdin with timeout on Windows. The following code uses WaitForSingleObject without success. Creating a blocking thread seems only transfer the problem to canceling the thread. Is there a way to read a line with timeout, other than peeking from time to time?

#include <assert.h>
#include <stdio.h>
#include <windows.h>
#define TIMEOUT_MS 5000
static void PrintByteArray(const void *b, size_t len) { 
    const unsigned char *u = (const unsigned char *)b;
    printf("len %zu [", len);
    for (size_t i = 0; i < len; ++i) {
        printf(" %02x", u[i]);
    }
    puts(" ]");
}
int main() {
    const HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
    assert(hStdIn != INVALID_HANDLE_VALUE);
    DWORD r;
    BOOL ok = GetConsoleMode(hStdIn, &r);
    assert(ok);
    printf("ConsoleMode %lx\n", r); // mode is 1f7, flag ENABLE_LINE_INPUT 0x0002 is set
    
    r = WaitForSingleObject(hStdIn, TIMEOUT_MS);
    switch (r) {
    case WAIT_OBJECT_0: {
        puts("WAIT_OBJECT_0");
        char buf[8];
        DWORD readLen;
        BOOL ok = ReadConsoleA(hStdIn, buf, sizeof(buf), &readLen, NULL);
        if (!ok) {
            puts("ReadConsole fail");
            return 1;
        }
        PrintByteArray(buf, readLen);
        break;
    }
    case WAIT_TIMEOUT:
        puts("Timeout");
        break;
    default:
        printf("WaitForSingleObject err: %lu\n", r);
        return 1;
    }
    return 0;
}

Using Windows Terminal with cmd: WaitForSingleObject returns WAIT_OBJECT_0 immediately when there is no input, then ReadConsole blocks forever.

Using conhost with cmd: if there is no input, WaitForSingleObject timeouts correctly. If there is input, WaitForSingleObject returns WAIT_OBJECT_0 as soon as one character is available, rather than one line. After WaitForSingleObject returns WAIT_OBJECT_0, ReadConsole blocks forever.

Expected behavior: if there is a line within timeout, return the line; if there isn't a line within timeout, return timeout.