32

I wrote and maintain a program rlwrap that uses a pseudo-terminal to communicate with a child process. Pseudo-terminals (ptys) are found in all Unix(-like) systems, but they behave slightly differently on different platforms.

Case in point: In rlwrap, the parent process keeps the slave pty open to keep tabs on the child's terminal settings (on Linux and FreeBSD one can use the master for that, but not in Solaris, for example)

On FreeBSD (8.2) (but not Linux) this leads to the loss of the child's final output. For example:

#include <stdio.h>

/* save as test.c and compile with gcc -o test test.c -lutil */

#define BUFSIZE 255

int main(void) {
  int master, slave;
  char buf[BUFSIZE];
  int nread;

  openpty(&master, &slave, NULL, NULL, NULL);

  if (fork()) {       /* parent:                                                      */
    close(slave);     /* leave this out and lose slave's final words ... WHY?         */
    do {
      nread = read(master, buf, BUFSIZE);
      write(STDOUT_FILENO, buf, nread); /* echo child's output to stdout              */
    } while (nread > 0);     
  } else {             /* child:                                                      */
    login_tty(slave);  /* this makes child a session leader and slave a controlling   */
                       /* terminal for it, then dup()s std{in,out,err} to slave       */ 
    printf("Feeling OK :-)\n");
    sleep(1);
    printf("Feeling unwell ... Arghhh!\n"); /* this line may get lost                 */
  }
  return 0;
}

The parent process will echo the child's output, as expected, but when I omit the close(slave) (keeping it open like in rlwrap):

  • on FreeBSD, the parent doesn't see the final output line, it reads an EOF instead. (If anything, I would have expected the opposite - that keeping the slave end open would prevent the output from being lost)
  • On Linux, on the other hand, an EOF is never seen, not even after the child has died (whether we close the slave or not)

Is this behaviour documented somewhere? Is there a rationale for it? Can I circumvent it without closing the slave in the parent process?

I found out that not making the slave a controlling terminal - replacing the login_tty call with a few simple dup() calls - will cure the problem. This is no solution for rlwrap however: quite a few commands need a controlling terminal (/dev/tty) to talk to, so rlwrap has to provide one for them.

Hans Lub
  • 5,513
  • 1
  • 23
  • 43
  • Don't forget to check correctly the return values of each function. In Linux you'll get a deadlock in the child because `read(2)` will return -1 (`EIO`) and the loop will never end. – Fernando Silveira May 12 '14 at 18:47
  • @Fernando Silveira I tried to make the example program as simple as possible; it turned out a bit _too_ sloppy as a result. I tidied it up a bit (of course one should also check for fork() < 0 etc.) But that doesn't really answer the original question - why does FreeBSD return an EOF, instead of the last bytes written() to the slave when (and only when) the slave is kept open in the parent, and that slave is the controlling terminal for the child? – Hans Lub May 12 '14 at 20:08
  • I can reproduce this by running the test program (after adding missing includes `` and ``) using `cpuset -l 0` to disable multiple cores. I think the proper solution is to keep the slave end open only on strange systems that need it. – jilles May 12 '14 at 22:44
  • @jilles: Thanks for the confirmation! Your solution is exactly what I have done in `rlwrap` (issuing a `tcgetattr()` on the master end at program startup, and keeping ths slave open in the parent only if that fails). Still, it bothers me a bit that I can find _no documentation at all_ (linux, BSD, or any 'non-strange' system) that the master end supports `tcgetattr()` _and always will return the correct results_ (i.e. the same results as the slave end would have yielded) – Hans Lub May 13 '14 at 07:30
  • 1
    Just a guess. The close at thw other side "flushes" the data. – harper Jun 04 '14 at 18:03
  • @harper: the close() happens before the final output is written. – Hans Lub Jun 05 '14 at 20:41
  • Don't have anything but 10.0 docs handy, but definitely there's something undocumented happening/needing to be done as you've discovered. Best thing to do ask about this on freebsd-doc mailing list. You'll likely get a good working solution from stackover for this, but getting the actual root problem tracked down and properly documented is best for the longterm. – D'Nabre Nov 03 '14 at 07:16
  • There is a small chance that SIGCLD will cause read/write to return -1 and errno = EINTR. Try masking or ignoring the signal properly. – lonewasp Dec 24 '14 at 20:50
  • @lonewasp: Indeed, there is a 100% chance that `SIGCHLD` will cause `read()` to return -1. But _only_ if I close the slave in the parent. The puzzling case is when I _don't_ close it. In that case, on FreeBSD the parent will get an EOF (not an EIO!) _instead_ of the "Arghh", while Linux will just restart the `read()` after reading the "Arghh". – Hans Lub Dec 25 '14 at 10:54
  • First of all close master in child code. This seems to look like the pipe case - you open two ends and need to close proper one in each process to get only one data flow line. I would guess that if you don't close slave in parent - master might sill think some fd is associated with its input end and will not fire EOF. On the other hand if master is not closed in child it might get some of salve input instead of real master end. Rather blind guess, but falls nicely into unpredicatble or implementation undefined behavior. – lonewasp Dec 25 '14 at 17:52
  • @lonewasp: Child closing master doesn't make any difference. Parent closing slave does, and that is what `rlwrap` does, if it can, but sometimes it cannot. The main puzzle remains: where does the "Arghh" go under FreeBSD? – Hans Lub Dec 26 '14 at 11:49
  • If you add sleep(1) after last write in child process you'll get "Arggh" or sleep(2) in parent before read cycle and get none. So it seems like on FreeBSD, if you have more then one descriptor to pipe end all buffered/undelivered data is discarded on close() for given descriptor. Perhaps on Ubuntu "same" buffer might be tied to reading end of pipe and getting all the date before close() in child process or some shared buffer is used. – lonewasp Dec 26 '14 at 14:32
  • Usually (certainly in `rlwrap`s case) a child `exec`s some command. There is no way to make this command sleep after its farewell speech. – Hans Lub Dec 26 '14 at 14:42

