1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-25 01:02:24 +00:00

uxpty: option to make three pipes instead of a pty.

Not every "session" channel in SSH allocates a pty at all, of course,
and so I'll need a way to run a subprocess without doing so. The
simplest approach seems to be to expand uxpty's remit so that the pty
is optional: now it can open either a pty or a set of pipes for
stdin/out/err, according to an option provided to pty_backend_create.

(It amuses me that without this option I'd have an SSH server which is
incapable of _not_ honouring the "pty-req" channel request. That's
normally the easy part!)

This breaks the previous one-to-one coupling between pty backend
instances and file descriptors passed to uxsel, which I was using to
look up the Pty structure in a tree234 indexed by fd when an uxsel
notification came back. So now each Pty structure contains a
collection of subobjects of a new type PtyFd, and _those_ are what's
stored in the fd-indexed tree.

Another awkward part is that uxsel_set is not incremental: the rwx
flags you pass to it completely supersede the previous set for that
file descriptor, so I had to set up the logic that decides whether
we're trying to read or write each fd in a way that can cope equally
well with the fd aliasing another one (if it's the pty master) or not
(if there are three completely separate pipes).
This commit is contained in:
Simon Tatham 2018-10-18 20:31:13 +01:00
parent 63d08fc308
commit 6b7a1cd1c1

View File

@ -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;
}