6

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!

please delete me
  • 137
  • 1
  • 1
  • 7
  • Why are you putting `infile` in `writeset`? You are not writing to it. – aschepler Nov 11 '11 at 23:19
  • Good point, I will update to reflect this. – please delete me Nov 11 '11 at 23:20
  • At a guess, its probably not good to have `infile` in `writeset` given that `infile` was opened read-only. Not sure if this is causing the problem, though. – Aaron Dufour Nov 11 '11 at 23:22
  • Is this for an UNIX-like system or something else? – ott-- Nov 11 '11 at 23:25
  • I am programming this on Ubuntu – please delete me Nov 11 '11 at 23:26
  • Not sure if it'd work, but can you use `feof(..)` to determine if `stdin` has data waiting or not? Just FYI, but I've found when doing input-based programs that an entire line is buffered and then submitted to the program once the new line key is struck. – Jonathon Nov 12 '11 at 00:00
  • I think the argument to `select` should read `fileno(stdin)+1`, not `fileno(infile)+1`. Note that in this case, fixing it won't change how the program runs, because `STDIN_FILENO` is 0. – Joey Adams Nov 12 '11 at 18:07

4 Answers4

7

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.

ott--
  • 5,642
  • 4
  • 24
  • 27
5

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.

Joey Adams
  • 41,996
  • 18
  • 86
  • 115
  • I did realize that the terminal is buffering lines so my character precedes a newline. However, I didn't realize select() was implemented differently on Linux. I have made the changes suggested and the program still runs the same. Even when I input a character and press enter, the terminal is cutting in and telling me that the : command not found. – please delete me Nov 12 '11 at 15:09
  • 1
    @MJPiarulli: If your input is not consumed by the program (e.g. with `read` or `fgets`), it will be read by the terminal instead. – Joey Adams Nov 12 '11 at 18:01
0

You'd want to make your program multithreaded. Create a thread that prints out the file every 1 second interval, and your main thread would be getting input from stdin, then signal the other thread to stop printing whenever you get the input

JosephH
  • 8,465
  • 4
  • 34
  • 62
  • Thanks for the suggestion, but we haven't learned about creating multiple threads in class yet, so I think my professor wants us to go about this another way. – please delete me Nov 11 '11 at 23:19
0

Part of your problem is that you are using sleep(1) which will cause that line to take a full second to execute. If the user types a character, they will have to wait up to a full second before your program responds. So even once you get the non-blocking portion working you will still have issues.

The solution is to use nanosleep or usleep to pause the program for less than 1 second. My recommendation would be to sleep for 1/100 of a second *using one of those functions) and check for user key presses every time. On the 100th time, output the next portion of the file. That way the file still goes the right speed, but the user can stop it whenever they want and the program will respond to their command very quickly.

SoapBox
  • 20,457
  • 3
  • 51
  • 87
  • 2
    Using `select`, you don't have to repeatedly check for keypresses. `select` can sleep and wait for input simultaneously. – Joey Adams Nov 12 '11 at 03:46