cconsoletermiosposix-select

select() responds to stdin but not to /dev/tty


This is a very similar question to select() does not seem to work on TTY but I don't appear to be making the same mistake with FD_SET as the OP there.

On my Linux box (running Ubuntu 20.04 LTS for Desktop), I'm trying to read an unbuffered character from the console in both blocking mode (calling fgetc() unconditionally) and non-blocking mode (repeatedly asking select() whether it is worth calling fgetc()).

Both modes work if I'm reading from stdin. However, if I want to explicitly read from the console independent of whether stdin has been redirected, my understanding was that I should open "/dev/tty" instead. This also works in blocking mode, but not in non-blocking mode: select() keeps returning 0 at the end of each timeout despite frantic key-pressing. I'm not sure why stdin and /dev/tty would behave differently in this regard. Their termios flags seem the same.

$ gcc foo.c

$ ./a.out
c_iflag = 25862
c_oflag = 5
c_cflag = 191
c_lflag = 35387

Well?? 
read character 'g' in blocking mode from stdin

We're waiting... 0 0 
read character 'g' in non-blocking mode from stdin

$ ./a.out /dev/tty
c_iflag = 25862
c_oflag = 5
c_cflag = 191
c_lflag = 35387

Well?? 
read character 'g' in blocking mode from /dev/tty

We're waiting... 0 0 0 0 0 0 0 0 0 0 
too slow: read nothing in non-blocking mode from /dev/tty

$ ggggggggggg

If I attempt to use /dev/console or /dev/tty0 I just fail with a null FILE * from fopen(). Here is the source foo.c. What am I missing?

#include <stdio.h>
#include <sys/select.h>
#include <termios.h>

int main( int argc, const char * argv[] )
{
    
    const char * name = "stdin";
    FILE * filePointer = stdin;
    int fileDescriptor;
    struct timeval timeout;
    struct termios term;
    fd_set rdset;
    int result, iCycle, nCycles = 10;
    char c;

    if( argc > 1 ) /* open the user-specified file instead of stdin */
    {
        name = argv[ 1 ];
        filePointer = fopen( name, "r" );
    }
    if( !filePointer ) return fprintf( stderr, "failed to open %s", name );
    
    fileDescriptor = fileno( filePointer );

    /* make things unbuffered, non-canonical, non-echoey */
    setvbuf( filePointer, NULL, _IONBF, 0 );
    tcgetattr( fileDescriptor, &term );
    printf("c_iflag = %d\n", term.c_iflag);
    printf("c_oflag = %d\n", term.c_oflag);
    printf("c_cflag = %d\n", term.c_cflag);
    printf("c_lflag = %d\n", term.c_lflag);
    term.c_lflag &= ~ICANON;
    term.c_lflag &= ~ECHO;
    tcsetattr( fileDescriptor, TCSANOW, &term );
    
    /* get an unbuffered character (blocking mode) */
    fprintf( stdout, "\nWell?? " );
    fflush( stdout );
    c = fgetc( filePointer );
    fprintf( stdout, "\nread character '%c' in blocking mode from %s\n", c, name );
    fflush( stdout );

    /* get an unbuffered character (non-blocking mode) */
    fprintf( stdout, "\nWe're waiting... " );
    fflush( stdout );
    for( iCycle = 0; iCycle < nCycles; iCycle++ )
    {
        FD_ZERO( &rdset );
        FD_SET( fileDescriptor, &rdset );
        timeout.tv_sec  = 1;
        timeout.tv_usec = 0;
        result = select( 1, &rdset, NULL, NULL, &timeout );
        if( result > 0 ) break;
        fprintf( stdout, "%d ", result );
        fflush( stdout );
        if( result > 0 ) break;
    }
    if( iCycle < nCycles )
    {
        c = fgetc( filePointer );
        fprintf( stdout, "\nread character '%c' in non-blocking mode from %s\n", c, name );
        fflush( stdout );
    }
    else
    {
        fprintf( stdout, "\ntoo slow: read nothing in non-blocking mode from %s\n", name );
        fflush( stdout );
    }
    
    /* restore canonicality (avoid having to type `reset` after every run) */
    term.c_lflag |= ICANON;
    term.c_lflag |= ECHO;
    tcsetattr( fileDescriptor, TCSANOW, &term );
    if( filePointer != stdin ) fclose( filePointer );
    return 0;
}

Solution

  • The problem is the first argument to select(). select() only checks the first nfds descriptors in each FD set. Since you specified 1, it only checks for input on FD 0, and ignored the FD used for /dev/tty.

    Change it to:

    select(fileDescriptor+1, &rdset, NULL, NULL, &timeout );