diff --git a/unix/uxpty.c b/unix/uxpty.c index 6ff62b69..dea208f7 100644 --- a/unix/uxpty.c +++ b/unix/uxpty.c @@ -70,9 +70,19 @@ typedef struct Pty Pty; */ static int pty_signal_pipe[2] = { -1, -1 }; /* obviously bogus initial val */ +typedef struct PtyFd { + int fd; + Pty *pty; +} PtyFd; + struct Pty { Conf *conf; + int master_fd, slave_fd; + int pipefds[6]; + PtyFd fds[3]; + int master_i, master_o, master_e; + Seat *seat; char name[FILENAME_MAX]; pid_t child_pid; @@ -84,40 +94,42 @@ struct Pty { }; /* - * We store our pty backends in a tree sorted by master fd, so that - * when we get an uxsel notification we know which backend instance - * is the owner of the pty that caused it. + * We store all the (active) PtyFd structures in a tree sorted by fd, + * so that when we get an uxsel notification we know which backend + * instance is the owner of the pty that caused it, and then we can + * find out which fd is the relevant one too. */ -static int pty_compare_by_fd(void *av, void *bv) +static int ptyfd_compare(void *av, void *bv) { - Pty *a = (Pty *)av; - Pty *b = (Pty *)bv; + PtyFd *a = (PtyFd *)av; + PtyFd *b = (PtyFd *)bv; - if (a->master_fd < b->master_fd) + if (a->fd < b->fd) return -1; - else if (a->master_fd > b->master_fd) + else if (a->fd > b->fd) return +1; return 0; } -static int pty_find_by_fd(void *av, void *bv) +static int ptyfd_find(void *av, void *bv) { int a = *(int *)av; - Pty *b = (Pty *)bv; + PtyFd *b = (PtyFd *)bv; - if (a < b->master_fd) + if (a < b->fd) return -1; - else if (a > b->master_fd) + else if (a > b->fd) return +1; return 0; } -static tree234 *ptys_by_fd = NULL; +static tree234 *ptyfds = NULL; /* - * We also have a tree sorted by child pid, so that when we wait() - * in response to the signal we know which backend instance is the - * owner of the process that caused the signal. + * We also have a tree of Pty structures themselves, sorted by child + * pid, so that when we wait() in response to the signal we know which + * backend instance is the owner of the process that caused the + * signal. */ static int pty_compare_by_pid(void *av, void *bv) { @@ -401,10 +413,6 @@ static void pty_open_master(Pty *pty) #ifndef SET_NONBLOCK_VIA_OPENPT nonblock(pty->master_fd); #endif - - if (!ptys_by_fd) - ptys_by_fd = newtree234(pty_compare_by_fd); - add234(ptys_by_fd, pty); } static Pty *new_pty_struct(void) @@ -584,7 +592,7 @@ void pty_pre_init(void) static void pty_try_wait(void); -static void pty_real_select_result(Pty *pty, int event, int status) +static void pty_real_select_result(Pty *pty, int fd, int event, int status) { char buf[4096]; int ret; @@ -597,48 +605,77 @@ static void pty_real_select_result(Pty *pty, int event, int status) */ if ((WIFEXITED(status) || WIFSIGNALED(status))) { /* - * The primary child process died. We could keep - * the terminal open for remaining subprocesses to - * output to, but conventional wisdom seems to feel - * that that's the Wrong Thing for an xterm-alike, - * so we bail out now (though we don't necessarily - * _close_ the window, depending on the state of - * Close On Exit). This would be easy enough to - * change or make configurable if necessary. + * The primary child process died. + */ + pty->child_dead = TRUE; + del234(ptys_by_pid, pty); + pty->exit_code = status; + + /* + * If this is an ordinary pty session, this is also the + * moment to terminate the whole backend. + * + * We _could_ instead keep the terminal open for remaining + * subprocesses to output to, but conventional wisdom + * seems to feel that that's the Wrong Thing for an + * xterm-alike, so we bail out now (though we don't + * necessarily _close_ the window, depending on the state + * of Close On Exit). This would be easy enough to change + * or make configurable if necessary. */ - pty->exit_code = status; - pty->child_dead = TRUE; - del234(ptys_by_pid, pty); - finished = TRUE; + if (pty->master_fd >= 0) + finished = TRUE; } } else { if (event == 1) { + int is_stdout = (fd == pty->master_o); - ret = read(pty->master_fd, buf, sizeof(buf)); + ret = read(fd, buf, sizeof(buf)); - /* - * Clean termination condition is that either ret == 0, or ret - * < 0 and errno == EIO. Not sure why the latter, but it seems - * to happen. Boo. - */ - if (ret == 0 || (ret < 0 && errno == EIO)) { - /* - * We assume a clean exit if the pty has closed but the - * actual child process hasn't. The only way I can - * imagine this happening is if it detaches itself from - * the pty and goes daemonic - in which case the - * expected usage model would precisely _not_ be for - * the pterm window to hang around! - */ - finished = TRUE; - pty_try_wait(); /* one last effort to collect exit code */ - if (!pty->child_dead) - pty->exit_code = 0; + /* + * Treat EIO on a pty master as equivalent to EOF (because + * that's how the kernel seems to report the event where + * the last process connected to the other end of the pty + * went away). + */ + if (fd == pty->master_fd && ret < 0 && errno == EIO) + ret = 0; + + if (ret == 0) { + /* + * EOF on this input fd, so to begin with, we may as + * well close it, and remove all references to it in + * the pty's fd fields. + */ + uxsel_del(fd); + close(fd); + if (pty->master_fd == fd) + pty->master_fd = -1; + if (pty->master_o == fd) + pty->master_o = -1; + if (pty->master_e == fd) + pty->master_e = -1; + + if (is_stdout) { + /* + * We assume a clean exit if the pty (or stdout + * pipe) has closed, but the actual child process + * hasn't. The only way I can imagine this + * happening is if it detaches itself from the pty + * and goes daemonic - in which case the expected + * usage model would precisely _not_ be for the + * pterm window to hang around! + */ + finished = TRUE; + pty_try_wait(); /* one last effort to collect exit code */ + if (!pty->child_dead) + pty->exit_code = 0; + } } else if (ret < 0) { perror("read pty master"); exit(1); } else if (ret > 0) { - seat_stdout(pty->seat, buf, ret); + seat_output(pty->seat, !is_stdout, buf, ret); } } else if (event == 2) { /* @@ -650,10 +687,13 @@ static void pty_real_select_result(Pty *pty, int event, int status) if (finished && !pty->finished) { int close_on_exit; + int i; - uxsel_del(pty->master_fd); - pty_close(pty); - pty->master_fd = -1; + for (i = 0; i < 3; i++) + if (pty->fds[i].fd >= 0) + uxsel_del(pty->fds[i].fd); + + pty_close(pty); pty->finished = TRUE; @@ -700,14 +740,12 @@ static void pty_try_wait(void) pty = find234(ptys_by_pid, &pid, pty_find_by_pid); if (pty) - pty_real_select_result(pty, -1, status); + pty_real_select_result(pty, -1, -1, status); } while (pid > 0); } void pty_select_result(int fd, int event) { - Pty *pty; - if (fd == pty_signal_pipe[0]) { char c[1]; @@ -717,21 +755,46 @@ void pty_select_result(int fd, int event) pty_try_wait(); } else { - pty = find234(ptys_by_fd, &fd, pty_find_by_fd); + PtyFd *ptyfd = find234(ptyfds, &fd, ptyfd_find); - if (pty) - pty_real_select_result(pty, event, 0); + if (ptyfd) + pty_real_select_result(ptyfd->pty, fd, event, 0); } } +static void pty_uxsel_setup_fd(Pty *pty, int fd) +{ + int rwx = 0; + + if (fd < 0) + return; + + /* read from standard output and standard error pipes */ + if (pty->master_o == fd || pty->master_e == fd) + rwx |= 1; + /* write to standard input pipe if we have any data */ + if (pty->master_i == fd && bufchain_size(&pty->output_data)) + rwx |= 2; + + uxsel_set(fd, rwx, pty_select_result); +} + static void pty_uxsel_setup(Pty *pty) { - int rwx; - - rwx = 1; /* always want to read from pty */ - if (bufchain_size(&pty->output_data)) - rwx |= 2; /* might also want to write to it */ - uxsel_set(pty->master_fd, rwx, pty_select_result); + /* + * We potentially have three separate fds here, but on the other + * hand, some of them might be the same (if they're a pty master). + * So we can't just call uxsel_set(master_o, 1) and then + * uxsel_set(master_i, 2), without the latter potentially undoing + * the work of the former if master_o == master_i. + * + * Instead, here we call a single uxsel on each one of these fds + * (if it exists at all), and for each one, check it against all + * three to see which bits to set. + */ + pty_uxsel_setup_fd(pty, pty->master_o); + pty_uxsel_setup_fd(pty, pty->master_e); + pty_uxsel_setup_fd(pty, pty->master_i); /* * In principle this only needs calling once for all pty @@ -779,8 +842,8 @@ static void copy_ttymodes_into_termios( * and port numbers. */ Backend *pty_backend_create( - Seat *seat, LogContext *logctx, Conf *conf, char **argv, - struct ssh_ttymodes ttymodes) + Seat *seat, LogContext *logctx, Conf *conf, char **argv, const char *cmd, + struct ssh_ttymodes ttymodes, int pipes_instead) { int slavefd; pid_t pid, pgrp; @@ -789,6 +852,7 @@ Backend *pty_backend_create( long windowid; #endif Pty *pty; + int i; if (single_pty) { pty = single_pty; @@ -800,6 +864,12 @@ Backend *pty_backend_create( pty_stamped_utmp = FALSE; #endif } + for (i = 0; i < 6; i++) + pty->pipefds[i] = -1; + for (i = 0; i < 3; i++) { + pty->fds[i].fd = -1; + pty->fds[i].pty = pty; + } pty->seat = seat; pty->backend.vt = &pty_backend; @@ -808,35 +878,76 @@ Backend *pty_backend_create( pty->term_width = conf_get_int(conf, CONF_width); pty->term_height = conf_get_int(conf, CONF_height); - if (pty->master_fd < 0) - pty_open_master(pty); + if (!ptyfds) + ptyfds = newtree234(ptyfd_compare); + + if (pipes_instead) { + if (pty->master_fd >= 0) { + /* If somehow we've got a pty master already and don't + * need it, throw it away! */ + close(pty->master_fd); + if (pty_utmp_helper_pipe >= 0) { + close(pty_utmp_helper_pipe); /* don't need this either */ + pty_utmp_helper_pipe = -1; + } + } + + + for (i = 0; i < 6; i += 2) { + if (pipe(pty->pipefds + i) < 0) { + backend_free(&pty->backend); + return NULL; + } + } + + pty->fds[0].fd = pty->master_i = pty->pipefds[1]; + pty->fds[1].fd = pty->master_o = pty->pipefds[2]; + pty->fds[2].fd = pty->master_e = pty->pipefds[4]; + + add234(ptyfds, &pty->fds[0]); + add234(ptyfds, &pty->fds[1]); + add234(ptyfds, &pty->fds[2]); + } else { + if (pty->master_fd < 0) + pty_open_master(pty); #ifndef OMIT_UTMP - /* - * Stamp utmp (that is, tell the utmp helper process to do so), - * or not. - */ - if (pty_utmp_helper_pipe >= 0) { /* if it's < 0, we can't anyway */ - if (!conf_get_int(conf, CONF_stamp_utmp)) { - close(pty_utmp_helper_pipe); /* just let the child process die */ - pty_utmp_helper_pipe = -1; - } else { - const char *location = seat_get_x_display(pty->seat); - int len = strlen(location)+1, pos = 0; /* +1 to include NUL */ - while (pos < len) { - int ret = write(pty_utmp_helper_pipe, location+pos, len - pos); - if (ret < 0) { - perror("pterm: writing to utmp helper process"); - close(pty_utmp_helper_pipe); /* arrgh, just give up */ - pty_utmp_helper_pipe = -1; - break; + /* + * Stamp utmp (that is, tell the utmp helper process to do so), + * or not. + */ + if (pty_utmp_helper_pipe >= 0) { /* if it's < 0, we can't anyway */ + if (!conf_get_int(conf, CONF_stamp_utmp)) { + /* We're not stamping utmp, so just let the child + * process die that was waiting to unstamp it later. */ + close(pty_utmp_helper_pipe); + pty_utmp_helper_pipe = -1; + } else { + const char *location = seat_get_x_display(pty->seat); + int len = strlen(location)+1, pos = 0; /* +1 to include NUL */ + while (pos < len) { + int ret = write(pty_utmp_helper_pipe, + location + pos, len - pos); + if (ret < 0) { + perror("pterm: writing to utmp helper process"); + close(pty_utmp_helper_pipe); /* arrgh, just give up */ + pty_utmp_helper_pipe = -1; + break; + } + pos += ret; } - pos += ret; } - } - } + } #endif + pty->master_i = pty->master_fd; + pty->master_o = pty->master_fd; + pty->master_e = -1; + + pty->fds[0].fd = pty->master_fd; + add234(ptyfds, &pty->fds[0]); + } + #ifndef NOT_X_WINDOWS /* for Mac OS X native compilation */ got_windowid = seat_get_windowid(pty->seat, &windowid); #endif @@ -891,62 +1002,74 @@ Backend *pty_backend_create( } } - slavefd = pty_open_slave(pty); - if (slavefd < 0) { - perror("slave pty: open"); - _exit(1); - } + pgrp = getpid(); - close(pty->master_fd); - noncloexec(slavefd); - dup2(slavefd, 0); - dup2(slavefd, 1); - dup2(slavefd, 2); - close(slavefd); - setsid(); + if (pipes_instead) { + int i; + dup2(pty->pipefds[0], 0); + dup2(pty->pipefds[3], 1); + dup2(pty->pipefds[5], 2); + for (i = 0; i < 6; i++) + close(pty->pipefds[i]); + + setsid(); + } else { + slavefd = pty_open_slave(pty); + if (slavefd < 0) { + perror("slave pty: open"); + _exit(1); + } + + close(pty->master_fd); + noncloexec(slavefd); + dup2(slavefd, 0); + dup2(slavefd, 1); + dup2(slavefd, 2); + close(slavefd); + setsid(); #ifdef TIOCSCTTY - ioctl(0, TIOCSCTTY, 1); + ioctl(0, TIOCSCTTY, 1); #endif - pgrp = getpid(); - tcsetpgrp(0, pgrp); - - /* - * Set up configuration-dependent termios settings on the new - * pty. Linux would have let us do this on the pty master - * before we forked, but that fails on OS X, so we do it here - * instead. - */ - if (tcgetattr(0, &attrs) == 0) { - /* - * Set the backspace character to be whichever of ^H and - * ^? is specified by bksp_is_delete. - */ - attrs.c_cc[VERASE] = conf_get_int(conf, CONF_bksp_is_delete) - ? '\177' : '\010'; + tcsetpgrp(0, pgrp); /* - * Set the IUTF8 bit iff the character set is UTF-8. + * Set up configuration-dependent termios settings on the new + * pty. Linux would have let us do this on the pty master + * before we forked, but that fails on OS X, so we do it here + * instead. */ + if (tcgetattr(0, &attrs) == 0) { + /* + * Set the backspace character to be whichever of ^H and + * ^? is specified by bksp_is_delete. + */ + attrs.c_cc[VERASE] = conf_get_int(conf, CONF_bksp_is_delete) + ? '\177' : '\010'; + + /* + * Set the IUTF8 bit iff the character set is UTF-8. + */ #ifdef IUTF8 - if (seat_is_utf8(seat)) - attrs.c_iflag |= IUTF8; - else - attrs.c_iflag &= ~IUTF8; + if (seat_is_utf8(seat)) + attrs.c_iflag |= IUTF8; + else + attrs.c_iflag &= ~IUTF8; #endif - copy_ttymodes_into_termios(&attrs, ttymodes); + copy_ttymodes_into_termios(&attrs, ttymodes); - tcsetattr(0, TCSANOW, &attrs); + tcsetattr(0, TCSANOW, &attrs); + } } setpgid(pgrp, pgrp); - { + if (!pipes_instead) { int ptyfd = open(pty->name, O_WRONLY, 0); if (ptyfd >= 0) close(ptyfd); } setpgid(pgrp, pgrp); - { + if (!pipes_instead) { char *term_env_var = dupprintf("TERM=%s", conf_get_str(conf, CONF_termtype)); putenv(term_env_var); @@ -1004,20 +1127,26 @@ Backend *pty_backend_create( putty_signal(SIGQUIT, SIG_DFL); putty_signal(SIGPIPE, SIG_DFL); block_signal(SIGPIPE, 0); - if (argv) { + if (argv || cmd) { /* - * Exec the exact argument list we were given. + * If we were given a separated argument list, try to exec + * it. */ - execvp(argv[0], argv); + if (argv) { + execvp(argv[0], argv); + } /* - * If that fails, and if we had exactly one argument, pass - * that argument to $SHELL -c. + * Otherwise, if we were given a single command string, + * try passing that to $SHELL -c. * - * This arranges that we can _either_ follow 'pterm -e' - * with a list of argv elements to be fed directly to - * exec, _or_ with a single argument containing a command - * to be parsed by a shell (but, in cases of doubt, the - * former is more reliable). + * In the case of pterm, this system of fallbacks arranges + * that we can _either_ follow 'pterm -e' with a list of + * argv elements to be fed directly to exec, _or_ with a + * single argument containing a command to be parsed by a + * shell (but, in cases of doubt, the former is more + * reliable). We arrange this by setting argv to the full + * argument list, and also setting cmd to the single + * element of argv if it's a length-1 list. * * A quick survey of other terminal emulators' -e options * (as of Debian squeeze) suggests that: @@ -1031,12 +1160,15 @@ Backend *pty_backend_create( * modes in order to be a drop-in replacement for either * xterm or gnome-terminal, and hence for anyone's * plausible uses of the Debian-style alias - * 'x-terminal-emulator'... + * 'x-terminal-emulator'. + * + * In other use cases, a caller can set only one of argv + * and cmd to get a fixed handling of the input. */ - if (argv[1] == NULL) { + if (cmd) { char *shell = getenv("SHELL"); if (shell) - execl(shell, shell, "-c", argv[0], (void *)NULL); + execl(shell, shell, "-c", cmd, (void *)NULL); } } else { char *shell = getenv("SHELL"); @@ -1064,6 +1196,18 @@ Backend *pty_backend_create( close(pty->slave_fd); if (!ptys_by_pid) ptys_by_pid = newtree234(pty_compare_by_pid); + if (pty->pipefds[0] >= 0) { + close(pty->pipefds[0]); + pty->pipefds[0] = -1; + } + if (pty->pipefds[3] >= 0) { + close(pty->pipefds[3]); + pty->pipefds[3] = -1; + } + if (pty->pipefds[5] >= 0) { + close(pty->pipefds[5]); + pty->pipefds[5] = -1; + } add234(ptys_by_pid, pty); } @@ -1092,9 +1236,16 @@ static const char *pty_init(Seat *seat, Backend **backend_handle, const char *host, int port, char **realhost, int nodelay, int keepalive) { + const char *cmd = NULL; struct ssh_ttymodes modes; + memset(&modes, 0, sizeof(modes)); - *backend_handle= pty_backend_create(seat, logctx, conf, pty_argv, modes); + + if (pty_argv && pty_argv[0] && !pty_argv[1]) + cmd = pty_argv[0]; + + *backend_handle= pty_backend_create( + seat, logctx, conf, pty_argv, cmd, modes, FALSE); *realhost = dupstr(""); return NULL; } @@ -1116,10 +1267,15 @@ static void pty_reconfig(Backend *be, Conf *conf) static void pty_free(Backend *be) { Pty *pty = container_of(be, Pty, backend); + int i; + + pty_close(pty); /* Either of these may fail `not found'. That's fine with us. */ del234(ptys_by_pid, pty); - del234(ptys_by_fd, pty); + for (i = 0; i < 3; i++) + if (pty->fds[i].fd >= 0) + del234(ptyfds, &pty->fds[i]); bufchain_clear(&pty->output_data); @@ -1141,11 +1297,11 @@ static void pty_try_write(Pty *pty) void *data; int len, ret; - assert(pty->master_fd >= 0); + assert(pty->master_i >= 0); while (bufchain_size(&pty->output_data) > 0) { bufchain_prefix(&pty->output_data, &data, &len); - ret = write(pty->master_fd, data, len); + ret = write(pty->master_i, data, len); if (ret < 0 && (errno == EWOULDBLOCK)) { /* @@ -1170,7 +1326,7 @@ static int pty_send(Backend *be, const char *buf, int len) { Pty *pty = container_of(be, Pty, backend); - if (pty->master_fd < 0) + if (pty->master_i < 0) return 0; /* ignore all writes if fd closed */ bufchain_add(&pty->output_data, buf, len); @@ -1181,10 +1337,25 @@ static int pty_send(Backend *be, const char *buf, int len) static void pty_close(Pty *pty) { + int i; + + if (pty->master_o >= 0) + uxsel_del(pty->master_o); + if (pty->master_e >= 0) + uxsel_del(pty->master_e); + if (pty->master_i >= 0) + uxsel_del(pty->master_i); + if (pty->master_fd >= 0) { close(pty->master_fd); pty->master_fd = -1; } + for (i = 0; i < 6; i++) { + if (pty->pipefds[i] >= 0) + close(pty->pipefds[i]); + pty->pipefds[i] = -1; + } + pty->master_i = pty->master_o = pty->master_e = -1; #ifndef OMIT_UTMP if (pty_utmp_helper_pipe >= 0) { close(pty_utmp_helper_pipe); /* this causes utmp to be cleaned up */ @@ -1214,6 +1385,9 @@ static void pty_size(Backend *be, int width, int height) pty->term_width = width; pty->term_height = height; + if (pty->master_fd < 0) + return; + seat_get_window_pixel_size(pty->seat, &xpixel, &ypixel); size.ws_row = (unsigned short)pty->term_height; @@ -1232,7 +1406,8 @@ static void pty_special(Backend *be, SessionSpecialCode code, int arg) Pty *pty = container_of(be, Pty, backend); if (code == SS_BRK) { - tcsendbreak(pty->master_fd, 0); + if (pty->master_fd >= 0) + tcsendbreak(pty->master_fd, 0); return; }