/** * Copyright (c) 2007-2012, Timothy Stack * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Timothy Stack nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined HAVE_NCURSESW_CURSES_H # include #elif defined HAVE_NCURSESW_H # include #elif defined HAVE_NCURSES_CURSES_H # include #elif defined HAVE_NCURSES_H # include #elif defined HAVE_CURSES_H # include #else # error "SysV or X/Open-compatible Curses header file required" #endif #ifdef HAVE_PTY_H #include #endif #ifdef HAVE_UTIL_H #include #endif #ifdef HAVE_LIBUTIL_H #include #endif #include #include #include "auto_fd.hh" using namespace std; /** * An RAII class for opening a PTY and forking a child process. */ class child_term { public: class error : public std::exception { public: error(int err) : e_err(err) { }; int e_err; }; child_term() { struct winsize ws; auto_fd slave; memset(&ws, 0, sizeof(ws)); if (isatty(STDIN_FILENO) && tcgetattr(STDIN_FILENO, &this->ct_termios) == -1) { throw error(errno); } if (isatty(STDOUT_FILENO) && ioctl(STDOUT_FILENO, TIOCGWINSZ, &this->ct_winsize) == -1) { throw error(errno); } ws.ws_col = 80; ws.ws_row = 24; if (openpty(this->ct_master.out(), slave.out(), NULL, NULL, &ws) < 0) { throw error(errno); } if ((this->ct_child = fork()) == -1) throw error(errno); if (this->ct_child == 0) { this->ct_master.reset(); dup2(slave, STDIN_FILENO); dup2(slave, STDOUT_FILENO); setenv("TERM", "xterm-color", 1); } else { slave.reset(); } }; virtual ~child_term() { (void)this->wait_for_child(); if (isatty(STDIN_FILENO) && tcsetattr(STDIN_FILENO, TCSANOW, &this->ct_termios) == -1) { perror("tcsetattr"); } if (isatty(STDOUT_FILENO) && ioctl(STDOUT_FILENO, TIOCSWINSZ, &this->ct_winsize) == -1) { perror("ioctl"); } }; int wait_for_child(void) { int retval = -1; if (this->ct_child > 0) { kill(this->ct_child, SIGTERM); this->ct_child = -1; while (wait(&retval) < 0 && (errno == EINTR)); } return retval; }; bool is_child() { return this->ct_child == 0; }; pid_t get_child_pid() { return this->ct_child; }; int get_fd() { return this->ct_master; }; protected: pid_t ct_child; auto_fd ct_master; struct termios ct_termios; struct winsize ct_winsize; }; /** * @param fd The file descriptor to switch to raw mode. * @return Zero on success, -1 on error. */ static int tty_raw(int fd) { struct termios attr[1]; assert(fd >= 0); if (tcgetattr(fd, attr) == -1) return -1; attr->c_lflag &= ~(ECHO | ICANON | IEXTEN); attr->c_iflag &= ~(ICRNL | INPCK | ISTRIP | IXON); attr->c_cflag &= ~(CSIZE | PARENB); attr->c_cflag |= (CS8); attr->c_oflag &= ~(OPOST); attr->c_cc[VMIN] = 1; attr->c_cc[VTIME] = 0; return tcsetattr(fd, TCSANOW, attr); } static void dump_memory(FILE *dst, const char *src, int len) { int lpc; for (lpc = 0; lpc < len; lpc++) { fprintf(dst, "%02x", src[lpc]); } } static char *hex2bits(const char *src) { int len, pos = sizeof(int); char *retval; len = strlen(src) / 2; retval = new char[sizeof(uint32_t) + len]; *((uint32_t *)retval) = len; while ((size_t)pos < (sizeof(uint32_t) + len)) { int val; sscanf(src, "%2x", &val); src += 2; retval[pos] = (char)val; pos += 1; } return retval; } typedef enum { ET_NONE, ET_READ, } expect_type_t; struct expect_read { uint32_t er_length; char er_data[]; }; struct expect { expect_type_t e_type; union { struct expect_read *read; } e_arg; }; struct expect_read_state { uint32_t ers_pos; }; class expect_handler { public: expect_handler() { memset(&this->eh_state, 0, sizeof(this->eh_state)); }; int process_input(const char *buffer, size_t blen) { if (this->eh_queue.empty()) return 0; uint32_t &exp_pos = this->eh_state.es_read.ers_pos; struct expect &next = this->eh_queue.front(); int cmp_len = min((next.e_arg.read->er_length - exp_pos), (uint32_t)blen); char *exp_start = &next.e_arg.read->er_data[this->eh_state.es_read.ers_pos]; int retval = 0; assert(buffer != NULL || blen == 0); if (memcmp(exp_start, buffer, cmp_len) == 0) { exp_pos += cmp_len; if (exp_pos == next.e_arg.read->er_length) { retval = 1; if (!this->eh_queue.empty()) { exp_pos = 0; this->eh_queue.pop(); } } } else { printf("Detected output differences at offset %d, " "expecting:\n ", exp_pos); dump_memory(stdout, exp_start, cmp_len); printf("\nGot:\n "); dump_memory(stdout, buffer, cmp_len); retval = -1; } fprintf(stderr, "pi ret %d\n", retval); return retval; }; queue eh_queue; private: union { struct expect_read_state es_read; } eh_state; }; typedef enum { CT_SLEEP, CT_WRITE, } command_type_t; struct command { command_type_t c_type; union { char *b; } c_arg; }; static struct { const char *sd_program_name; sig_atomic_t sd_looping; pid_t sd_child_pid; const char *sd_to_child_name; FILE *sd_to_child; const char *sd_from_child_name; FILE *sd_from_child; queue sd_replay; bool sd_user_step; } scripty_data; static void sigchld(int sig) { } static void sigpass(int sig) { kill(scripty_data.sd_child_pid, sig); } static void usage(void) { const char *usage_msg = "usage: %s [-h] [-t to_child] [-f from_child] -- \n" "\n" "Recorder for TTY I/O from a child process." "\n" "Options:\n" " -h Print this message, then exit.\n" " -t The file where any input sent to the child process\n" " should be stored.\n" " -f The file where any output from the child process\n" " should be stored.\n" " -r The file containing the input to be sent to the child\n" " process.\n" " -e The file containing the expected output from the child\n" " process.\n" "\n" "Examples:\n" " To record a session for playback later:\n" " $ scripty -t input.0 -f output.0 -- myCursesApp\n" "\n" " To replay the recorded session:\n" " $ scripty -r input.0 -- myCursesApp\n"; fprintf(stderr, usage_msg, scripty_data.sd_program_name); } int main(int argc, char *argv[]) { int c, fd, retval = EXIT_SUCCESS; expect_handler ex_handler; bool passout = true; FILE *file; scripty_data.sd_program_name = argv[0]; scripty_data.sd_looping = true; while ((c = getopt(argc, argv, "ht:f:r:e:ns")) != -1) { switch (c) { case 'h': usage(); exit(retval); break; case 's': scripty_data.sd_user_step = true; break; case 't': scripty_data.sd_to_child_name = optarg; break; case 'f': scripty_data.sd_from_child_name = optarg; break; case 'e': if ((file = fopen(optarg, "r")) == NULL) { fprintf(stderr, "error: cannot open %s\n", optarg); retval = EXIT_FAILURE; } else { char line[32 * 1024]; while (fgets(line, sizeof(line), file)) { char *sp; if (line[0] == '#' || (sp = strchr(line, ' ')) == NULL) { } else { struct expect exp; *sp = '\0'; sp += 1; if (strcmp(line, "read") == 0) { exp.e_type = ET_READ; exp.e_arg.read = (struct expect_read *)hex2bits(sp); } else { fprintf(stderr, "error: unknown command -- %s\n", line); retval = EXIT_FAILURE; } ex_handler.eh_queue.push(exp); } } fclose(file); file = NULL; } break; case 'r': if ((file = fopen(optarg, "r")) == NULL) { fprintf(stderr, "error: cannot open %s\n", optarg); retval = EXIT_FAILURE; } else { char line[32 * 1024]; while (fgets(line, sizeof(line), file)) { char *sp; if (line[0] == '#' || (sp = strchr(line, ' ')) == NULL) { } else { struct command cmd; *sp = '\0'; sp += 1; if (strcmp(line, "sleep") == 0) { cmd.c_type = CT_SLEEP; } else if (strcmp(line, "write") == 0) { cmd.c_type = CT_WRITE; cmd.c_arg.b = hex2bits(sp); scripty_data.sd_replay.push(cmd); } else { fprintf(stderr, "error: unknown command -- %s\n", line); retval = EXIT_FAILURE; } } } fclose(file); file = NULL; } break; case 'n': passout = false; break; default: retval = EXIT_FAILURE; break; } } argc -= optind; argv += optind; if ((scripty_data.sd_to_child_name != NULL) && (scripty_data.sd_to_child = fopen(scripty_data.sd_to_child_name, "w")) == NULL) { fprintf(stderr, "error: unable to open %s -- %s\n", scripty_data.sd_to_child_name, strerror(errno)); retval = EXIT_FAILURE; } if (scripty_data.sd_from_child_name != NULL) { if (strcmp(scripty_data.sd_from_child_name, "-") == 0) { scripty_data.sd_from_child = stdout; } else if ((scripty_data.sd_from_child = fopen(scripty_data.sd_from_child_name, "w")) == NULL) { fprintf(stderr, "error: unable from open %s -- %s\n", scripty_data.sd_from_child_name, strerror(errno)); retval = EXIT_FAILURE; } } fd = open("/tmp/scripty.err", O_WRONLY|O_CREAT|O_APPEND, 0666); dup2(fd, STDERR_FILENO); close(fd); fprintf(stderr, "startup\n"); if (scripty_data.sd_to_child != NULL) fcntl(fileno(scripty_data.sd_to_child), F_SETFD, 1); if (scripty_data.sd_from_child != NULL) fcntl(fileno(scripty_data.sd_from_child), F_SETFD, 1); if (retval != EXIT_FAILURE) { child_term ct; if (ct.is_child()) { execvp(argv[0], argv); perror("execvp"); exit(-1); } else { int maxfd, out_len = 0; bool got_expected = true; bool got_user_step; struct timeval last, now; char out_buffer[8192]; fd_set read_fds; scripty_data.sd_child_pid = ct.get_child_pid(); signal(SIGINT, sigpass); signal(SIGTERM, sigpass); signal(SIGCHLD, sigchld); gettimeofday(&now, NULL); last = now; FD_ZERO(&read_fds); FD_SET(STDIN_FILENO, &read_fds); FD_SET(ct.get_fd(), &read_fds); fprintf(stderr, "goin in the loop\n"); tty_raw(STDIN_FILENO); if (!ex_handler.eh_queue.empty()) { got_expected = false; } maxfd = max(STDIN_FILENO, ct.get_fd()); while (scripty_data.sd_looping) { fd_set ready_rfds = read_fds; struct timeval diff, to; int rc; to.tv_sec = 0; to.tv_usec = 10000; rc = select(maxfd + 1, &ready_rfds, NULL, NULL, &to); if (rc == 0) { if (!got_expected) { switch (ex_handler.process_input(NULL, 0)) { case -1: scripty_data.sd_looping = false; retval = EXIT_FAILURE; break; case 0: break; case 1: got_expected = true; break; } } if (!scripty_data.sd_replay.empty() && got_expected && (!scripty_data.sd_user_step || got_user_step)) { struct command cmd = scripty_data.sd_replay.front(); int len; fprintf(stderr, " us %d got %d\n", scripty_data.sd_user_step, got_user_step); scripty_data.sd_replay.pop(); fprintf(stderr, "replay %zd\n", scripty_data.sd_replay.size()); switch (cmd.c_type) { case CT_SLEEP: break; case CT_WRITE: len = *((int *)cmd.c_arg.b); log_perror(write(ct.get_fd(), cmd.c_arg.b + sizeof(int), len)); delete [] cmd.c_arg.b; break; } got_user_step = false; got_expected = false; } } else if (rc < 0) { switch (errno) { case EINTR: break; default: fprintf(stderr, "select %s\n", strerror(errno)); scripty_data.sd_looping = false; break; } } else { char buffer[1024]; fprintf(stderr, "fds ready %d\n", rc); gettimeofday(&now, NULL); timersub(&now, &last, &diff); if (FD_ISSET(STDIN_FILENO, &ready_rfds)) { rc = read(STDIN_FILENO, buffer, sizeof(buffer)); if (rc < 0) { scripty_data.sd_looping = false; } else if (rc == 0) { FD_CLR(STDIN_FILENO, &read_fds); } else if (!scripty_data.sd_replay.empty()) { if (scripty_data.sd_user_step) { got_user_step = true; } } else { log_perror(write(ct.get_fd(), buffer, rc)); if (scripty_data.sd_to_child != NULL) { fprintf(scripty_data.sd_to_child, "sleep %ld.%06ld\n" "write ", (long)diff.tv_sec, (long)diff.tv_usec); dump_memory(scripty_data.sd_to_child, buffer, rc); fprintf(scripty_data.sd_to_child, "\n"); } if (scripty_data.sd_from_child != NULL) { fprintf(stderr, "do write %d\n", out_len); fprintf(scripty_data.sd_from_child, "read "); dump_memory(scripty_data.sd_from_child, out_buffer, out_len); fprintf(scripty_data.sd_from_child, "\n"); fprintf(scripty_data.sd_from_child, "# write "); dump_memory(scripty_data.sd_from_child, buffer, rc); fprintf(scripty_data.sd_from_child, "\n"); out_len = 0; } } } if (FD_ISSET(ct.get_fd(), &ready_rfds)) { rc = read(ct.get_fd(), buffer, sizeof(buffer)); fprintf(stderr, "read rc %d\n", rc); if (rc <= 0) { scripty_data.sd_looping = false; if (scripty_data.sd_from_child) { fprintf(scripty_data.sd_from_child, "read "); dump_memory(scripty_data.sd_from_child, out_buffer, out_len); fprintf(scripty_data.sd_from_child, "\n"); out_len = 0; } } else { if (passout) log_perror(write(STDOUT_FILENO, buffer, rc)); if (scripty_data.sd_from_child != NULL) { fprintf(stderr, "got out %d\n", rc); memcpy(&out_buffer[out_len], buffer, rc); out_len += rc; } switch (ex_handler.process_input(buffer, rc)) { case -1: scripty_data.sd_looping = false; retval = EXIT_FAILURE; break; case 0: break; case 1: got_expected = true; break; } } } } last = now; } } if (!ex_handler.eh_queue.empty()) { fprintf(stderr, "More input expected from child\n"); retval = EXIT_FAILURE; } retval = ct.wait_for_child() || retval; } if (scripty_data.sd_to_child != NULL) { fclose(scripty_data.sd_to_child); scripty_data.sd_to_child = NULL; } if (scripty_data.sd_from_child != NULL) { fclose(scripty_data.sd_from_child); scripty_data.sd_from_child = NULL; } return retval; }