From: Karel Zak Date: Thu, 3 Oct 2019 12:33:25 +0000 (+0200) Subject: script: use lib/pty-session X-Git-Tag: v2.35-rc1~120 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=ec10634e7ec4;p=thirdparty%2Futil-linux.git script: use lib/pty-session This patch consolidate pseudo-terminal stuff in util-linux. From now there is only one implementation used in su(1) --pty, scriptlive(1) and script(1). The new stuff is based on the original script(1) -- it means poll() and signalfd() based. Note that script(1) code does not provide fallback for systems/libc where is no openpty(). Signed-off-by: Karel Zak --- diff --git a/configure.ac b/configure.ac index 0649bced2b..fc8a9507f0 100644 --- a/configure.ac +++ b/configure.ac @@ -1822,7 +1822,7 @@ UL_REQUIRES_LINUX([setarch]) AM_CONDITIONAL([BUILD_SETARCH], [test "x$build_setarch" = xyes]) UL_BUILD_INIT([script], [check]) -UL_REQUIRES_HAVE([script], [sys_signalfd_h], [sys/signalfd.h header]) +UL_REQUIRES_HAVE([script], [pty]) AM_CONDITIONAL([BUILD_SCRIPT], [test "x$build_script" = xyes]) UL_BUILD_INIT([scriptreplay], [yes]) diff --git a/term-utils/Makemodule.am b/term-utils/Makemodule.am index 8b01ade8b8..57657ac44a 100644 --- a/term-utils/Makemodule.am +++ b/term-utils/Makemodule.am @@ -1,12 +1,12 @@ if BUILD_SCRIPT usrbin_exec_PROGRAMS += script dist_man_MANS += term-utils/script.1 -script_SOURCES = term-utils/script.c lib/monotonic.c +script_SOURCES = term-utils/script.c \ + lib/pty-session.c \ + include/pty-session.h \ + lib/monotonic.c script_CFLAGS = $(AM_CFLAGS) -Wno-format-y2k -script_LDADD = $(LDADD) libcommon.la $(REALTIME_LIBS) -if HAVE_UTIL -script_LDADD += -lutil -endif +script_LDADD = $(LDADD) libcommon.la $(MATH_LIBS) $(REALTIME_LIBS) -lutil if HAVE_UTEMPTER script_LDADD += -lutempter endif diff --git a/term-utils/script.c b/term-utils/script.c index 17a747394d..00b8d36b9c 100644 --- a/term-utils/script.c +++ b/term-utils/script.c @@ -1,5 +1,7 @@ /* - * Copyright (c) 1980 Regents of the University of California. + * Copyright (C) 1980 Regents of the University of California. + * Copyright (C) 2013-2019 Karel Zak + * * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -30,17 +32,6 @@ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ - -/* - * 1999-02-22 Arkadiusz Miśkiewicz - * - added Native Language Support - * - * 2000-07-30 Per Andreas Buer - added "q"-option - * - * 2014-05-30 Csaba Kos - * - fixed a rare deadlock after child termination - */ - #include #include #include @@ -75,16 +66,16 @@ #include "xalloc.h" #include "optutils.h" #include "signames.h" - +#include "pty-session.h" #include "debug.h" static UL_DEBUG_DEFINE_MASK(script); UL_DEBUG_DEFINE_MASKNAMES(script) = UL_DEBUG_EMPTY_MASKNAMES; #define SCRIPT_DEBUG_INIT (1 << 1) -#define SCRIPT_DEBUG_POLL (1 << 2) -#define SCRIPT_DEBUG_SIGNAL (1 << 3) -#define SCRIPT_DEBUG_IO (1 << 4) +#define SCRIPT_DEBUG_PTY (1 << 2) +#define SCRIPT_DEBUG_IO (1 << 3) +#define SCRIPT_DEBUG_SIGNAL (1 << 4) #define SCRIPT_DEBUG_MISC (1 << 5) #define SCRIPT_DEBUG_ALL 0xFFFF @@ -133,14 +124,9 @@ struct script_stream { }; struct script_control { - char *shell; /* shell to be executed */ - char *command; /* command to be executed */ uint64_t outsz; /* current output files size */ uint64_t maxsz; /* maximum output files size */ - int master; /* pseudoterminal master file descriptor */ - int slave; /* pseudoterminal slave file descriptor */ - struct script_stream out; /* output */ struct script_stream in; /* input */ @@ -152,31 +138,20 @@ struct script_control { int ttycols; int ttylines; - int poll_timeout; /* poll() timeout, used in end of execution */ + struct ul_pty *pty; pid_t child; /* child pid */ int childstatus; /* child process exit value */ - struct termios attrs; /* slave terminal runtime attributes */ - struct winsize win; /* terminal window size */ -#if !HAVE_LIBUTIL || !HAVE_PTY_H - char *line; /* terminal line */ -#endif + unsigned int append:1, /* append output */ rc_wanted:1, /* return child exit value */ flush:1, /* flush after each write */ quiet:1, /* suppress most output */ force:1, /* write output to links */ - isterm:1, /* is child process running as terminal */ - die:1; /* terminate program */ - - sigset_t sigset; /* catch SIGCHLD and SIGWINCH with signalfd() */ - sigset_t sigorg; /* original signal mask */ - int sigfd; /* file descriptor for signalfd() */ + isterm:1; /* is child process running as terminal */ }; -static void restore_tty(struct script_control *ctl, int mode); -static void __attribute__((__noreturn__)) fail(struct script_control *ctl); -static uint64_t log_info(struct script_control *ctl, const char *name, const char *msgfmt, ...); +static ssize_t log_info(struct script_control *ctl, const char *name, const char *msgfmt, ...); static void script_init_debug(void) { @@ -301,13 +276,15 @@ static struct script_log *log_associate(struct script_control *ctl, return log; } -static void log_close(struct script_control *ctl, +static int log_close(struct script_control *ctl, struct script_log *log, const char *msg, int status) { + int rc = 0; + if (!log->initialized) - return; + return 0; DBG(MISC, ul_debug("closing %s", log->filename)); @@ -340,18 +317,22 @@ static void log_close(struct script_control *ctl, break; } - if (close_stream(log->fp) != 0) - err(EXIT_FAILURE, "write failed: %s", log->filename); + if (close_stream(log->fp) != 0) { + warn(_("write failed: %s"), log->filename); + rc = -errno; + } log->fp = NULL; log->initialized = 0; + + return rc; } -static void log_start(struct script_control *ctl, +static int log_start(struct script_control *ctl, struct script_log *log) { if (log->initialized) - return; + return 0; DBG(MISC, ul_debug("opening %s", log->filename)); @@ -363,9 +344,8 @@ static void log_start(struct script_control *ctl, "a" UL_CLOEXECSTR : "w" UL_CLOEXECSTR); if (!log->fp) { - restore_tty(ctl, TCSANOW); warn(_("cannot open %s"), log->filename); - fail(ctl); + return -errno; } /* write header, etc. */ @@ -401,99 +381,115 @@ static void log_start(struct script_control *ctl, } log->initialized = 1; + return 0; } -static void start_logging(struct script_control *ctl) +static int start_logging(struct script_control *ctl) { size_t i; /* start all output logs */ - for (i = 0; i < ctl->out.nlogs; i++) - log_start(ctl, ctl->out.logs[i]); + for (i = 0; i < ctl->out.nlogs; i++) { + int rc = log_start(ctl, ctl->out.logs[i]); + if (rc) + return rc; + } /* start all input logs */ - for (i = 0; i < ctl->in.nlogs; i++) - log_start(ctl, ctl->in.logs[i]); + for (i = 0; i < ctl->in.nlogs; i++) { + int rc = log_start(ctl, ctl->in.logs[i]); + if (rc) + return rc; + } + return 0; } -static size_t log_write(struct script_control *ctl, +static ssize_t log_write(struct script_control *ctl, struct script_stream *stream, struct script_log *log, char *obuf, size_t bytes) { + int rc; + ssize_t ssz = 0; + struct timeval now, delta; + if (!log->fp) return 0; - DBG(IO, ul_debug(" writining %s", log->filename)); + DBG(IO, ul_debug(" writing [file=%s]", log->filename)); switch (log->format) { case SCRIPT_FMT_RAW: - if (fwrite_all(obuf, 1, bytes, log->fp)) { + DBG(IO, ul_debug(" log raw data")); + rc = fwrite_all(obuf, 1, bytes, log->fp); + if (rc) { warn(_("cannot write %s"), log->filename); - fail(ctl); + return rc; } + ssz = bytes; break; - case SCRIPT_FMT_TIMING_SIMPLE: - { - struct timeval now, delta; - int sz; - DBG(IO, ul_debug(" writing timing info")); + case SCRIPT_FMT_TIMING_SIMPLE: + DBG(IO, ul_debug(" log timing info")); gettime_monotonic(&now); timersub(&now, &log->oldtime, &delta); - sz = fprintf(log->fp, "%ld.%06ld %zd\n", + ssz = fprintf(log->fp, "%ld.%06ld %zd\n", (long)delta.tv_sec, (long)delta.tv_usec, bytes); + if (ssz < 0) + return -errno; + log->oldtime = now; - bytes = sz > 0 ? sz : 0; break; - } - case SCRIPT_FMT_TIMING_MULTI: - { - struct timeval now, delta; - int sz; - DBG(IO, ul_debug(" writing multi-stream timing info")); + case SCRIPT_FMT_TIMING_MULTI: + DBG(IO, ul_debug(" log multi-stream timing info")); gettime_monotonic(&now); timersub(&now, &log->oldtime, &delta); - sz = fprintf(log->fp, "%c %ld.%06ld %zd\n", + ssz = fprintf(log->fp, "%c %ld.%06ld %zd\n", stream->ident, (long)delta.tv_sec, (long)delta.tv_usec, bytes); + if (ssz < 0) + return -errno; + log->oldtime = now; - bytes = sz > 0 ? sz : 0; - } + break; default: break; } if (ctl->flush) fflush(log->fp); - - return bytes; + return ssz; } -static uint64_t log_stream_activity( +static ssize_t log_stream_activity( struct script_control *ctl, struct script_stream *stream, char *buf, size_t bytes) { size_t i; - uint64_t outsz = 0; + ssize_t outsz = 0; - for (i = 0; i < stream->nlogs; i++) - outsz += log_write(ctl, stream, stream->logs[i], buf, bytes); + for (i = 0; i < stream->nlogs; i++) { + ssize_t ssz = log_write(ctl, stream, stream->logs[i], buf, bytes); + + if (ssz < 0) + return ssz; + outsz += ssz; + } return outsz; } -static uint64_t log_signal(struct script_control *ctl, int signum, char *msgfmt, ...) +static ssize_t log_signal(struct script_control *ctl, int signum, char *msgfmt, ...) { struct script_log *log; struct timeval now, delta; char msg[BUFSIZ] = {0}; va_list ap; - int sz; + ssize_t sz; assert(ctl); @@ -526,15 +522,15 @@ static uint64_t log_signal(struct script_control *ctl, int signum, char *msgfmt, signum_to_signame(signum)); log->oldtime = now; - return sz > 0 ? sz : 0; + return sz; } -static uint64_t log_info(struct script_control *ctl, const char *name, const char *msgfmt, ...) +static ssize_t log_info(struct script_control *ctl, const char *name, const char *msgfmt, ...) { struct script_log *log; char msg[BUFSIZ] = {0}; va_list ap; - int sz; + ssize_t sz; assert(ctl); @@ -559,61 +555,22 @@ static uint64_t log_info(struct script_control *ctl, const char *name, const cha else sz = fprintf(log->fp, "H %f %s\n", 0.0, name); - return sz > 0 ? sz : 0; -} - -static void die_if_link(struct script_control *ctl, const char *filename) -{ - struct stat s; - - if (ctl->force) - return; - if (lstat(filename, &s) == 0 && (S_ISLNK(s.st_mode) || s.st_nlink > 1)) - errx(EXIT_FAILURE, - _("output file `%s' is a link\n" - "Use --force if you really want to use it.\n" - "Program not started."), filename); -} - -static void restore_tty(struct script_control *ctl, int mode) -{ - struct termios rtt; - - if (!ctl->isterm) - return; - - rtt = ctl->attrs; - tcsetattr(STDIN_FILENO, mode, &rtt); + return sz; } -static void enable_rawmode_tty(struct script_control *ctl) -{ - struct termios rtt; - - if (!ctl->isterm) - return; - - rtt = ctl->attrs; - cfmakeraw(&rtt); - rtt.c_lflag &= ~ECHO; - tcsetattr(STDIN_FILENO, TCSANOW, &rtt); -} -static void __attribute__((__noreturn__)) done_log(struct script_control *ctl, const char *msg) +static void log_done(struct script_control *ctl, const char *msg) { int status; size_t i; - DBG(MISC, ul_debug("done!")); - - restore_tty(ctl, TCSADRAIN); + DBG(MISC, ul_debug("stop logging")); if (WIFSIGNALED(ctl->childstatus)) status = WTERMSIG(ctl->childstatus) + 0x80; else status = WEXITSTATUS(ctl->childstatus); - DBG(MISC, ul_debug(" status=%d", status)); /* close all output logs */ @@ -623,441 +580,140 @@ static void __attribute__((__noreturn__)) done_log(struct script_control *ctl, c /* close all input logs */ for (i = 0; i < ctl->in.nlogs; i++) log_close(ctl, ctl->in.logs[i], msg, status); - - if (!ctl->quiet) - printf(_("Script done.\n")); - -#ifdef HAVE_LIBUTEMPTER - if (ctl->master >= 0) - utempter_remove_record(ctl->master); -#endif - kill(ctl->child, SIGTERM); /* make sure we don't create orphans */ - - exit(ctl->rc_wanted ? status : EXIT_SUCCESS); -} - -static void __attribute__((__noreturn__)) done(struct script_control *ctl) -{ - done_log(ctl, NULL); } -static void __attribute__((__noreturn__)) fail(struct script_control *ctl) -{ - DBG(MISC, ul_debug("fail!")); - kill(0, SIGTERM); - done(ctl); -} - -static void wait_for_child(struct script_control *ctl, int wait) +static void wait_for_child(void *data) { + struct script_control *ctl = (struct script_control *) data; int status; pid_t pid; - int options = wait ? 0 : WNOHANG; + int options = 0; - DBG(MISC, ul_debug("waiting for child")); - - while ((pid = wait3(&status, options, NULL)) > 0) - if (pid == ctl->child) - ctl->childstatus = status; -} + if (ctl->child == (pid_t) -1) + return; -/* data from master to stdout */ -static void write_output(struct script_control *ctl, char *obuf, - ssize_t bytes) -{ - DBG(IO, ul_debug(" writing to output")); + DBG(MISC, ul_debug("waiting for child")); - if (write_all(STDOUT_FILENO, obuf, bytes)) { - DBG(IO, ul_debug(" writing output *failed*")); - warn(_("write failed")); - fail(ctl); + if (ul_pty_is_running(ctl->pty)) { + /* wait for specific child */ + options = WNOHANG; + for (;;) { + pid = waitpid(ctl->child, &status, options); + if (pid != (pid_t) - 1) { + ctl->childstatus = status; + ctl->child = (pid_t) -1; + ul_pty_set_child(ctl->pty, (pid_t) -1); + } else + break; + } + } else { + /* final wait */ + while ((pid = wait3(&status, options, NULL)) > 0) { + if (pid == ctl->child) { + ctl->childstatus = status; + ctl->child = (pid_t) -1; + ul_pty_set_child(ctl->pty, (pid_t) -1); + } + } } } -static int write_to_shell(struct script_control *ctl, - char *buf, size_t bufsz) +static void callback_child_sigstop(void *data) { - return write_all(ctl->master, buf, bufsz); + struct script_control *ctl = (struct script_control *) data; + + DBG(SIGNAL, ul_debug(" child stop by SIGSTOP -- stop parent too")); + kill(getpid(), SIGSTOP); + DBG(SIGNAL, ul_debug(" resume")); + kill(ctl->child, SIGCONT); } -/* - * The script(1) 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" | script - * - * Unfortunately, the child (usually shell) can ignore stdin at all, so we - * don't wait forever to avoid dead locks... - * - * Note that script is primarily designed for interactive sessions as it - * maintains master+slave tty stuff within the session. Use pipe to write to - * script(1) and assume non-interactive (tee-like) behavior is NOT well - * supported. - */ -static void write_eof_to_shell(struct script_control *ctl) +static int callback_log_stream_activity(void *data, int fd, char *buf, size_t bufsz) { - unsigned int tries = 0; - struct pollfd fds[] = { - { .fd = ctl->slave, .events = POLLIN } - }; - char c = DEF_EOF; + struct script_control *ctl = (struct script_control *) data; + ssize_t ssz = 0; - DBG(IO, ul_debug(" waiting for empty slave")); - while (poll(fds, 1, 10) == 1 && tries < 8) { - DBG(IO, ul_debug(" slave is not empty")); - xusleep(250000); - tries++; - } - if (tries < 8) - DBG(IO, ul_debug(" slave is empty now")); + DBG(IO, ul_debug("stream activity callback")); - DBG(IO, ul_debug(" sending EOF to master")); - write_to_shell(ctl, &c, sizeof(char)); -} + /* from stdin (user) to command */ + if (fd == STDIN_FILENO) + ssz = log_stream_activity(ctl, &ctl->in, buf, (size_t) bufsz); -static void handle_io(struct script_control *ctl, int fd, int *eof) -{ - char buf[BUFSIZ]; - ssize_t bytes; - DBG(IO, ul_debug("%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; - fail(ctl); - } + /* from command (master) to stdout and log */ + else if (fd == ul_pty_get_childfd(ctl->pty)) + ssz = log_stream_activity(ctl, &ctl->out, buf, (size_t) bufsz); - if (bytes == 0) { - *eof = 1; - return; - } + if (ssz < 0) + return (int) ssz; - /* from stdin (user) to command */ - if (fd == STDIN_FILENO) { - DBG(IO, ul_debug(" stdin --> master %zd bytes", bytes)); + DBG(IO, ul_debug(" append %ld bytes [summary=%zu, max=%zu]", ssz, + ctl->outsz, ctl->maxsz)); - if (write_to_shell(ctl, buf, bytes)) { - warn(_("write failed")); - fail(ctl); - } - /* without sync write_output() will write both input & - * shell output that looks like double echoing */ - fdatasync(ctl->master); - ctl->outsz += log_stream_activity(ctl, &ctl->in, buf, (size_t) bytes); + ctl->outsz += ssz; - /* from command (master) to stdout and log */ - } else if (fd == ctl->master) { - DBG(IO, ul_debug(" master --> stdout %zd bytes", bytes)); - write_output(ctl, buf, bytes); - ctl->outsz += log_stream_activity(ctl, &ctl->out, buf, (size_t) bytes); - } /* check output limit */ if (ctl->maxsz != 0 && ctl->outsz >= ctl->maxsz) { if (!ctl->quiet) printf(_("Script terminated, max output files size %"PRIu64" exceeded.\n"), ctl->maxsz); DBG(IO, ul_debug("output size %"PRIu64", exceeded limit %"PRIu64, ctl->outsz, ctl->maxsz)); - done_log(ctl, _("max output size exceeded")); + log_done(ctl, _("max output size exceeded")); + return 1; } + return 0; } -static void handle_signal(struct script_control *ctl, int fd) +static int callback_log_signal(void *data, struct signalfd_siginfo *info, void *sigdata) { - struct signalfd_siginfo info; - ssize_t bytes; + struct script_control *ctl = (struct script_control *) data; + ssize_t ssz = 0; - DBG(SIGNAL, ul_debug("signal FD %d active", fd)); - - bytes = read(fd, &info, sizeof(info)); - if (bytes != sizeof(info)) { - if (bytes < 0 && (errno == EAGAIN || errno == EINTR)) - return; - fail(ctl); - } - - switch (info.ssi_signo) { - case SIGCHLD: - DBG(SIGNAL, ul_debug(" get signal SIGCHLD [ssi_code=%d, ssi_status=%d]", - info.ssi_code, info.ssi_status)); - if (info.ssi_code == CLD_EXITED - || info.ssi_code == CLD_KILLED - || info.ssi_code == CLD_DUMPED) { - wait_for_child(ctl, 0); - ctl->poll_timeout = 10; - - /* In case of ssi_code is CLD_TRAPPED, CLD_STOPPED, or CLD_CONTINUED */ - } else if (info.ssi_status == SIGSTOP && ctl->child) { - DBG(SIGNAL, ul_debug(" child stop by SIGSTOP -- stop parent too")); - kill(getpid(), SIGSTOP); - DBG(SIGNAL, ul_debug(" resume")); - kill(ctl->child, SIGCONT); - } - return; + switch (info->ssi_signo) { case SIGWINCH: - DBG(SIGNAL, ul_debug(" get signal SIGWINCH")); - if (ctl->isterm) { - ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&ctl->win); - ioctl(ctl->slave, TIOCSWINSZ, (char *)&ctl->win); - log_signal(ctl, info.ssi_signo, - "ROWS=%d COLS=%d", - ctl->win.ws_row, - ctl->win.ws_col); - } + { + struct winsize *win = (struct winsize *) sigdata; + ssz = log_signal(ctl, info->ssi_signo, "ROWS=%d COLS=%d", + win->ws_row, win->ws_col); break; + } case SIGTERM: /* fallthrough */ case SIGINT: /* fallthrough */ case SIGQUIT: - log_signal(ctl, info.ssi_signo, NULL); - DBG(SIGNAL, ul_debug(" get signal SIG{TERM,INT,QUIT}")); - fprintf(stderr, _("\nSession terminated.\n")); - /* Child termination is going to generate SIGCHILD (see above) */ - kill(ctl->child, SIGTERM); - return; + ssz = log_signal(ctl, info->ssi_signo, NULL); + break; default: - abort(); - } - DBG(SIGNAL, ul_debug("signal handle on FD %d done", fd)); -} - -static void do_io(struct script_control *ctl) -{ - int ret, eof = 0; - enum { - POLLFD_SIGNAL = 0, - POLLFD_MASTER, - POLLFD_STDIN - - }; - struct pollfd pfd[] = { - [POLLFD_SIGNAL] = { .fd = ctl->sigfd, .events = POLLIN | POLLERR | POLLHUP }, - [POLLFD_MASTER] = { .fd = ctl->master, .events = POLLIN | POLLERR | POLLHUP }, - [POLLFD_STDIN] = { .fd = STDIN_FILENO, .events = POLLIN | POLLERR | POLLHUP } - }; - - while (!ctl->die) { - size_t i; - int errsv; - - DBG(POLL, ul_debug("calling poll()")); - - /* wait for input or signal */ - ret = poll(pfd, ARRAY_SIZE(pfd), ctl->poll_timeout); - errsv = errno; - DBG(POLL, ul_debug("poll() rc=%d", ret)); - - if (ret < 0) { - if (errsv == EAGAIN) - continue; - warn(_("poll failed")); - fail(ctl); - } - if (ret == 0) { - DBG(POLL, ul_debug("setting die=1")); - ctl->die = 1; - break; - } - - for (i = 0; i < ARRAY_SIZE(pfd); i++) { - if (pfd[i].revents == 0) - continue; - - DBG(POLL, ul_debug(" 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) - handle_io(ctl, 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(POLL, ul_debug(" ignore FD")); - pfd[i].fd = -1; - if (i == POLLFD_STDIN) { - write_eof_to_shell(ctl); - DBG(POLL, ul_debug(" ignore STDIN")); - } - } - continue; - case POLLFD_SIGNAL: - handle_signal(ctl, pfd[i].fd); - break; - } - } - } - - DBG(POLL, ul_debug("poll() done")); - - if (!ctl->die) - wait_for_child(ctl, 1); - - done(ctl); -} - -static void getslave(struct script_control *ctl) -{ -#ifndef HAVE_LIBUTIL - ctl->line[strlen("/dev/")] = 't'; - ctl->slave = open(ctl->line, O_RDWR | O_CLOEXEC); - if (ctl->slave < 0) { - warn(_("cannot open %s"), ctl->line); - fail(ctl); - } - if (ctl->isterm) { - tcsetattr(ctl->slave, TCSANOW, &ctl->attrs); - ioctl(ctl->slave, TIOCSWINSZ, (char *)&ctl->win); + /* no log */ + break; } -#endif - setsid(); - ioctl(ctl->slave, TIOCSCTTY, 0); -} - -/* don't use DBG() stuff here otherwise it will be in the typescript file */ -static void __attribute__((__noreturn__)) do_shell(struct script_control *ctl) -{ - char *shname; - - getslave(ctl); - - /* close things irrelevant for this process */ - close(ctl->master); - close(ctl->sigfd); - - dup2(ctl->slave, STDIN_FILENO); - dup2(ctl->slave, STDOUT_FILENO); - dup2(ctl->slave, STDERR_FILENO); - close(ctl->slave); - - ctl->master = -1; - - shname = strrchr(ctl->shell, '/'); - if (shname) - shname++; - else - shname = ctl->shell; - - sigprocmask(SIG_SETMASK, &ctl->sigorg, NULL); - - /* - * When invoked from within /etc/csh.login, script spawns a csh shell - * that spawns programs that cannot be killed with a SIGTERM. This is - * because csh has a documented behavior wherein it disables all - * signals when processing the /etc/csh.* files. - * - * Let's restore the default behavior. - */ - signal(SIGTERM, SIG_DFL); - if (access(ctl->shell, X_OK) == 0) { - if (ctl->command) - execl(ctl->shell, shname, "-c", ctl->command, NULL); - else - execl(ctl->shell, shname, "-i", NULL); - } else { - if (ctl->command) - execlp(shname, "-c", ctl->command, NULL); - else - execlp(shname, "-i", NULL); - } - warn(_("failed to execute %s"), ctl->shell); - fail(ctl); + return ssz < 0 ? ssz : 0; } - -static void getmaster(struct script_control *ctl) +static void die_if_link(struct script_control *ctl, const char *filename) { -#if defined(HAVE_LIBUTIL) && defined(HAVE_PTY_H) - int rc; - - ctl->isterm = isatty(STDIN_FILENO); - - if (ctl->isterm) { - if (tcgetattr(STDIN_FILENO, &ctl->attrs) != 0) - err(EXIT_FAILURE, _("failed to get terminal attributes")); - ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&ctl->win); - rc = openpty(&ctl->master, &ctl->slave, NULL, &ctl->attrs, &ctl->win); - } else - rc = openpty(&ctl->master, &ctl->slave, NULL, NULL, NULL); - - if (rc < 0) { - warn(_("openpty failed")); - fail(ctl); - } -#else - char *pty, *bank, *cp; - - ctl->isterm = isatty(STDIN_FILENO); - - pty = &ctl->line[strlen("/dev/ptyp")]; - for (bank = "pqrs"; *bank; bank++) { - ctl->line[strlen("/dev/pty")] = *bank; - *pty = '0'; - if (access(ctl->line, F_OK) != 0) - break; - for (cp = "0123456789abcdef"; *cp; cp++) { - *pty = *cp; - ctl->master = open(ctl->line, O_RDWR | O_CLOEXEC); - if (ctl->master >= 0) { - char *tp = &ctl->line[strlen("/dev/")]; - int ok; - - /* verify slave side is usable */ - *tp = 't'; - ok = access(ctl->line, R_OK | W_OK) == 0; - *tp = 'p'; - if (ok) { - if (ctl->isterm) { - tcgetattr(STDIN_FILENO, &ctl->attrs); - ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&ctl->win); - } - return; - } - close(ctl->master); - ctl->master = -1; - } - } - } - ctl->master = -1; - warn(_("out of pty's")); - fail(ctl); -#endif /* not HAVE_LIBUTIL */ + struct stat s; - DBG(IO, ul_debug("master fd: %d", ctl->master)); + if (ctl->force) + return; + if (lstat(filename, &s) == 0 && (S_ISLNK(s.st_mode) || s.st_nlink > 1)) + errx(EXIT_FAILURE, + _("output file `%s' is a link\n" + "Use --force if you really want to use it.\n" + "Program not started."), filename); } int main(int argc, char **argv) { struct script_control ctl = { -#if !HAVE_LIBUTIL || !HAVE_PTY_H - .line = "/dev/ptyXX", -#endif - .master = -1, - .slave = -1, - .out = { .ident = 'O' }, .in = { .ident = 'I' }, - - .poll_timeout = -1 }; - int ch, format = 0; + struct ul_pty_callbacks *cb; + int ch, format = 0, caught_signal = 0, rc = 0; const char *outfile = NULL, *infile = NULL; - const char *timingfile = NULL; + const char *timingfile = NULL, *shell = NULL, *command = NULL; enum { FORCE_OPTION = CHAR_MAX + 1 }; @@ -1098,6 +754,7 @@ int main(int argc, char **argv) close_stdout_atexit(); script_init_debug(); + ON_DBG(PTY, ul_pty_init_debug(0xFFFF)); while ((ch = getopt_long(argc, argv, "aB:c:efI:O:o:qm:T:t::Vh", longopts, NULL)) != -1) { @@ -1108,7 +765,7 @@ int main(int argc, char **argv) ctl.append = 1; break; case 'c': - ctl.command = optarg; + command = optarg; break; case 'e': ctl.rc_wanted = 1; @@ -1200,11 +857,21 @@ int main(int argc, char **argv) log_associate(&ctl, &ctl.in, timingfile, format); } - ctl.shell = getenv("SHELL"); - if (ctl.shell == NULL) - ctl.shell = _PATH_BSHELL; + shell = getenv("SHELL"); + if (!shell) + shell = _PATH_BSHELL; - getmaster(&ctl); + ctl.isterm = isatty(STDIN_FILENO); + ctl.pty = ul_new_pty(ctl.isterm); + if (!ctl.pty) + err(EXIT_FAILURE, "failed to allocate PTY handler"); + + ul_pty_set_callback_data(ctl.pty, (void *) &ctl); + cb = ul_pty_get_callbacks(ctl.pty); + cb->child_wait = wait_for_child; + cb->child_sigstop = callback_child_sigstop; + cb->log_stream_activity = callback_log_stream_activity; + cb->log_signal = callback_log_signal; if (!ctl.quiet) { printf(_("Script started")); @@ -1216,67 +883,120 @@ int main(int argc, char **argv) printf(_(", timing file is '%s'"), timingfile); printf(_(".\n")); } - enable_rawmode_tty(&ctl); + + if (ul_pty_setup(ctl.pty)) + err(EXIT_FAILURE, _("failed to create pseudo-terminal")); + + fflush(stdout); + + /* + * We have terminal, do not use err() from now, use "goto done" + */ #ifdef HAVE_LIBUTEMPTER - utempter_add_record(ctl.master, NULL); + utempter_add_record(ul_pty_get_childfd(ctl.pty), NULL); #endif - /* setup signal handler */ - sigemptyset(&ctl.sigset); - sigaddset(&ctl.sigset, SIGCHLD); - sigaddset(&ctl.sigset, SIGWINCH); - sigaddset(&ctl.sigset, SIGTERM); - sigaddset(&ctl.sigset, SIGINT); - sigaddset(&ctl.sigset, SIGQUIT); - /* block signals used for signalfd() to prevent the signals being - * handled according to their default dispositions */ - sigprocmask(SIG_BLOCK, &ctl.sigset, &ctl.sigorg); + switch ((int) (ctl.child = fork())) { + case -1: /* error */ + warn(_("cannot create child process")); + rc = -errno; + goto done; - if ((ctl.sigfd = signalfd(-1, &ctl.sigset, SFD_CLOEXEC)) < 0) - err(EXIT_FAILURE, _("cannot set signal handler")); + case 0: /* child */ + { + const char *shname; - DBG(SIGNAL, ul_debug("signal fd=%d", ctl.sigfd)); + ul_pty_init_slave(ctl.pty); - fflush(stdout); - ctl.child = fork(); + signal(SIGTERM, SIG_DFL); /* because /etc/csh.login */ - switch (ctl.child) { - case -1: /* error */ - warn(_("fork failed")); - fail(&ctl); + 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; - case 0: /* child */ - do_shell(&ctl); + } + default: break; - default: /* parent */ - start_logging(&ctl); - - if (timingfile && format == SCRIPT_FMT_TIMING_MULTI) { - char buf[FORMAT_TIMESTAMP_MAX]; - time_t tvec = script_time((time_t *)NULL); - - strtime_iso(&tvec, ISO_TIMESTAMP, buf, sizeof(buf)); - log_info(&ctl, "START_TIME", buf); - - if (ctl.isterm) { - init_terminal_info(&ctl); - log_info(&ctl, "TERM", ctl.ttytype); - log_info(&ctl, "TTY", ctl.ttyname); - log_info(&ctl, "COLUMNS", "%d", ctl.ttycols); - log_info(&ctl, "LINES", "%d", ctl.ttylines); - } - log_info(&ctl, "SHELL", ctl.shell); - log_info(&ctl, "TIMING_LOG", timingfile); - if (outfile) - log_info(&ctl, "OUTPUT_LOG", outfile); - if (infile) - log_info(&ctl, "INPUT_LOG", infile); + } + + /* parent */ + ul_pty_set_child(ctl.pty, ctl.child); + + rc = start_logging(&ctl); + if (rc) + goto done; + + if (timingfile && format == SCRIPT_FMT_TIMING_MULTI) { + char buf[FORMAT_TIMESTAMP_MAX]; + time_t tvec = script_time((time_t *)NULL); + + strtime_iso(&tvec, ISO_TIMESTAMP, buf, sizeof(buf)); + log_info(&ctl, "START_TIME", buf); + + if (ctl.isterm) { + init_terminal_info(&ctl); + log_info(&ctl, "TERM", ctl.ttytype); + log_info(&ctl, "TTY", ctl.ttyname); + log_info(&ctl, "COLUMNS", "%d", ctl.ttycols); + log_info(&ctl, "LINES", "%d", ctl.ttylines); } - do_io(&ctl); - break; + log_info(&ctl, "SHELL", shell); + if (command) + log_info(&ctl, "COMMAND", command); + log_info(&ctl, "TIMING_LOG", timingfile); + if (outfile) + log_info(&ctl, "OUTPUT_LOG", outfile); + if (infile) + log_info(&ctl, "INPUT_LOG", infile); + } + + /* this is the main loop */ + rc = ul_pty_proxy_master(ctl.pty); + + /* all done; cleanup and kill */ + caught_signal = ul_pty_get_delivered_signal(ctl.pty); + + if (!caught_signal && ctl.child != (pid_t)-1) + wait_for_child(&ctl); /* final wait */ + + if (caught_signal && ctl.child != (pid_t)-1) { + fprintf(stderr, "\nSession terminated, killing shell..."); + kill(ctl.child, SIGTERM); + sleep(2); + kill(ctl.child, SIGKILL); + fprintf(stderr, " ...killed.\n"); + } + +done: + ul_pty_cleanup(ctl.pty); + log_done(&ctl, NULL); + + if (!ctl.quiet) + printf(_("Script done.\n")); + +#ifdef HAVE_LIBUTEMPTER + if (ul_pty_get_childfd(ctl.pty) >= 0) + utempter_remove_record(ul_pty_get_childfd(ctl.pty)); +#endif + ul_free_pty(ctl.pty); + + /* default exit code */ + rc = rc ? EXIT_FAILURE : EXIT_SUCCESS; + + /* exit code based on child status */ + if (ctl.rc_wanted && rc == EXIT_SUCCESS) { + if (WIFSIGNALED(ctl.childstatus)) + rc = WTERMSIG(ctl.childstatus) + 0x80; + else + rc = WEXITSTATUS(ctl.childstatus); } - /* should not happen, all used functions are non-return */ - return EXIT_FAILURE; + DBG(MISC, ul_debug("done [rc=%d]", rc)); + return rc; } diff --git a/tests/expected/script/buffering-race b/tests/expected/script/buffering-race index d223252f1b..884ad00693 100644 --- a/tests/expected/script/buffering-race +++ b/tests/expected/script/buffering-race @@ -1,3 +1,3 @@ -Script started, file is /dev/null +Script started, output log file is '/dev/null'. Hallo World -Script done, file is /dev/null +Script done. diff --git a/tests/expected/script/replay b/tests/expected/script/replay index cefee37329..b2aff5e263 100644 --- a/tests/expected/script/replay +++ b/tests/expected/script/replay @@ -1,8 +1,8 @@ record script output with timing -Script started, file is typescript +Script started, output log file is 'typescript', timing file is 'timingfile'. hello world all done -Script done, file is typescript +Script done. replay script output hello world all done