cunixtty

Why is getchar() hanging despite poll() returning a "good" value?


I am making a text editor in C, and using the poll() function to determine if an input is ready (if not, it continues with other functionalities not shown in this example. Then, if a character is available (i.e. the if (!poll(...)) condition being false), it is collected with getchar() and processed. Here is the code:

#include <poll.h>
#include <stdio.h>

int main(void) {

        input_set_tty_raw();
        
        char input_history[1000];
        int curr = 0;
        struct pollfd in = {.fd = 0, .events = POLLIN};
        
        while (1) {
                if (!poll(&in, 1, 20)) {  // arbitrary timeout for poll function
                        continue;
                }
                char c = getchar();

                /* problem code */
                if (input_history[curr - 1] == '\033' && c == '[') {
                        while (poll(&in, 1, 0)) {
                                getchar();
                        }
                        return input_restore_tty();
                }

                // handle inputs
                input_history[curr++] = c;
        }

        return input_restore_tty();
}

In case you are wondering, the input_set_tty_raw() is equivalent to running $ stty raw, allowing for pure (or raw) handling of inputs, character by character. The input_restore_tty function returns the terminal back to the original state.

A problem arises with special "escape sequences" that are passed into stdin when keys such as the arrow keys are pressed. For example, when pressing an arrow key, a sequence of bytes 27-91-6x are passed in. You can use this small utility to check out this behavior:

#include <stdio.h>

int main(void) {
        input_set_tty_raw();
        while (1) {
                char c = getchar();
                if (c == 'q') {
                        return input_restore_tty();
                }
                printf("%d\n\r", c);
        }
}

The aforementioned 27-91-6x behavior seems to share the 27-91 header (which is a "\033[") with other escape sequences, such as hitting the delete (not backspace) key on a keyboard entering 27-91-51-126.

Therefore, in the second conditional labeled problem code, I check if the previous character entered was this \033 escape character and if the current character is the 91 (or [) character. If so, I let all the other inputs pass using the while statement.

From testing, it seems like the delay between the \033 and the [ is non-zero, while the remainder of characters in the escape sequence seem to be entered instantly, hence the inner poll having a timeout of 0. I have put a return statement inside the conditional to show exactly when all the inputs are handled.

However, this code seems to hang. Compiling and running this, you can see that the code does not return when you press an arrow key, as expected. However, it returns when you press another key after that, indicating that the program is hanging in the getchar() function inside the problem code block.

To my knowledge, poll() only returned a non-zero value when there was input available. So, why is it hanging, and how can I fix this?

If needed, the helper functions are below, with their required include.

#include <termios.h>

static struct termios tbufsave;
int input_set_tty_raw(void) {
        struct termios tbuf;
        if (tcgetattr(0, &tbufsave) == -1) {
                return 1;
        }

        tbuf = tbufsave;
        tbuf.c_iflag &= ~(INLCR | ICRNL | ISTRIP | IXON | BRKINT);
        tbuf.c_oflag &= ~OPOST;
        tbuf.c_lflag &= ~(ICANON | ISIG | ECHO);
        tbuf.c_cc[VMIN] = 1;
        tbuf.c_cc[VTIME] = 0;

        return tcsetattr(0, TCSANOW, &tbuf) == -1;   // 1 for error
}

int input_restore_tty(void) {
        return tcsetattr(0, TCSANOW, &tbufsave) == -1;   // 1 for error
}

Solution

  • Transferring a comment into an answer.

    Yes, using input_set_tty_raw() means that the terminal driver returns characters immediately, but you also need to let the standard I/O package know that it should not buffer characters. You would probably do this using setvbuf().

    Alternatively, use read() rather than standard I/O to collect the data.