Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making stdin non-blocking

Tags:

c

I have an exercise where I am required to print a file slowly (1 second intervals) until the file ends, unless the user types a character.

So far, the program outputs the file in one second intervals which is great, but when I type a character, nothing happens. My guess is that I am using select wrong somehow.

This is the final program I ended up submitting.

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    FILE* infile;
    char str[100];
    fd_set readset;
    struct timeval tv;

    // open a file
    if((infile = fopen("infile", "r")) == NULL)
    {
        (void)printf("Couldn't open the file\n");
        exit(1);
    }
    // file was opened successfully
    else
    {       
        // while we are not at the end of a file
        while(fgets(str, 100, infile) != NULL)
        {
            FD_ZERO(&readset);
            FD_SET(fileno(stdin), &readset);
            // set the time value to 1 second
            tv.tv_sec = 1;
            tv.tv_usec = 0;
            select(fileno(infile)+1, &readset, NULL, NULL, &tv);
            // the user typed a character so exit
            if(FD_ISSET(fileno(stdin), &readset))
            {
                fclose(infile);
                exit(0);
            }
            // the user didn't type a character so print the next line
            else
            {
                fgets(str, 100, stdin);
                puts(str);
            }
        }

        // clean up
        fclose(infile);
    }

    // report success
    return 0;
}

Thanks for the help!

like image 218
please delete me Avatar asked Nov 11 '11 23:11

please delete me


2 Answers

This is a working version, using tcgetattr/tcsetattr:

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>

int main(void) {
    FILE* infile;
    char str[100];
    fd_set readset;
    struct timeval tv;
    struct termios ttystate, ttysave;

    // open a file
    if((infile = fopen("infile", "r")) == NULL)
    {
        (void)printf("Couldn't open the file\n");
        exit(1);
    }
    // file was opened successfully

    //get the terminal state
    tcgetattr(STDIN_FILENO, &ttystate);
    ttysave = ttystate;
    //turn off canonical mode and echo
    ttystate.c_lflag &= ~(ICANON | ECHO);
    //minimum of number input read.
    ttystate.c_cc[VMIN] = 1;

    //set the terminal attributes.
    tcsetattr(STDIN_FILENO, TCSANOW, &ttystate);

    // while we are not at the end of a file
    while(fgets (str, 100, infile))
    {
        // set the time value to 1 second
        tv.tv_sec = 1;
        tv.tv_usec = 0;

        FD_ZERO(&readset);
        FD_SET(fileno(stdin), &readset);

        select(fileno(stdin)+1, &readset, NULL, NULL, &tv);
        // the user typed a character so exit
        if(FD_ISSET(fileno(stdin), &readset))
        {
            fgetc (stdin); // discard character
            break;
        }
        // the user didn't type a character so print the next line
        else
        {
            puts(str);
            // not needed: sleep(1);
        }
    }

    // clean up
    fclose(infile);

    ttystate.c_lflag |= ICANON | ECHO;
    //set the terminal attributes.
    tcsetattr(STDIN_FILENO, TCSANOW, &ttysave);
    // report success
    return 0;
}

The sleep(1); isn't needed anymore.

like image 141
ott-- Avatar answered Nov 08 '22 01:11

ott--


The terminal is buffering lines. It doesn't send text to the program until you press the Enter key. There might be a way to disable terminal line buffering, but I imagine it is beyond the scope of your assignment.

It stops when you press Enter. However, it doesn't quit immediately. That's something you'll want to fix. Get rid of that sleep(1).

Now your program spams text! You gave select a timeout of one second, didn't you?

// set the time value to 1 second
tv.tv_sec = 1;
tv.tv_usec = 0;

The reason the timeout doesn't stick is because select is modifying the timeout value. From the man page:

On Linux, select() modifies timeout to reflect the amount of time not slept; most other implementations do not do this. (POSIX.1-2001 permits either behavior.) This causes problems both when Linux code which reads timeout is ported to other operating systems, and when code is ported to Linux that reuses a struct timeval for multiple select()s in a loop without reinitializing it. Consider timeout to be undefined after select() returns.

You will need to initialize the timeval before every call to select, not just once at the beginning of the program.

like image 27
Joey Adams Avatar answered Nov 08 '22 00:11

Joey Adams