tomsmeding
tomsmeding

Reputation: 926

select(2) and ioctl(2) returning 0 while stdin has data

I'm trying to detect whether there is data on stdin for me to read.

Specifically, I've turned off canonical mode using tcsetattr, so I can read one character at a time (blocking). I want to detect escape sequences like those produced by the arrow keys, but distinguish those from a lone escape key. Additionally, I want to know quickly which was entered; I'm assuming my terminal is reasonably quick and the rest of an escape sequence after the ^[ follows reasonably quickly.

Suppose I already know (e.g. using select(2)) that there's something to read on stdin. I read a character using getchar(), and it's a ^[, ASCII 27. Now I want to know whether there is more coming within a certain time interval, or this escape character is everything (which would indicate a hit of the escape key). Using select(2) for this doesn't seem to work, since (I noticed, and read elsewhere) the rest of the characters are already buffered, so select(2) has nothing to detect anymore. So I turned to ioctl(2) using FIONREAD, but that doesn't seem to work either.

Minimal (non-)working example:

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <sys/select.h>
#include <sys/ioctl.h>
#include <assert.h>

struct termios tios_bak;

void initkeyboard(void){
    struct termios tios;
    tcgetattr(0,&tios_bak);
    tios=tios_bak;

    tios.c_lflag&=~ICANON;
    tios.c_cc[VMIN]=1; // Read one char at a time
    tios.c_cc[VTIME]=0; // No timeout on reading, make it a blocking read

    tcsetattr(0,TCSAFLUSH,&tios);
}

void endkeyboard(void){
    tcsetattr(0,TCSAFLUSH,&tios_bak);
}

int main(void){
    initkeyboard();
    atexit(endkeyboard);

    printf("Press an arrow key or the escape key, or the escape key followed by something else.\n");

    char c=getchar();
    if(c!=27){
        printf("Please input an escape sequence or key\n");
        exit(1);
    }

    // Now we use select(2) to determine whether there's anything more to read.
    // If it was a lone escape key, there won't be anything new in a while.
    fd_set rdset;
    FD_ZERO(&rdset);
    FD_SET(0,&rdset);
    struct timeval tv;
    tv.tv_sec=1; // Here we wait one second; this is just to illustrate. In a real environment
    tv.tv_usec=0; // I'd wait something like 100ms, since that's reasonable for a terminal.
    int ret=select(1,&rdset,NULL,NULL,&tv);

    assert(ret!=-1); // (Error checking basically omitted)
    if(ret==0){
        printf("select(2) returned 0.\n");
        int n;
        assert(ioctl(0,FIONREAD,&n)>=0);
        assert(n>=0);
        if(n==0){
            printf("ioctl(2) gave 0; nothing to read: lone escape key\n");
            // INSERT printf("%c\n",getchar()); HERE TO DEMONSTRATE THIS IS WRONG IN CASE OF ESCAPE SEQUENCE
        } else {
            c=getchar();
            printf("ioctl(2) says %d bytes in read buffer (first char=%c)\n",n,c);
        }
    } else {
        c=getchar();
        printf("select(2) returned %d: there was more to read (first char=%c)\n",ret,c);
    }
}

Sorry for the long code. What happens is the following:

  1. When you just press the escape key, it successfully detects that.
  2. When you press the escape key and then quickly some other key (like 'a'), the code successfully detects there's more to read, which is fine; specific detection of escape sequences is out of scope.
  3. When you generate an escape sequence, e.g. by pressing an arrow key, both select(2) and ioctl(2) return there's nothing to read, while clearly there is; this can be easily checked by inserting printf("%c\n",getchar()); at the indicated location. That will print [ (in the case of an arrow key, at least).

Question: how to correctly detect input in case (3)?

Upvotes: 5

Views: 1551

Answers (1)

David Ranieri
David Ranieri

Reputation: 41046

From the getchar man page:

It is not advisable to mix calls to input functions from the stdio library with low-level calls to read(2) for the file descriptor associated with the input stream; the results will be undefined and very probably not what you want.

Do not mix buffered and unbuffered input functions.

select must be used in combination with

read(fileno(stdin), &c, 1);

instead of

c = getchar();

Upvotes: 5

Related Questions