Thread: How to read from a pipe while keeping interactive keyboard in C

  1. #1
    Registered User
    Join Date
    Feb 2017
    Posts
    4

    Question How to read from a pipe while keeping interactive keyboard in C

    I'm writing a similar version of less and am using ncurses to make it easier. However, it needs to read from stdin when data is piped-in while maintaining the ability to interact with the keyboard such as "Move Up" and "Move Down".

    The issue I'm having is that the call to fgets freezes the whole interactive program when using tail -f. e.g.,

    Code:
    tail -f logfile |./app
    tail -f logfile | grep cron |./app
    
    Note that it works fine when using cat, but not tail -f.

    Is there a way to read from stdin (from a pipe) while keep the interactive keys enabled without affecting its functionality?

    Would I need to create a named pipe to pipe tail into it, and my program read from it like a file and still have access to stdin via getch?

    Code:
    // gcc app.c -lncurses
    
    
    #include <stdio.h>
    #include <ncurses.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <string.h>
    
    void print_status(WINDOW * win, const char *str) {
        init_pair(1, COLOR_RED, COLOR_WHITE);
        clear();
        wattron(win, COLOR_PAIR(1));
        mvwhline(win, 0, 0, ' ', COLS);
        mvwaddnstr(win, 0, 0, str, COLS);
        wattroff(win, COLOR_PAIR(1));
        wrefresh(win);
    }
    
    void print_line(WINDOW * win, const char *str, int *i) {
        init_pair(2, COLOR_RED, COLOR_BLACK);
        if (*i > LINES - 2) {
            *i = 1;
            wclear(win);
        }
        wattron(win, COLOR_PAIR(2));
        mvwprintw(win, (*i)++, 0, str);
        wattroff(win, COLOR_PAIR(2));
        wrefresh(win);
    }
    
    #define TTY "/dev/tty"
    static int open_terminal(char **result, int mode) {
        const char *device = TTY;
        *result = strdup(device);
        return open(device, mode);
    }
    
    int main() {
        WINDOW *menu_win, *tail;
        int i = 1, q = 1, fd1, fd2, is_atty = 1;
        char buf[1024];
    
        initscr();
        clear();
        noecho();
        halfdelay(5);
        start_color();
        nonl();
        keypad(stdscr, TRUE);
        intrflush(stdscr, FALSE);
    
        menu_win = newwin(1, COLS, 0, 0);
        tail = newwin(LINES - 1, COLS, 1, 0);
        keypad(menu_win, TRUE);
    
        char *device = NULL;
        FILE *pipe_input = stdin;
        if(!isatty(fileno(stdin))) {
            is_atty = 0;
            if((fd1 = open_terminal(&device, O_RDONLY)) >= 0) {
                if((fd2 = dup(fileno(stdin))) >= 0) {
                    pipe_input = fdopen(fd2, "r");
                    if(freopen(device, "r", stdin) == 0)
                        perror("cannot open tty-input");
                    if(fileno(stdin) != 0)    /* some functions may read fd #0 */
                        (void)dup2(fileno(stdin), 0);
                }
            }
        }
        // freopen("/dev/tty", "r", stdin);
        print_status(menu_win, "Use arrow keys to go up and down");
        while(q) {
            switch (wgetch(menu_win)) {
                case KEY_UP:
                    print_status(menu_win, "Moved up");
                    break;
                case KEY_DOWN:
                    print_status(menu_win, "Moved down");
                    break;
                case 'q':
                    q = 0;
                    break;
                default:
                    if(is_atty)
                        break;
                    while (fgets(buf, 1024, pipe_input) != NULL) {
                        print_line(tail, buf, &i);
                    }
                    break;
            }
        }
        endwin();
        return 0;
    }




  2. #2
    Registered User
    Join Date
    Jun 2015
    Posts
    1,643
    Somewhat hacky (and with no error handling) :
    Code:
    #define _POSIX_SOURCE
    #include <stdio.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[]) {
        FILE *input = NULL;
    
        if (argc > 1)
            input = fopen(argv[1], "r");
    
        if (!isatty(fileno(stdin))) {
            // duplicate stdin file descriptor and open it as a FILE*
            input = fdopen(dup(fileno(stdin)), "r");
            // reopen stdin on the terminal (assuming stdout is still connected to the terminal)
            freopen(ttyname(fileno(stdout)), "r", stdin);
        }
    
        char line[1000];
        while (fgets(line, sizeof line, input) != NULL) {
            printf("%s", line);
            getchar();  // read from stdin (hopefully the terminal!)
        }
    
        return 0;
    }

  3. #3
    Registered User
    Join Date
    Feb 2017
    Posts
    4
    Thanks for your reply. I'm not sure why, but looks like fgets keeps blocking wgetch. The interesting part is that using `cat` (without follow mode) wgetch works, but not with tail -f.

    Code:
        keypad(menu_win, TRUE);
    
        FILE *input = NULL;
        if (!isatty(fileno(stdin))) {
            // duplicate stdin file descriptor and open it as a FILE*
            input = fdopen(dup(fileno(stdin)), "r");
            // reopen stdin on the terminal (assuming stdout is still connected to the terminal)
            freopen(ttyname(fileno(stdout)), "r", stdin);
        }
    
        print_status(menu_win, "Use arrow keys to go up and down");
        while(q) {
            switch (wgetch(menu_win)) {
                case KEY_UP:
                    print_status(menu_win, "Moved up");
                    break;
                case KEY_DOWN:
                    print_status(menu_win, "Moved down");
                    break;
                case 'q':
                    q = 0;
                    break;
                default:
                    while (fgets(buf, 1024, input) != NULL) {
                        print_line(tail, buf, &i);
                    }
                    break;
            }
        }
        endwin();

  4. #4
    Registered User
    Join Date
    Jun 2015
    Posts
    1,643
    Yeah, I wasn't really thinking. If it worked with cat then it's not what I thought. Presumably ncurses wgetch reads from the terminal, not stdin specifically. I'm not sure why it's blocking with tail -f.

  5. #5
    Registered User
    Join Date
    Feb 2017
    Posts
    4
    Quote Originally Posted by algorism View Post
    Yeah, I wasn't really thinking. If it worked with cat then it's not what I thought. Presumably ncurses wgetch reads from the terminal, not stdin specifically. I'm not sure why it's blocking with tail -f.
    Do you think forking a process and reading pipe data (stdin) from the child and then writing that data from the child into the parent would help? Am I over complicating this?

  6. #6
    Registered User
    Join Date
    Jun 2015
    Posts
    1,643
    Quote Originally Posted by berrydev View Post
    Do you think forking a process and reading pipe data (stdin) from the child and then writing that data from the child into the parent would help? Am I over complicating this?
    I have no idea what's going on. I didn't even look at your original code (duh!), and I see that you were basically already doing what I suggested in my first post.

    Anyway, if you try that fork idea, let me know what happened.

    I'll at least try running your program and think about it a little.

  7. #7
    Registered User
    Join Date
    Jun 2015
    Posts
    1,643
    So I got a chance to run your program. It's not really like less at all. To be like less it should be able to take a filename as input and display that file. It should display the first screenful of lines of input and then let the user move up and down through the input with the arrow keys. That would involve scrolling the text window and adding a line to the top or bottom. It would also require either randomly accessing the file or reading in the entire file.

    I don't see how that can be done directly on piped input. The obvious solution is to save the data to a file and seek that.

    As for the blocking on the pipe input, you need to set the fd to non-blocking mode:
    Code:
                    int flags = fcntl(fd2, F_GETFL);
                    fcntl(fd2, F_SETFL, flags | O_NONBLOCK);
    The only problem is that the tail program keeps going after the main program quits.

  8. #8
    Registered User
    Join Date
    Feb 2017
    Posts
    4
    That appears to solve the blocking issue. Thanks a lot for pointing this out (was starting to pull my hair out).

    Like you said, tail keeps running for some reason, but if I write one more char to the tailing file, the program quits. I wonder if it's a matter of restoring stdin.

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Is possible read text form keyboard using read() function?
    By Vanhapolle in forum C Programming
    Replies: 3
    Last Post: 12-08-2013, 11:51 PM
  2. pipe() read(), write() - more then 128Kb of data
    By chihwahli in forum C Programming
    Replies: 3
    Last Post: 11-01-2011, 04:32 AM
  3. Interactive vs non-interactive terminal session question
    By Overworked_PhD in forum Tech Board
    Replies: 2
    Last Post: 06-18-2009, 07:30 PM
  4. Fgets to read dynamicallly 4m keyboard????
    By ganesh bala in forum C Programming
    Replies: 2
    Last Post: 04-09-2009, 03:12 AM
  5. non-blocking keyboard read
    By pppbigppp in forum Linux Programming
    Replies: 10
    Last Post: 01-17-2007, 06:51 AM

Tags for this Thread