summaryrefslogtreecommitdiffstats
path: root/lib/pty-session.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pty-session.c')
-rw-r--r--lib/pty-session.c727
1 files changed, 727 insertions, 0 deletions
diff --git a/lib/pty-session.c b/lib/pty-session.c
new file mode 100644
index 0000000..2d9741c
--- /dev/null
+++ b/lib/pty-session.c
@@ -0,0 +1,727 @@
+/*
+ * This is pseudo-terminal container for child process where parent creates a
+ * proxy between the current std{in,out,etrr} and the child's pty. Advantages:
+ *
+ * - child has no access to parent's terminal (e.g. su --pty)
+ * - parent can log all traffic between user and child's terminall (e.g. script(1))
+ * - it's possible to start commands on terminal although parent has no terminal
+ *
+ * This code is in the public domain; do with it what you wish.
+ *
+ * Written by Karel Zak <kzak@redhat.com> in Jul 2019
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <pty.h>
+#include <poll.h>
+#include <sys/signalfd.h>
+#include <paths.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "c.h"
+#include "all-io.h"
+#include "ttyutils.h"
+#include "pty-session.h"
+#include "monotonic.h"
+#include "debug.h"
+
+static UL_DEBUG_DEFINE_MASK(ulpty);
+UL_DEBUG_DEFINE_MASKNAMES(ulpty) = UL_DEBUG_EMPTY_MASKNAMES;
+
+#define ULPTY_DEBUG_INIT (1 << 1)
+#define ULPTY_DEBUG_SETUP (1 << 2)
+#define ULPTY_DEBUG_SIG (1 << 3)
+#define ULPTY_DEBUG_IO (1 << 4)
+#define ULPTY_DEBUG_DONE (1 << 5)
+#define ULPTY_DEBUG_ALL 0xFFFF
+
+#define DBG(m, x) __UL_DBG(ulpty, ULPTY_DEBUG_, m, x)
+#define ON_DBG(m, x) __UL_DBG_CALL(ulpty, ULPTY_DEBUG_, m, x)
+
+#define UL_DEBUG_CURRENT_MASK UL_DEBUG_MASK(ulpty)
+#include "debugobj.h"
+
+void ul_pty_init_debug(int mask)
+{
+ if (ulpty_debug_mask)
+ return;
+ __UL_INIT_DEBUG_FROM_ENV(ulpty, ULPTY_DEBUG_, mask, ULPTY_DEBUG);
+}
+
+struct ul_pty *ul_new_pty(int is_stdin_tty)
+{
+ struct ul_pty *pty = calloc(1, sizeof(*pty));
+
+ if (!pty)
+ return NULL;
+
+ DBG(SETUP, ul_debugobj(pty, "alloc handler"));
+ pty->isterm = is_stdin_tty;
+ pty->master = -1;
+ pty->slave = -1;
+ pty->sigfd = -1;
+ pty->child = (pid_t) -1;
+
+ return pty;
+}
+
+void ul_free_pty(struct ul_pty *pty)
+{
+ free(pty);
+}
+
+void ul_pty_slave_echo(struct ul_pty *pty, int enable)
+{
+ assert(pty);
+ pty->slave_echo = enable ? 1 : 0;
+}
+
+int ul_pty_get_delivered_signal(struct ul_pty *pty)
+{
+ assert(pty);
+ return pty->delivered_signal;
+}
+
+struct ul_pty_callbacks *ul_pty_get_callbacks(struct ul_pty *pty)
+{
+ assert(pty);
+ return &pty->callbacks;
+}
+
+void ul_pty_set_callback_data(struct ul_pty *pty, void *data)
+{
+ assert(pty);
+ pty->callback_data = data;
+}
+
+void ul_pty_set_child(struct ul_pty *pty, pid_t child)
+{
+ assert(pty);
+ pty->child = child;
+}
+
+int ul_pty_get_childfd(struct ul_pty *pty)
+{
+ assert(pty);
+ return pty->master;
+}
+
+pid_t ul_pty_get_child(struct ul_pty *pty)
+{
+ assert(pty);
+ return pty->child;
+}
+
+/* it's active when signals are redurected to sigfd */
+int ul_pty_is_running(struct ul_pty *pty)
+{
+ assert(pty);
+ return pty->sigfd >= 0;
+}
+
+void ul_pty_set_mainloop_time(struct ul_pty *pty, struct timeval *tv)
+{
+ assert(pty);
+ if (!tv) {
+ DBG(IO, ul_debugobj(pty, "mainloop time: clear"));
+ timerclear(&pty->next_callback_time);
+ } else {
+ pty->next_callback_time.tv_sec = tv->tv_sec;
+ pty->next_callback_time.tv_usec = tv->tv_usec;
+ DBG(IO, ul_debugobj(pty, "mainloop time: %ld.%06ld", tv->tv_sec, tv->tv_usec));
+ }
+}
+
+static void pty_signals_cleanup(struct ul_pty *pty)
+{
+ if (pty->sigfd != -1)
+ close(pty->sigfd);
+ pty->sigfd = -1;
+
+ /* restore original setting */
+ sigprocmask(SIG_SETMASK, &pty->orgsig, NULL);
+}
+
+/* call me before fork() */
+int ul_pty_setup(struct ul_pty *pty)
+{
+ struct termios slave_attrs;
+ sigset_t ourset;
+ int rc = 0;
+
+ assert(pty->sigfd == -1);
+
+ /* save the current signals setting */
+ sigprocmask(0, NULL, &pty->orgsig);
+
+ if (pty->isterm) {
+ DBG(SETUP, ul_debugobj(pty, "create for terminal"));
+
+ /* original setting of the current terminal */
+ if (tcgetattr(STDIN_FILENO, &pty->stdin_attrs) != 0) {
+ rc = -errno;
+ goto done;
+ }
+ ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&pty->win);
+ /* create master+slave */
+ rc = openpty(&pty->master, &pty->slave, NULL, &pty->stdin_attrs, &pty->win);
+ if (rc)
+ goto done;
+
+ /* set the current terminal to raw mode; pty_cleanup() reverses this change on exit */
+ slave_attrs = pty->stdin_attrs;
+ cfmakeraw(&slave_attrs);
+
+ if (pty->slave_echo)
+ slave_attrs.c_lflag |= ECHO;
+ else
+ slave_attrs.c_lflag &= ~ECHO;
+
+ tcsetattr(STDIN_FILENO, TCSANOW, &slave_attrs);
+ } else {
+ DBG(SETUP, ul_debugobj(pty, "create for non-terminal"));
+
+ rc = openpty(&pty->master, &pty->slave, NULL, NULL, NULL);
+ if (rc)
+ goto done;
+
+ tcgetattr(pty->slave, &slave_attrs);
+
+ if (pty->slave_echo)
+ slave_attrs.c_lflag |= ECHO;
+ else
+ slave_attrs.c_lflag &= ~ECHO;
+
+ tcsetattr(pty->slave, TCSANOW, &slave_attrs);
+ }
+
+ sigfillset(&ourset);
+ if (sigprocmask(SIG_BLOCK, &ourset, NULL)) {
+ rc = -errno;
+ goto done;
+ }
+
+ sigemptyset(&ourset);
+ sigaddset(&ourset, SIGCHLD);
+ sigaddset(&ourset, SIGWINCH);
+ sigaddset(&ourset, SIGALRM);
+ sigaddset(&ourset, SIGTERM);
+ sigaddset(&ourset, SIGINT);
+ sigaddset(&ourset, SIGQUIT);
+
+ if (pty->callbacks.flush_logs)
+ sigaddset(&ourset, SIGUSR1);
+
+ if ((pty->sigfd = signalfd(-1, &ourset, SFD_CLOEXEC)) < 0)
+ rc = -errno;
+done:
+ if (rc)
+ ul_pty_cleanup(pty);
+
+ DBG(SETUP, ul_debugobj(pty, "pty setup done [master=%d, slave=%d, rc=%d]",
+ pty->master, pty->slave, rc));
+ return rc;
+}
+
+/* cleanup in parent process */
+void ul_pty_cleanup(struct ul_pty *pty)
+{
+ struct termios rtt;
+
+ pty_signals_cleanup(pty);
+
+ if (pty->master == -1 || !pty->isterm)
+ return;
+
+ DBG(DONE, ul_debugobj(pty, "cleanup"));
+ rtt = pty->stdin_attrs;
+ tcsetattr(STDIN_FILENO, TCSADRAIN, &rtt);
+}
+
+/* call me in child process */
+void ul_pty_init_slave(struct ul_pty *pty)
+{
+ DBG(SETUP, ul_debugobj(pty, "initialize slave"));
+
+ setsid();
+
+ ioctl(pty->slave, TIOCSCTTY, 1);
+ close(pty->master);
+
+ dup2(pty->slave, STDIN_FILENO);
+ dup2(pty->slave, STDOUT_FILENO);
+ dup2(pty->slave, STDERR_FILENO);
+
+ close(pty->slave);
+
+ if (pty->sigfd >= 0)
+ close(pty->sigfd);
+
+ pty->slave = -1;
+ pty->master = -1;
+ pty->sigfd = -1;
+
+ sigprocmask(SIG_SETMASK, &pty->orgsig, NULL);
+
+ DBG(SETUP, ul_debugobj(pty, "... initialize slave done"));
+}
+
+static int write_output(char *obuf, ssize_t bytes)
+{
+ DBG(IO, ul_debug(" writing output"));
+
+ if (write_all(STDOUT_FILENO, obuf, bytes)) {
+ DBG(IO, ul_debug(" writing output *failed*"));
+ return -errno;
+ }
+
+ return 0;
+}
+
+static int write_to_child(struct ul_pty *pty, char *buf, size_t bufsz)
+{
+ return write_all(pty->master, buf, bufsz);
+}
+
+/*
+ * The pty is usually faster than shell, so it's a good idea to wait until
+ * the previous message has been already read by shell from slave before we
+ * write to master. This is necessary especially for EOF situation when we can
+ * send EOF to master before shell is fully initialized, to workaround this
+ * problem we wait until slave is empty. For example:
+ *
+ * echo "date" | su --pty
+ *
+ * Unfortunately, the child (usually shell) can ignore stdin at all, so we
+ * don't wait forever to avoid dead locks...
+ *
+ * Note that su --pty is primarily designed for interactive sessions as it
+ * maintains master+slave tty stuff within the session. Use pipe to write to
+ * pty and assume non-interactive (tee-like) behavior is NOT well supported.
+ */
+void ul_pty_write_eof_to_child(struct ul_pty *pty)
+{
+ unsigned int tries = 0;
+ struct pollfd fds[] = {
+ { .fd = pty->slave, .events = POLLIN }
+ };
+ char c = DEF_EOF;
+
+ DBG(IO, ul_debugobj(pty, " waiting for empty slave"));
+ while (poll(fds, 1, 10) == 1 && tries < 8) {
+ DBG(IO, ul_debugobj(pty, " slave is not empty"));
+ xusleep(250000);
+ tries++;
+ }
+ if (tries < 8)
+ DBG(IO, ul_debugobj(pty, " slave is empty now"));
+
+ DBG(IO, ul_debugobj(pty, " sending EOF to master"));
+ write_to_child(pty, &c, sizeof(char));
+}
+
+static int mainloop_callback(struct ul_pty *pty)
+{
+ int rc;
+
+ if (!pty->callbacks.mainloop)
+ return 0;
+
+ DBG(IO, ul_debugobj(pty, "calling mainloop callback"));
+ rc = pty->callbacks.mainloop(pty->callback_data);
+
+ DBG(IO, ul_debugobj(pty, " callback done [rc=%d]", rc));
+ return rc;
+}
+
+static int handle_io(struct ul_pty *pty, int fd, int *eof)
+{
+ char buf[BUFSIZ];
+ ssize_t bytes;
+ int rc = 0;
+
+ DBG(IO, ul_debugobj(pty, " handle I/O on fd=%d", fd));
+ *eof = 0;
+
+ /* read from active FD */
+ bytes = read(fd, buf, sizeof(buf));
+ if (bytes < 0) {
+ if (errno == EAGAIN || errno == EINTR)
+ return 0;
+ return -errno;
+ }
+
+ if (bytes == 0) {
+ *eof = 1;
+ return 0;
+ }
+
+ /* from stdin (user) to command */
+ if (fd == STDIN_FILENO) {
+ DBG(IO, ul_debugobj(pty, " stdin --> master %zd bytes", bytes));
+
+ if (write_to_child(pty, buf, bytes))
+ return -errno;
+
+ /* without sync write_output() will write both input &
+ * shell output that looks like double echoing */
+ fdatasync(pty->master);
+
+ /* from command (master) to stdout */
+ } else if (fd == pty->master) {
+ DBG(IO, ul_debugobj(pty, " master --> stdout %zd bytes", bytes));
+ write_output(buf, bytes);
+ }
+
+ if (pty->callbacks.log_stream_activity)
+ rc = pty->callbacks.log_stream_activity(
+ pty->callback_data, fd, buf, bytes);
+
+ return rc;
+}
+
+void ul_pty_wait_for_child(struct ul_pty *pty)
+{
+ int status;
+ pid_t pid;
+ int options = 0;
+
+ if (pty->child == (pid_t) -1)
+ return;
+
+ DBG(SIG, ul_debug("waiting for child [child=%d]", (int) pty->child));
+
+ if (ul_pty_is_running(pty)) {
+ /* wait for specific child */
+ options = WNOHANG;
+ for (;;) {
+ pid = waitpid(pty->child, &status, options);
+ DBG(SIG, ul_debug(" waitpid done [rc=%d]", (int) pid));
+ if (pid != (pid_t) - 1) {
+ if (pty->callbacks.child_die)
+ pty->callbacks.child_die(
+ pty->callback_data,
+ pty->child, status);
+ ul_pty_set_child(pty, (pid_t) -1);
+ } else
+ break;
+ }
+ } else {
+ /* final wait */
+ while ((pid = wait3(&status, options, NULL)) > 0) {
+ DBG(SIG, ul_debug(" wait3 done [rc=%d]", (int) pid));
+ if (pid == pty->child) {
+ if (pty->callbacks.child_die)
+ pty->callbacks.child_die(
+ pty->callback_data,
+ pty->child, status);
+ ul_pty_set_child(pty, (pid_t) -1);
+ }
+ }
+ }
+}
+
+static int handle_signal(struct ul_pty *pty, int fd)
+{
+ struct signalfd_siginfo info;
+ ssize_t bytes;
+ int rc = 0;
+
+ DBG(SIG, ul_debugobj(pty, " handle signal on fd=%d", fd));
+
+ bytes = read(fd, &info, sizeof(info));
+ if (bytes != sizeof(info)) {
+ if (bytes < 0 && (errno == EAGAIN || errno == EINTR))
+ return 0;
+ return -errno;
+ }
+
+ switch (info.ssi_signo) {
+ case SIGCHLD:
+ DBG(SIG, ul_debugobj(pty, " get signal SIGCHLD"));
+
+ if (info.ssi_code == CLD_EXITED
+ || info.ssi_code == CLD_KILLED
+ || info.ssi_code == CLD_DUMPED) {
+
+ if (pty->callbacks.child_wait)
+ pty->callbacks.child_wait(pty->callback_data,
+ pty->child);
+ else
+ ul_pty_wait_for_child(pty);
+
+ } else if (info.ssi_status == SIGSTOP && pty->child > 0)
+ pty->callbacks.child_sigstop(pty->callback_data,
+ pty->child);
+
+ if (pty->child <= 0) {
+ DBG(SIG, ul_debugobj(pty, " no child, setting leaving timeout"));
+ pty->poll_timeout = 10;
+ timerclear(&pty->next_callback_time);
+ }
+ return 0;
+ case SIGWINCH:
+ DBG(SIG, ul_debugobj(pty, " get signal SIGWINCH"));
+ if (pty->isterm) {
+ ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&pty->win);
+ ioctl(pty->slave, TIOCSWINSZ, (char *)&pty->win);
+
+ if (pty->callbacks.log_signal)
+ rc = pty->callbacks.log_signal(pty->callback_data,
+ &info, (void *) &pty->win);
+ }
+ break;
+ case SIGTERM:
+ /* fallthrough */
+ case SIGINT:
+ /* fallthrough */
+ case SIGQUIT:
+ DBG(SIG, ul_debugobj(pty, " get signal SIG{TERM,INT,QUIT}"));
+ pty->delivered_signal = info.ssi_signo;
+ /* Child termination is going to generate SIGCHILD (see above) */
+ if (pty->child > 0)
+ kill(pty->child, SIGTERM);
+
+ if (pty->callbacks.log_signal)
+ rc = pty->callbacks.log_signal(pty->callback_data,
+ &info, (void *) &pty->win);
+ break;
+ case SIGUSR1:
+ DBG(SIG, ul_debugobj(pty, " get signal SIGUSR1"));
+ if (pty->callbacks.flush_logs)
+ rc = pty->callbacks.flush_logs(pty->callback_data);
+ break;
+ default:
+ abort();
+ }
+
+ return rc;
+}
+
+/* loop in parent */
+int ul_pty_proxy_master(struct ul_pty *pty)
+{
+ int rc = 0, ret, eof = 0;
+ enum {
+ POLLFD_SIGNAL = 0,
+ POLLFD_MASTER,
+ POLLFD_STDIN
+
+ };
+ struct pollfd pfd[] = {
+ [POLLFD_SIGNAL] = { .fd = -1, .events = POLLIN | POLLERR | POLLHUP },
+ [POLLFD_MASTER] = { .fd = pty->master, .events = POLLIN | POLLERR | POLLHUP },
+ [POLLFD_STDIN] = { .fd = STDIN_FILENO, .events = POLLIN | POLLERR | POLLHUP }
+ };
+
+ /* We use signalfd and standard signals by handlers are blocked
+ * at all
+ */
+ assert(pty->sigfd >= 0);
+
+ pfd[POLLFD_SIGNAL].fd = pty->sigfd;
+ pty->poll_timeout = -1;
+
+ while (!pty->delivered_signal) {
+ size_t i;
+ int errsv, timeout;
+
+ DBG(IO, ul_debugobj(pty, "--poll() loop--"));
+
+ /* note, callback usually updates @next_callback_time */
+ if (timerisset(&pty->next_callback_time)) {
+ struct timeval now;
+
+ DBG(IO, ul_debugobj(pty, " callback requested"));
+ gettime_monotonic(&now);
+ if (timercmp(&now, &pty->next_callback_time, >)) {
+ rc = mainloop_callback(pty);
+ if (rc)
+ break;
+ }
+ }
+
+ /* set timeout */
+ if (timerisset(&pty->next_callback_time)) {
+ struct timeval now, rest;
+
+ gettime_monotonic(&now);
+ timersub(&pty->next_callback_time, &now, &rest);
+ timeout = (rest.tv_sec * 1000) + (rest.tv_usec / 1000);
+ } else
+ timeout = pty->poll_timeout;
+
+ /* wait for input, signal or timeout */
+ DBG(IO, ul_debugobj(pty, "calling poll() [timeout=%dms]", timeout));
+ ret = poll(pfd, ARRAY_SIZE(pfd), timeout);
+
+ errsv = errno;
+ DBG(IO, ul_debugobj(pty, "poll() rc=%d", ret));
+
+ /* error */
+ if (ret < 0) {
+ if (errsv == EAGAIN)
+ continue;
+ rc = -errno;
+ break;
+ }
+
+ /* timeout */
+ if (ret == 0) {
+ if (timerisset(&pty->next_callback_time)) {
+ rc = mainloop_callback(pty);
+ if (rc == 0)
+ continue;
+ } else
+ rc = 0;
+
+ DBG(IO, ul_debugobj(pty, "leaving poll() loop [timeout=%d, rc=%d]", timeout, rc));
+ break;
+ }
+ /* event */
+ for (i = 0; i < ARRAY_SIZE(pfd); i++) {
+ rc = 0;
+
+ if (pfd[i].revents == 0)
+ continue;
+
+ DBG(IO, ul_debugobj(pty, " active pfd[%s].fd=%d %s %s %s %s",
+ i == POLLFD_STDIN ? "stdin" :
+ i == POLLFD_MASTER ? "master" :
+ i == POLLFD_SIGNAL ? "signal" : "???",
+ pfd[i].fd,
+ pfd[i].revents & POLLIN ? "POLLIN" : "",
+ pfd[i].revents & POLLHUP ? "POLLHUP" : "",
+ pfd[i].revents & POLLERR ? "POLLERR" : "",
+ pfd[i].revents & POLLNVAL ? "POLLNVAL" : ""));
+
+ switch (i) {
+ case POLLFD_STDIN:
+ case POLLFD_MASTER:
+ /* data */
+ if (pfd[i].revents & POLLIN)
+ rc = handle_io(pty, pfd[i].fd, &eof);
+ /* EOF maybe detected by two ways:
+ * A) poll() return POLLHUP event after close()
+ * B) read() returns 0 (no data)
+ *
+ * POLLNVAL means that fd is closed.
+ */
+ if ((pfd[i].revents & POLLHUP) || (pfd[i].revents & POLLNVAL) || eof) {
+ DBG(IO, ul_debugobj(pty, " ignore FD"));
+ pfd[i].fd = -1;
+ if (i == POLLFD_STDIN) {
+ ul_pty_write_eof_to_child(pty);
+ DBG(IO, ul_debugobj(pty, " ignore STDIN"));
+ }
+ }
+ continue;
+ case POLLFD_SIGNAL:
+ rc = handle_signal(pty, pfd[i].fd);
+ break;
+ }
+ if (rc)
+ break;
+ }
+ }
+
+ pty_signals_cleanup(pty);
+
+ DBG(IO, ul_debug("poll() done [signal=%d, rc=%d]", pty->delivered_signal, rc));
+ return rc;
+}
+
+#ifdef TEST_PROGRAM_PTY
+/*
+ * $ make test_pty
+ * $ ./test_pty
+ *
+ * ... and see for example tty(1) or "ps afu"
+ */
+static void child_sigstop(void *data __attribute__((__unused__)), pid_t child)
+{
+ kill(getpid(), SIGSTOP);
+ kill(child, SIGCONT);
+}
+
+int main(int argc, char *argv[])
+{
+ struct ul_pty_callbacks *cb;
+ const char *shell, *command = NULL, *shname = NULL;
+ int caught_signal = 0;
+ pid_t child;
+ struct ul_pty *pty;
+
+ shell = getenv("SHELL");
+ if (shell == NULL)
+ shell = _PATH_BSHELL;
+ if (argc == 2)
+ command = argv[1];
+
+ ul_pty_init_debug(0);
+
+ pty = ul_new_pty(isatty(STDIN_FILENO));
+ if (!pty)
+ err(EXIT_FAILURE, "failed to allocate PTY handler");
+
+ cb = ul_pty_get_callbacks(pty);
+ cb->child_sigstop = child_sigstop;
+
+ if (ul_pty_setup(pty))
+ err(EXIT_FAILURE, "failed to create pseudo-terminal");
+
+ fflush(stdout); /* ??? */
+
+ switch ((int) (child = fork())) {
+ case -1: /* error */
+ ul_pty_cleanup(pty);
+ err(EXIT_FAILURE, "cannot create child process");
+ break;
+
+ case 0: /* child */
+ ul_pty_init_slave(pty);
+
+ signal(SIGTERM, SIG_DFL); /* because /etc/csh.login */
+
+ shname = strrchr(shell, '/');
+ shname = shname ? shname + 1 : shell;
+
+ if (command)
+ execl(shell, shname, "-c", command, (char *)NULL);
+ else
+ execl(shell, shname, "-i", (char *)NULL);
+ err(EXIT_FAILURE, "failed to execute %s", shell);
+ break;
+
+ default:
+ break;
+ }
+
+ /* parent */
+ ul_pty_set_child(pty, child);
+
+ /* this is the main loop */
+ ul_pty_proxy_master(pty);
+
+ /* all done; cleanup and kill */
+ caught_signal = ul_pty_get_delivered_signal(pty);
+
+ if (!caught_signal && ul_pty_get_child(pty) != (pid_t)-1)
+ ul_pty_wait_for_child(pty); /* final wait */
+
+ if (caught_signal && ul_pty_get_child(pty) != (pid_t)-1) {
+ fprintf(stderr, "\nSession terminated, killing shell...");
+ kill(child, SIGTERM);
+ sleep(2);
+ kill(child, SIGKILL);
+ fprintf(stderr, " ...killed.\n");
+ }
+
+ ul_pty_cleanup(pty);
+ ul_free_pty(pty);
+ return EXIT_SUCCESS;
+}
+
+#endif /* TEST_PROGRAM */
+