Reputation: 5678
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
Reputation: 13073
I think there is unique separate behaviour for the Pty.
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
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
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
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
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