diff options
Diffstat (limited to 'src/utils/squish-pty.c')
| -rw-r--r-- | src/utils/squish-pty.c | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/src/utils/squish-pty.c b/src/utils/squish-pty.c new file mode 100644 index 0000000..c8375a5 --- /dev/null +++ b/src/utils/squish-pty.c @@ -0,0 +1,355 @@ +#define _GNU_SOURCE 1 +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stropts.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <termios.h> +#include <unistd.h> + +static void +fail_io (const char *msg, ...) + __attribute__ ((noreturn)) + __attribute__ ((format (printf, 1, 2))); + +/* Prints MSG, formatting as with printf(), + plus an error message based on errno, + and exits. */ +static void +fail_io (const char *msg, ...) +{ + va_list args; + + va_start (args, msg); + vfprintf (stderr, msg, args); + va_end (args); + + if (errno != 0) + fprintf (stderr, ": %s", strerror (errno)); + putc ('\n', stderr); + exit (EXIT_FAILURE); +} + +/* If FD is a terminal, configures it for noncanonical input mode + with VMIN and VTIME set as indicated. + If FD is not a terminal, has no effect. */ +static void +make_noncanon (int fd, int vmin, int vtime) +{ + if (isatty (fd)) + { + struct termios termios; + if (tcgetattr (fd, &termios) < 0) + fail_io ("tcgetattr"); + termios.c_lflag &= ~(ICANON | ECHO); + termios.c_cc[VMIN] = vmin; + termios.c_cc[VTIME] = vtime; + if (tcsetattr (fd, TCSANOW, &termios) < 0) + fail_io ("tcsetattr"); + } +} + +/* Make FD non-blocking if NONBLOCKING is true, + or blocking if NONBLOCKING is false. */ +static void +make_nonblocking (int fd, bool nonblocking) +{ + int flags = fcntl (fd, F_GETFL); + if (flags < 0) + fail_io ("fcntl"); + if (nonblocking) + flags |= O_NONBLOCK; + else + flags &= ~O_NONBLOCK; + if (fcntl (fd, F_SETFL, flags) < 0) + fail_io ("fcntl"); +} + +/* Handle a read or write on *FD, which is the pty if FD_IS_PTY + is true, that returned end-of-file or error indication RETVAL. + The system call is named CALL, for use in error messages. + Returns true if processing may continue, false if we're all + done. */ +static bool +handle_error (ssize_t retval, int *fd, bool fd_is_pty, const char *call) +{ + if (fd_is_pty) + { + if (retval < 0) + { + if (errno == EIO) + { + /* Slave side of pty has been closed. */ + return false; + } + else + fail_io (call); + } + else + return true; + } + else + { + if (retval == 0) + { + close (*fd); + *fd = -1; + return true; + } + else + fail_io (call); + } +} + +/* Copies data from stdin to PTY and from PTY to stdout until no + more data can be read or written. */ +static void +relay (int pty, int dead_child_fd) +{ + struct pipe + { + int in, out; + char buf[BUFSIZ]; + size_t size, ofs; + bool active; + }; + struct pipe pipes[2]; + + /* Make PTY, stdin, and stdout non-blocking. */ + make_nonblocking (pty, true); + make_nonblocking (STDIN_FILENO, true); + make_nonblocking (STDOUT_FILENO, true); + + /* Configure noncanonical mode on PTY and stdin to avoid + waiting for end-of-line. We want to minimize context + switching on PTY (for efficiency) and minimize latency on + stdin to avoid a laggy user experience. */ + make_noncanon (pty, 16, 1); + make_noncanon (STDIN_FILENO, 1, 0); + + memset (pipes, 0, sizeof pipes); + pipes[0].in = STDIN_FILENO; + pipes[0].out = pty; + pipes[1].in = pty; + pipes[1].out = STDOUT_FILENO; + + while (pipes[0].in != -1 || pipes[1].in != -1) + { + fd_set read_fds, write_fds; + int retval; + int i; + + FD_ZERO (&read_fds); + FD_ZERO (&write_fds); + for (i = 0; i < 2; i++) + { + struct pipe *p = &pipes[i]; + + /* Don't do anything with the stdin->pty pipe until we + have some data for the pty->stdout pipe. If we get + too eager, Bochs will throw away our input. */ + if (i == 0 && !pipes[1].active) + continue; + + if (p->in != -1 && p->size + p->ofs < sizeof p->buf) + FD_SET (p->in, &read_fds); + if (p->out != -1 && p->size > 0) + FD_SET (p->out, &write_fds); + } + FD_SET (dead_child_fd, &read_fds); + + do + { + retval = select (FD_SETSIZE, &read_fds, &write_fds, NULL, NULL); + } + while (retval < 0 && errno == EINTR); + if (retval < 0) + fail_io ("select"); + + if (FD_ISSET (dead_child_fd, &read_fds)) + { + /* Child died. Do final relaying. */ + struct pipe *p = &pipes[1]; + if (p->out == -1) + return; + make_nonblocking (STDOUT_FILENO, false); + for (;;) + { + ssize_t n; + + /* Write buffer. */ + while (p->size > 0) + { + n = write (p->out, p->buf + p->ofs, p->size); + if (n < 0) + fail_io ("write"); + else if (n == 0) + fail_io ("zero-length write"); + p->ofs += n; + p->size -= n; + } + p->ofs = 0; + + p->size = n = read (p->in, p->buf, sizeof p->buf); + if (n <= 0) + return; + } + } + + for (i = 0; i < 2; i++) + { + struct pipe *p = &pipes[i]; + if (p->in != -1 && FD_ISSET (p->in, &read_fds)) + { + ssize_t n = read (p->in, p->buf + p->ofs + p->size, + sizeof p->buf - p->ofs - p->size); + if (n > 0) + { + p->active = true; + p->size += n; + if (p->size == BUFSIZ && p->ofs != 0) + { + memmove (p->buf, p->buf + p->ofs, p->size); + p->ofs = 0; + } + } + else if (!handle_error (n, &p->in, p->in == pty, "read")) + return; + } + if (p->out != -1 && FD_ISSET (p->out, &write_fds)) + { + ssize_t n = write (p->out, p->buf + p->ofs, p->size); + if (n > 0) + { + p->ofs += n; + p->size -= n; + if (p->size == 0) + p->ofs = 0; + } + else if (!handle_error (n, &p->out, p->out == pty, "write")) + return; + } + } + } +} + +static int dead_child_fd; + +static void +sigchld_handler (int signo __attribute__ ((unused))) +{ + if (write (dead_child_fd, "", 1) < 0) + _exit (1); +} + +int +main (int argc __attribute__ ((unused)), char *argv[]) +{ + int master, slave; + char *name; + pid_t pid; + struct sigaction sa; + int pipe_fds[2]; + struct itimerval zero_itimerval, old_itimerval; + + if (argc < 2) + { + fprintf (stderr, + "usage: squish-pty COMMAND [ARG]...\n" + "Squishes both stdin and stdout into a single pseudoterminal,\n" + "which is passed as stdout to run the specified COMMAND.\n"); + return EXIT_FAILURE; + } + + /* Open master side of pty and get ready to open slave. */ + master = open ("/dev/ptmx", O_RDWR | O_NOCTTY); + if (master < 0) + fail_io ("open \"/dev/ptmx\""); + if (grantpt (master) < 0) + fail_io ("grantpt"); + if (unlockpt (master) < 0) + fail_io ("unlockpt"); + + /* Open slave side of pty. */ + name = ptsname (master); + if (name == NULL) + fail_io ("ptsname"); + slave = open (name, O_RDWR); + if (slave < 0) + fail_io ("open \"%s\"", name); + + /* System V implementations need STREAMS configuration for the + slave. */ + if (isastream (slave)) + { + if (ioctl (slave, I_PUSH, "ptem") < 0 + || ioctl (slave, I_PUSH, "ldterm") < 0) + fail_io ("ioctl"); + } + + /* Arrange to get notified when a child dies, by writing a byte + to a pipe fd. We really want to use pselect() and + sigprocmask(), but Solaris 2.7 doesn't have it. */ + if (pipe (pipe_fds) < 0) + fail_io ("pipe"); + dead_child_fd = pipe_fds[1]; + + memset (&sa, 0, sizeof sa); + sa.sa_handler = sigchld_handler; + sigemptyset (&sa.sa_mask); + sa.sa_flags = SA_RESTART; + if (sigaction (SIGCHLD, &sa, NULL) < 0) + fail_io ("sigaction"); + + /* Save the virtual interval timer, which might have been set + by the process that ran us. It really should be applied to + our child process. */ + memset (&zero_itimerval, 0, sizeof zero_itimerval); + if (setitimer (ITIMER_VIRTUAL, &zero_itimerval, &old_itimerval) < 0) + fail_io ("setitimer"); + + pid = fork (); + if (pid < 0) + fail_io ("fork"); + else if (pid != 0) + { + /* Running in parent process. */ + int status; + close (slave); + relay (master, pipe_fds[0]); + + /* If the subprocess has died, die in the same fashion. + In particular, dying from SIGVTALRM tells the pintos + script that we ran out of CPU time. */ + if (waitpid (pid, &status, WNOHANG) > 0) + { + if (WIFEXITED (status)) + return WEXITSTATUS (status); + else if (WIFSIGNALED (status)) + raise (WTERMSIG (status)); + } + return 0; + } + else + { + /* Running in child process. */ + if (setitimer (ITIMER_VIRTUAL, &old_itimerval, NULL) < 0) + fail_io ("setitimer"); + if (dup2 (slave, STDOUT_FILENO) < 0) + fail_io ("dup2"); + if (close (pipe_fds[0]) < 0 || close (pipe_fds[1]) < 0 + || close (slave) < 0 || close (master) < 0) + fail_io ("close"); + execvp (argv[1], argv + 1); + fail_io ("exec"); + } +} |