5 Answers5

1

I think there is unique separate behaviour for the Pty.

  1. The system terminates if the last data is written
  2. The system terminates if the child exits (broken pipe?)

The code relies on the pipe existing long enough to send the data through, but the child exiting may cause the virtual channel to be deleted before the data is received.

This would be unique to Pty, and not exist for real terminal.

mksteve
  • 12,614
  • 3
  • 28
  • 50
0

On FreeBSD 10-STABLE I do get both output lines.

(You can replace openpty and fork with forkpty which basically takes care of login_tty as well.)

In FreeBSD 8.0, the old pty(4) driver was replaced by pts(4). The new pty(4) behaves differently from the old one. From the manual;

Unlike previous implementations, the master and slave device nodes are destroyed when the PTY becomes unused. A call to stat(2) on a nonexistent master device will already cause a new master device node to be created. The master device can only be destroyed by opening and closing it.

There might well have been significant changes between 8.0-RELEASE and 10.0-RELEASE

You also might want to look at the patch that is applied in the FreeBSD ports tree.

Roland Smith
  • 42,427
  • 3
  • 64
  • 94
  • I still don't get both output lines after moving from FreeBSD 8.2 to 10.0 (with libutil.so.9), at least not after removing the `close(slave)` line. – Hans Lub Aug 25 '14 at 22:27
0

printf does buffered output depending of the class of output device. Just try to put fflush(stdout); after the last printf to see if it's a problem with buffered output.

Luis Colorado
  • 10,974
  • 1
  • 16
  • 31
  • 1
    As the child's stdout is a pty, it will be line buffered, so there should be no need for an explicit fflush. Moreover, returning from main() flushes and closes all open file descriptors anyway. – Hans Lub Aug 25 '14 at 22:19
0

here is what I found on ubuntu linux Note: always check for errors

#include <stdio.h>
#include <stdlib.h>
#include <pty.h>    // openpty(), 
#include <utmp.h>   // login_tty()
#include <unistd.h> // read(), write()

/* save as test.c and compile with gcc -o test test.c -lutil */

#define BUFSIZE (255)

int main(void) 
{
    int master, slave;
    char buf[BUFSIZE];
    int nread;
    pid_t pid;

    if( -1 == openpty(&master, &slave, NULL, NULL, NULL) )
    { // then openpty failed 
        perror( "openpty failed" );
        exit( EXIT_FAILURE );
    }

    // implied else, openpty successful

    pid = fork();
    if( -1 == pid ) 
    { // then fork failed
        perror( "fork failed" );
        exit( EXIT_FAILURE );
    }

    // implied else, fork successful

    if( pid ) 
    {    /* parent:                                                      */
        close(slave);     /* leave this out and lose slave's final words ... WHY?         */
        do 
        {
            if( -1 == (nread = read(master, buf, BUFSIZE) ) )
            {// then, error occurred
                perror( "read failed" );
                exit( EXIT_FAILURE );
            }

            // implied else, read successful

            if ( nread )
            {   
                write(STDOUT_FILENO, buf, nread); /* echo child's output to stdout  */
            }
        } while (nread);    /* nread == 0 indicates EOF */     
    } 

    else // pid == 0
    {    /* child:                                                      */
        if( -1 == login_tty(slave) )  /* this makes child a session leader and slave a controlling   */
                       /* terminal for it, then dup()s std{in,out,err} to slave       */ 
        { // then login_tty failed
            perror( "login_tty failed" );
            exit( EXIT_FAILURE );
        }

        // implied else, login_tty successful

        printf("Feeling OK :-)\n");
        sleep(1);
        printf("Feeling unwell ... Arghhh!\n"); /* this line may get lost */
    } // end if

    return 0;
} // end function: main

when the close() statement is commented out then the parent never exits due to the read() statement blocking

when the close() statement is part of the source then the parent exits with a read error from trying to read from a terminal that is 'missing' when the child exits

here is the output when close() commentedpout

Feeling OK :-)
Feeling unwell ... Arghhh!

then the parent hangs on the read() statement

here is the output when close() is not commented out

Feeling OK :-)
Feeling unwell ... Arghhh!
read failed: Input/output error
user3629249
  • 16,402
  • 1
  • 16
  • 17
  • When writing a [MCVE](http://stackoverflow.com/help/mcve) I remove most error checking code if, like in this case, the problem at hand is not caused by an error condition. Your example confirms the observations in the original question - thanks! - but the question itself is still wide open. – Hans Lub Feb 24 '15 at 10:58
0

I'm not sure if I get this right: independent from pty or not, as long as one process has the channel open, the OS should not pass an EOF to the reader (because there is still a writer). (after the fork there are two open channels) only if you close the parent, a close on the slave should forward the EOF.

On a PTY, are you sure that NL's are handled correctly, since normally a CR should trigger the new-line.

(just a thought: if it is a controling tty, things might change since the OS handles singal deliveries differently and closing the chanel would normaly terminate all children processes of the child. Could this be an issue if the parent still has the handle open? )

thilo
  • 1