microsoft/terminal

Enter key buffered when using raw input and getchar()

Closed this issue · 5 comments

Environment

Microsoft Windows [Version 10.0.19041.746]
Windows Terminal Version: 1.4.3243.0
Microsoft Visual Studio Community 2019 Version 16.8.3

Steps to reproduce

  1. Compile this C program and run it
  2. Type 'h', 'e', 'l', 'l', 'o', enter, 'c', 'a', 't', 'Q'
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

static HANDLE hStdin = NULL;
static HANDLE hStdout = NULL;
static int savedConsoleOutputModeIsValid = 0;
static DWORD savedConsoleOutputMode = 0;
static int savedConsoleInputModeIsValid = 0;
static DWORD savedConsoleInputMode = 0;

static void restoreConsoleState(void)
{
    printf("\x1b[0m");
    fflush(stdout);

    if (savedConsoleOutputModeIsValid)
        SetConsoleMode(hStdout, savedConsoleOutputMode);
    if (savedConsoleInputModeIsValid)
        SetConsoleMode(hStdin, savedConsoleInputMode);
}

int setConsoleRaw(void)
{
    // Make sure the console state will be returned to its original state
    // when this program ends
    atexit(restoreConsoleState);

    // Get handles for stdin and stdout
    hStdin = GetStdHandle(STD_INPUT_HANDLE);
    if (hStdin == INVALID_HANDLE_VALUE || hStdin == NULL)
        return 1;
    hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
    if (hStdout == INVALID_HANDLE_VALUE || hStdout == NULL)
        return 1;

    // Set console to "raw" mode

    if (!GetConsoleMode(hStdout, &savedConsoleOutputMode))
        return 1;
    savedConsoleOutputModeIsValid = 1;
    DWORD newOutputMode = savedConsoleOutputMode;
    newOutputMode |= ENABLE_PROCESSED_OUTPUT;
    newOutputMode &= ~ENABLE_WRAP_AT_EOL_OUTPUT;
    newOutputMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
    if (!SetConsoleMode(hStdout, newOutputMode))
        return 1;
    if (!GetConsoleMode(hStdin, &savedConsoleInputMode))
        return 1;
    savedConsoleInputModeIsValid = 1;
    DWORD newInputMode = savedConsoleInputMode;
    newInputMode &= ~ENABLE_ECHO_INPUT;
    newInputMode &= ~ENABLE_LINE_INPUT;
    newInputMode &= ~ENABLE_PROCESSED_INPUT;
    newInputMode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
    if (!SetConsoleMode(hStdin, newInputMode))
        return 1;

    return 0;
}

int mygetchar(void)
{
#if 1
    return getchar();
#elif 0
    char c;
    if (!scanf_s("%c", &c, 1))
    {
        return EOF;
    }
    return c;
#else
    char c;
    DWORD numRead;
    if (!ReadConsoleA(hStdin, &c, 1, &numRead, NULL) || numRead < 1)
    {
        return EOF;
    }
    return (int)c;
#endif
}

int main(void)
{
    setConsoleRaw();

    int c;
    do
    {
        c = mygetchar();

        printf("%02x", c);
        if (isprint(c))
            printf(" (%c)", (char)c);

        printf("\n");

    } while (c != 'Q');

    return 0;
}

Expected behavior

Immediately after each key press, the hex value received and (if printable) the corresponding character should be written to the terminal on its own line. Program ends after 'Q' is typed and written.

Actual behavior

Output is as expected until enter key is pressed:
enter pressed: no output
'c' pressed: "0d" is output
'a' pressed: we get 2 lines of output: "63 (c)" then "61 (a)"
... and then output is as expected again

Note

The problem also occurs if using scanf_s(), but does not occur if using ReadConsoleA().

Sorry, mind breaking down the behaviors for me?

API You get 0x0D immediately when you press enter
ReadConsoleA
getchar ✔️
scanf

Happy to. Sorry, I made a mistake in my note above - I've corrected it.

  API           You get 0x0D immediately when you press enter
 ------------   ---------------------------------------------
 ReadConsoleA   YES
 getchar        NO
 scanf_s        NO

Using getchar and scanf_s:
when I press enter, my program doesn't get anything
when I press another key (say 'c') following enter, my program gets 0x0d
when I press another key (say 'a') following that, my program gets both 'c' and 'a'
Using ReadConsoleA, my program gets each character when it is typed, as I expect

I should also have mentioned that this happens both in conhost and Windows Terminal.

Thanks!

You know, I'm not sure what the answer is here. Looking through the sources for getchar (and in a way, scanf), it looks like it uses buffered I/O against the console handle. It's got some special handling for CR/LF, but...

The annoying thing here is that I can't help. The console machinery (ReadConsoleA, ReadConsoleInput, ReadFile) are working fine. There's an impedance mismatch between the console mode and the CRT's expectations.

The CRT looks like it sets the console mode itself when you call getch (different but identical-sounding function, eh)

Any ideas? I couldn't immediately find any APIs that make this more reliable.

(scanf operates on entire strings, so I'm not shocked that it wants a completed input line before it parses.)

Thanks for looking into this and trying to help. I will probably use ReadConsoleInput() rather than getchar() for my current project. I reported this just to make sure you know about it.

the-more-you-know

Thanks for letting us know! I think, we all learned something today 😄