Using named pipes to send keypresses to an interactive program

fifotty

I have a program, mpg123, which has an interactive mode that allows keyboard commands from stdin to do useful things such as control volume. I am trying to start up mpg123 so that it reads commands from a named pipe; that way I can have other programs interact with it.

In one terminal I do the following:

mkfifo pipe
tail -n1 -f pipe | mpg123 -vC /some/song.mp3

And in another terminal I do the following:

cat > pipe
-

Now, I was expecting that the - would be sent to the mpg123 program exactly the same as if I was sitting in that terminal smacking the - key, but that is not what happens. Can anyone please tell me what I am doing incorrectly?

Best Answer

It seems that -C causes mpg123 to read from the terminal, not from stdin. I see this, however, in my version of mpg123's man page:

-R, --remote
       Activate  generic  control interface.  mpg123 will then read and
       execute commands from stdin. Basic usage is ``load <filename> ''
       to  play some file and the obvious ``pause'', ``command.  ``jump
       <frame>'' will jump/seek to a given point (MPEG  frame  number).
       Issue ``help'' to get a full list of commands and syntax.

This may be what you are looking for; try mpg123 -vR <pipe. The interaction in your example would become something like the following (this sets the volume to 30%):

$ cat >pipe
load /some/song.mp3
volume 30

But then, what does -C do that -R doesn't that results in the former mode failing to read from stdin when a named pipe, rather than a terminal, is connected?

A quick look at the mpg123 source code indicates that it uses the termios facilities to read keypresses from the terminal, using tcsetattr to put it in the so-called "non-canonical mode", where keypresses are transmitted to the reader without further processing (in particular, without waiting for a complete line to have been typed):

struct termios tio = *pattern;
(...)

tio.c_lflag &= ~(ICANON|ECHO);
(...)

return tcsetattr(0,TCSANOW,&tio);

(This is the same as the GNU libc code sample.)

Then, in a loop, a function get_key is called, which uses select to tell whether file descriptor 0 (stdin) has data available and, if so, reads one byte from it (read(0,val,1)). But this still doesn't explain why a terminal works but a pipe doesn't! The answer lies in the terminal initialization code:

/* initialze terminal */
void term_init(void)
{
    debug("term_init");

    term_enable = 0;

    if(tcgetattr(0,&old_tio) < 0)
    {
        fprintf(stderr,"Can't get terminal attributes\n");
        return;
    }
    if(term_setup(&old_tio) < 0)
    {
        fprintf(stderr,"Can't set terminal attributes\n");
        return;
    }

    term_enable = 1;
}

Note that if either tcgetattr or term_setup fails, then term_enable is set to 0. (The function to read keys from the terminal starts with if(!term_enable) return 0;.) And, indeed, when stdin isn't a terminal, tcgetattr fails, the corresponding error message is printed, and the keypress-handling code is skipped:

$ mpg123 -C ~/input.mp3 <pipe
(...)
Can't get terminal attributes

This explains why attempting to send commands by piping into mpg123 -C fails. That's a debatable choice by the implementors; presumably by simply allowing tcgetattr / tcsetattr to fail (perhaps by using a switch for that purpose), instead of disabling the keypress-handling code handling, your attempt would have worked.

Related Question