| /* |
| * 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, NULL); |
| else |
| execl(shell, shname, "-i", 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 */ |
| |