]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
lib/pty-session: add generic PTY container code
authorKarel Zak <kzak@redhat.com>
Thu, 4 Jul 2019 09:57:11 +0000 (11:57 +0200)
committerKarel Zak <kzak@redhat.com>
Tue, 8 Oct 2019 11:11:54 +0000 (13:11 +0200)
The idea is to consolidate script(1), scriptlive(1) and su(1) --pty
and use the same code everywhere.

TODO: add callbacks for stdin/out logging (necessary for script(1)).

Signed-off-by: Karel Zak <kzak@redhat.com>
configure.ac
include/pty-session.h [new file with mode: 0644]
lib/Makemodule.am
lib/pty-session.c [new file with mode: 0644]

index 7e55a7d8273c774ab0bc753356862a931cdfd383..98a5a6c96b5a354c62fe5025d1d50ccfdf4fd1e4 100644 (file)
@@ -391,6 +391,7 @@ have_security_openpam_h=$ac_cv_header_security_openpam_h
 have_shadow_h=$ac_cv_header_shadow_h
 have_sys_signalfd_h=$ac_cv_header_sys_signalfd_h
 have_utmpx_h=$ac_cv_header_utmpx_h
+have_pty_h=$ac_cv_header_pty_h
 
 AS_CASE([$linux_os:$have_linux_version_h],
   [yes:no],
@@ -743,6 +744,12 @@ AS_IF([test "x$with_util" = xno], [
   UL_CHECK_LIB([util], [openpty])
 ])
 
+AS_IF([test "x$have_pty_h" = xyes -a "x$have_sys_signalfd_h" = xyes -a "x$have_util" = xyes], [
+   AM_CONDITIONAL([HAVE_PTY], [true])
+   AC_DEFINE([HAVE_PTY], [1], [have PTY support])
+], [
+   AM_CONDITIONAL([HAVE_PTY], [false])
+])
 
 AC_CHECK_TYPES([union semun], [], [], [[
 #include <sys/sem.h>
diff --git a/include/pty-session.h b/include/pty-session.h
new file mode 100644 (file)
index 0000000..908e75f
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * This code is in the public domain; do with it what you wish.
+ *
+ * Written by Karel Zak <kzak@redhat.com> in Jul 2019
+ */
+#ifndef UTIL_LINUX_PTY_SESSION_H
+#define UTIL_LINUX_PTY_SESSION_H
+
+#include <pty.h>
+#include <termios.h>
+#include <signal.h>
+
+struct ul_pty_callbacks {
+       void (*child_wait)(void *);
+       void (*child_sigstop)(void *);
+};
+
+struct ul_pty {
+       struct termios  stdin_attrs;    /* stdin and slave terminal runtime attributes */
+       int             master;         /* parent side */
+       int             slave;          /* child side */
+       int             sigfd;          /* signalfd() */
+       int             poll_timeout;
+       struct winsize  win;            /* terminal window size */
+       sigset_t        orgsig;         /* original signal mask */
+
+       int             delivered_signal;
+
+       struct ul_pty_callbacks callbacks;
+       void                    *callback_data;
+
+       pid_t           child;
+
+       unsigned int isterm:1;          /* is stdin terminal? */
+};
+
+void ul_pty_init_debug(int mask);
+struct ul_pty *ul_new_pty(int is_stdin_tty);
+
+sigset_t *ul_pty_get_orig_sigset(struct ul_pty *pty);
+int ul_pty_get_delivered_signal(struct ul_pty *pty);
+
+void ul_pty_set_callback_data(struct ul_pty *pty, void *data);
+void ul_pty_set_child(struct ul_pty *pty, pid_t child);
+
+struct ul_pty_callbacks *ul_pty_get_callbacks(struct ul_pty *pty);
+int ul_pty_is_running(struct ul_pty *pty);
+int ul_pty_setup(struct ul_pty *pty);
+void ul_pty_cleanup(struct ul_pty *pty);
+void ul_pty_init_slave(struct ul_pty *pty);
+int ul_pty_proxy_master(struct ul_pty *pty);
+
+#endif /* UTIL_LINUX_PTY_H */
index 3d989abb14d2838f88e32caa295967e7dda4abcc..ee5b68dbcb7a8a1ea012b3a41d9fc08a7cf77975 100644 (file)
@@ -88,7 +88,6 @@ check_PROGRAMS += \
        test_timeutils
 
 
-
 if LINUX
 if HAVE_CPU_SET_T
 check_PROGRAMS += test_cpuset
@@ -144,6 +143,14 @@ test_path_LDADD = $(LDADD)
 endif
 endif
 
+if HAVE_PTY
+check_PROGRAMS += test_pty
+test_pty_SOURCES = lib/pty-session.c \
+                  include/pty-session.h
+test_pty_CFLAGS = $(AM_CFLAGS) -DTEST_PROGRAM_PTY
+test_pty_LDADD = $(LDADD) libcommon.la -lutil
+endif
+
 if LINUX
 test_cpuset_SOURCES = lib/cpuset.c
 test_cpuset_CFLAGS = $(AM_CFLAGS) -DTEST_PROGRAM_CPUSET
diff --git a/lib/pty-session.c b/lib/pty-session.c
new file mode 100644 (file)
index 0000000..7bb4462
--- /dev/null
@@ -0,0 +1,586 @@
+/*
+ * 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 "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;
+}
+
+sigset_t *ul_pty_get_orig_sigset(struct ul_pty *pty)
+{
+       assert(pty);
+       return &pty->orgsig;
+}
+
+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;
+}
+
+/* it's active when signals are redurected to sigfd */
+int ul_pty_is_running(struct ul_pty *pty)
+{
+       assert(pty);
+       return pty->sigfd >= 0;
+}
+
+/* call me before fork() */
+int ul_pty_setup(struct ul_pty *pty)
+{
+       struct termios slave_attrs;
+       int rc;
+
+       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)
+                       return -errno;
+               ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&pty->win);
+               /* create master+slave */
+               rc = openpty(&pty->master, &pty->slave, NULL, &pty->stdin_attrs, &pty->win);
+
+               /* set the current terminal to raw mode; pty_cleanup() reverses this change on exit */
+               slave_attrs = pty->stdin_attrs;
+               cfmakeraw(&slave_attrs);
+               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) {
+                       tcgetattr(pty->slave, &slave_attrs);
+                       slave_attrs.c_lflag &= ~ECHO;
+                       tcsetattr(pty->slave, TCSANOW, &slave_attrs);
+               }
+       }
+
+       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;
+
+       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.
+ */
+static void 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 handle_io(struct ul_pty *pty, int fd, int *eof)
+{
+       char buf[BUFSIZ];
+       ssize_t bytes;
+
+       DBG(IO, ul_debugobj(pty, "%d FD active", 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);
+       }
+
+       return 0;
+}
+
+static int handle_signal(struct ul_pty *pty, int fd)
+{
+       struct signalfd_siginfo info;
+       ssize_t bytes;
+
+       DBG(SIG, ul_debugobj(pty, "signal FD %d active", 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)
+                       pty->callbacks.child_wait(pty->callback_data);
+
+               else if (info.ssi_status == SIGSTOP && pty->child > 0)
+                       pty->callbacks.child_sigstop(pty->callback_data);
+
+               if (pty->child <= 0)
+                       pty->poll_timeout = 10;
+               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);
+               }
+               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);
+               break;
+       default:
+               abort();
+       }
+
+       return 0;
+}
+
+/* loop in parent */
+int ul_pty_proxy_master(struct ul_pty *pty)
+{
+       sigset_t ourset;
+       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
+        */
+       sigfillset(&ourset);
+       if (sigprocmask(SIG_BLOCK, &ourset, NULL))
+               return -errno;
+
+       sigemptyset(&ourset);
+       sigaddset(&ourset, SIGCHLD);
+       sigaddset(&ourset, SIGWINCH);
+       sigaddset(&ourset, SIGALRM);
+       sigaddset(&ourset, SIGTERM);
+       sigaddset(&ourset, SIGINT);
+       sigaddset(&ourset, SIGQUIT);
+
+       if ((pty->sigfd = signalfd(-1, &ourset, SFD_CLOEXEC)) < 0) {
+               rc = -errno;
+               goto done;
+       }
+
+       pfd[POLLFD_SIGNAL].fd = pty->sigfd;
+       pty->poll_timeout = -1;
+
+       while (!pty->delivered_signal) {
+               size_t i;
+               int errsv;
+
+               DBG(IO, ul_debugobj(pty, "calling poll()"));
+
+               /* wait for input or signal */
+               ret = poll(pfd, ARRAY_SIZE(pfd), pty->poll_timeout);
+               errsv = errno;
+               DBG(IO, ul_debugobj(pty, "poll() rc=%d", ret));
+
+               if (ret < 0) {
+                       if (errsv == EAGAIN)
+                               continue;
+                       rc = -errno;
+                       break;
+               }
+               if (ret == 0) {
+                       DBG(IO, ul_debugobj(pty, "leaving poll() loop [timeout=%d]", pty->poll_timeout));
+                       rc = 0;
+                       break;
+               }
+
+               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",
+                                               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" : ""));
+                       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) */
+                               if ((pfd[i].revents & POLLHUP) || eof) {
+                                       DBG(IO, ul_debugobj(pty, " ignore FD"));
+                                       pfd[i].fd = -1;
+                                       if (i == POLLFD_STDIN) {
+                                               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;
+               }
+       }
+
+done:
+       if (pty->sigfd != -1)
+               close(pty->sigfd);
+       pty->sigfd = -1;
+
+       /* restore original setting */
+       sigprocmask(SIG_SETMASK, &pty->orgsig, NULL);
+
+       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"
+ */
+struct ptytest {
+       pid_t   child;
+       int     childstatus;
+
+       struct ul_pty *pty;
+};
+
+/* on child exit/dump/... */
+static void wait_for_child(void *data)
+{
+       struct ptytest *ss = (struct ptytest *) data;
+       int status;
+       pid_t pid;
+       int options = 0;
+
+       if (ss->child == (pid_t) -1)
+               return;
+
+       if (ul_pty_is_running(ss->pty)) {
+               /* wait for specific child */
+               options = WNOHANG;
+               for (;;) {
+                       pid = waitpid(ss->child, &status, options);
+                       if (pid != (pid_t) - 1) {
+                               ss->childstatus = status;
+                               ss->child = (pid_t) -1;
+                               ul_pty_set_child(ss->pty, (pid_t) -1);
+                       } else
+                               break;
+               }
+       } else {
+               /* final wait */
+               while ((pid = wait3(&status, options, NULL)) > 0) {
+                       if (pid == ss->child) {
+                               ss->childstatus = status;
+                               ss->child = (pid_t) -1;
+                               ul_pty_set_child(ss->pty, (pid_t) -1);
+                       }
+               }
+       }
+}
+
+static void child_sigstop(void *data)
+{
+       struct ptytest *ss = (struct ptytest *) data;
+       kill(getpid(), SIGSTOP);
+       kill(ss->child, SIGCONT);
+}
+
+int main(int argc, char *argv[])
+{
+       struct ptytest ss = { .child = (pid_t) -1 };
+       struct ul_pty_callbacks *cb;
+       const char *shell, *command = NULL, *shname = NULL;
+       int caught_signal = 0;
+
+       shell = getenv("SHELL");
+       if (shell == NULL)
+               shell = _PATH_BSHELL;
+       if (argc == 2)
+               command = argv[1];
+
+       ul_pty_init_debug(0);
+
+       ss.pty = ul_new_pty(isatty(STDIN_FILENO));
+       if (!ss.pty)
+               err(EXIT_FAILURE, "failed to allocate PTY handler");
+
+       ul_pty_set_callback_data(ss.pty, (void *) &ss);
+       cb = ul_pty_get_callbacks(ss.pty);
+       cb->child_wait = wait_for_child;
+       cb->child_sigstop = child_sigstop;
+
+       sigprocmask(SIG_BLOCK, NULL, ul_pty_get_orig_sigset(ss.pty));
+
+       if (ul_pty_setup(ss.pty))
+               err(EXIT_FAILURE, "failed to create pseudo-terminal");
+
+       fflush(stdout);                 /* ??? */
+
+       switch ((int) (ss.child = fork())) {
+       case -1: /* error */
+               ul_pty_cleanup(ss.pty);
+               err(EXIT_FAILURE, "cannot create child process");
+               break;
+
+       case 0: /* child */
+               ul_pty_init_slave(ss.pty);
+
+               signal(SIGTERM, SIG_DFL); /* because /etc/csh.login */
+
+               shname = strrchr(shell, '/');
+               shname = shname ? shname + 1 : shell;
+
+               if (command)
+                       execl(shell, shname, "-c", command, NULL);
+               else
+                       execl(shell, shname, "-i", NULL);
+               err(EXIT_FAILURE, "failed to execute %s", shell);
+               break;
+
+       default:
+               break;
+       }
+
+       /* parent */
+       ul_pty_set_child(ss.pty, ss.child);
+
+       /* this is the main loop */
+       ul_pty_proxy_master(ss.pty);
+
+       /* all done; cleanup and kill */
+       caught_signal = ul_pty_get_delivered_signal(ss.pty);
+
+       if (!caught_signal && ss.child != (pid_t)-1)
+               wait_for_child(&ss);    /* final wait */
+
+       if (caught_signal && ss.child != (pid_t)-1) {
+               fprintf(stderr, "\nSession terminated, killing shell...");
+               kill(ss.child, SIGTERM);
+               sleep(2);
+               kill(ss.child, SIGKILL);
+               fprintf(stderr, " ...killed.\n");
+       }
+
+       ul_pty_cleanup(ss.pty);
+       return EXIT_SUCCESS;
+}
+
+#endif /* TEST_PROGRAM */
+