Hans Lub
Hans Lub

Reputation: 5678

Final output on slave pty is lost if it was not closed in parent. Why?

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):

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.

Upvotes: 32

Views: 2517

Answers (5)

mksteve
mksteve

Reputation: 13073

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.

Upvotes: 1

thilo
thilo

Reputation: 1

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? )

Upvotes: 0

user3629249
user3629249

Reputation: 16540

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

Upvotes: 0

Luis Colorado
Luis Colorado

Reputation: 12668

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.

Upvotes: 0

Roland Smith
Roland Smith

Reputation: 43495

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.

Upvotes: 0

Related Questions