diff options
Diffstat (limited to 'test/scripty.cc')
-rw-r--r-- | test/scripty.cc | 1153 |
1 files changed, 1153 insertions, 0 deletions
diff --git a/test/scripty.cc b/test/scripty.cc new file mode 100644 index 0000000..2f7bb94 --- /dev/null +++ b/test/scripty.cc @@ -0,0 +1,1153 @@ +/** + * 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 <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <termios.h> +#include <unistd.h> + +#include "config.h" + +#if defined HAVE_NCURSESW_CURSES_H +# include <ncursesw/curses.h> +#elif defined HAVE_NCURSESW_H +# include <ncursesw.h> +#elif defined HAVE_NCURSES_CURSES_H +# include <ncurses/curses.h> +#elif defined HAVE_NCURSES_H +# include <ncurses.h> +#elif defined HAVE_CURSES_H +# include <curses.h> +#else +# error "SysV or X/Open-compatible Curses header file required" +#endif + +#ifdef HAVE_PTY_H +# include <pty.h> +#endif + +#ifdef HAVE_UTIL_H +# include <util.h> +#endif + +#ifdef HAVE_LIBUTIL_H +# include <libutil.h> +#endif + +#include <algorithm> +#include <map> +#include <queue> +#include <sstream> +#include <string> +#include <utility> + +#include "base/auto_fd.hh" +#include "base/auto_mem.hh" +#include "base/string_util.hh" +#include "fmt/format.h" +#include "ghc/filesystem.hpp" +#include "styling.hh" +#include "termios_guard.hh" +#include "ww898/cp_utf8.hpp" + +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; + }; + + explicit child_term(bool passin) + { + 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(), nullptr, nullptr, &ws) + < 0) { + throw error(errno); + } + + if ((this->ct_child = fork()) == -1) + throw error(errno); + + if (this->ct_child == 0) { + this->ct_master.reset(); + + if (!passin) { + 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() + { + 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() const + { + return this->ct_child == 0; + }; + + pid_t get_child_pid() const + { + return this->ct_child; + }; + + int get_fd() const + { + 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] & 0xff); + } +} + +static std::vector<char> +hex2bits(const char* src) +{ + std::vector<char> retval; + + for (size_t lpc = 0; src[lpc] && isdigit(src[lpc]); lpc += 2) { + int val; + + sscanf(&src[lpc], "%2x", &val); + retval.push_back((char) val); + } + + return retval; +} + +static const char* +tstamp() +{ + static char buf[64]; + + struct timeval tv; + gettimeofday(&tv, nullptr); + strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S.", localtime(&tv.tv_sec)); + auto dlen = strlen(buf); + snprintf(&buf[dlen], sizeof(buf) - dlen, "%.06d", tv.tv_usec); + + return buf; +} + +typedef enum { + CT_WRITE, +} command_type_t; + +struct command { + command_type_t c_type; + vector<char> c_arg; +}; + +static struct { + const char* sd_program_name{nullptr}; + sig_atomic_t sd_looping{true}; + + pid_t sd_child_pid{-1}; + + ghc::filesystem::path sd_actual_name; + auto_mem<FILE> sd_from_child{fclose}; + ghc::filesystem::path sd_expected_name; + + deque<struct command> sd_replay; +} scripty_data; + +static const std::map<std::string, std::string> CSI_TO_DESC = { + {")0", "Use alt charset"}, + + {"[?1000l", "Don't Send Mouse X & Y"}, + {"[?1002l", "Don’t Use Cell Motion Mouse Tracking"}, + {"[?1006l", "Don't ..."}, + {"[?1h", "Application cursor keys"}, + {"[?1l", "Normal cursor keys"}, + {"[?47h", "Use alternate screen buffer"}, + {"[?47l", "Use normal screen buffer"}, + {"[2h", "Set Keyboard Action mode"}, + {"[4h", "Set Replace mode"}, + {"[12h", "Set Send/Receive mode"}, + {"[20h", "Set Normal Linefeed mode"}, + {"[2l", "Reset Keyboard Action mode"}, + {"[4l", "Reset Replace mode"}, + {"[12l", "Reset Send/Receive mode"}, + {"[20l", "Reset Normal Linefeed mode"}, + {"[2J", "Erase all"}, +}; + +struct term_machine { + enum class state { + NORMAL, + ESCAPE_START, + ESCAPE_FIXED_LENGTH, + ESCAPE_VARIABLE_LENGTH, + ESCAPE_OSC, + }; + + struct term_attr { + term_attr(size_t pos, const std::string& desc) + : ta_pos(pos), ta_end(pos), ta_desc({desc}) + { + } + + term_attr(size_t pos, size_t end, const std::string& desc) + : ta_pos(pos), ta_end(end), ta_desc({desc}) + { + } + + size_t ta_pos; + size_t ta_end; + std::vector<std::string> ta_desc; + }; + + term_machine(child_term& ct) : tm_child_term(ct) + { + this->clear(); + } + + ~term_machine() + { + this->flush_line(); + } + + void clear() + { + std::fill(begin(this->tm_line), end(this->tm_line), ' '); + this->tm_line_attrs.clear(); + this->tm_new_data = false; + } + + void add_line_attr(const std::string& desc) + { + if (!this->tm_line_attrs.empty() + && this->tm_line_attrs.back().ta_pos == this->tm_cursor_x) + { + this->tm_line_attrs.back().ta_desc.emplace_back(desc); + } else { + this->tm_line_attrs.emplace_back(this->tm_cursor_x, desc); + } + } + + void write_char(char ch) + { + if (isprint(ch)) { + require(ch); + + this->tm_new_data = true; + this->tm_line[this->tm_cursor_x++] = (unsigned char) ch; + } else { + switch (ch) { + case '\a': + this->flush_line(); + fprintf(scripty_data.sd_from_child, "CTRL bell\n"); + break; + case '\x08': + this->add_line_attr("backspace"); + if (this->tm_cursor_x > 0) { + this->tm_cursor_x -= 1; + } + break; + case '\r': + this->add_line_attr("carriage-return"); + this->tm_cursor_x = 0; + break; + case '\n': + this->flush_line(); + if (this->tm_cursor_y >= 0) { + this->tm_cursor_y += 1; + } + this->tm_cursor_x = 0; + break; + case '\x0e': + this->tm_shift_start = this->tm_cursor_x; + break; + case '\x0f': + if (this->tm_shift_start != this->tm_cursor_x) { + this->tm_line_attrs.emplace_back( + this->tm_shift_start, this->tm_cursor_x, "alt"); + } + break; + default: + require(ch); + this->tm_new_data = true; + this->tm_line[this->tm_cursor_x++] = (unsigned char) ch; + break; + } + } + } + + void flush_line() + { + if (std::exchange(this->tm_waiting_on_input, false) + && !this->tm_user_input.empty()) + { + fprintf(stderr, "%s:flush keys\n", tstamp()); + fprintf(scripty_data.sd_from_child, "K "); + dump_memory( + scripty_data.sd_from_child, this->tm_user_input.data(), 1); + fprintf(scripty_data.sd_from_child, "\n"); + this->tm_user_input.erase(this->tm_user_input.begin()); + } + if (this->tm_new_data || !this->tm_line_attrs.empty()) { + // fprintf(scripty_data.sd_from_child, "flush %d\n", + // this->tm_flush_count); + fprintf(stderr, "%s:flush %zu\n", tstamp(), this->tm_flush_count++); + fprintf( + scripty_data.sd_from_child, "S % 3d \u250B", this->tm_cursor_y); + for (auto uch : this->tm_line) { + ww898::utf::utf8::write(uch, [](auto ch) { + fputc(ch, scripty_data.sd_from_child); + }); + } + fprintf(scripty_data.sd_from_child, "\u250B\n"); + for (size_t lpc = 0; lpc < this->tm_line_attrs.size(); lpc++) { + const auto& ta = this->tm_line_attrs[lpc]; + auto full_desc = fmt::format( + "{}", + fmt::join(ta.ta_desc.begin(), ta.ta_desc.end(), ", ")); + int line_len; + + if (ta.ta_pos == ta.ta_end) { + line_len = fprintf( + scripty_data.sd_from_child, + "A %s%s %s", + repeat("\u00B7", ta.ta_pos).c_str(), + ((lpc + 1 < this->tm_line_attrs.size()) + && (ta.ta_pos == this->tm_line_attrs[lpc + 1].ta_pos)) + ? "\u251C" + : "\u2514", + full_desc.c_str()); + line_len -= 2 + ta.ta_pos; + } else { + line_len = fprintf( + scripty_data.sd_from_child, + "A %s%s%s\u251b %s", + std::string(ta.ta_pos, ' ').c_str(), + ((lpc + 1 < this->tm_line_attrs.size()) + && (ta.ta_pos == this->tm_line_attrs[lpc + 1].ta_pos)) + ? "\u2518" + : "\u2514", + std::string(ta.ta_end - ta.ta_pos - 1, '-').c_str(), + full_desc.c_str()); + line_len -= 4; + } + for (size_t lpc2 = lpc + 1; lpc2 < this->tm_line_attrs.size(); + lpc2++) { + auto bar_pos = 7 + this->tm_line_attrs[lpc2].ta_pos; + + if (bar_pos < line_len) { + continue; + } + line_len += fprintf( + scripty_data.sd_from_child, + "%s\u2502", + std::string(bar_pos - line_len, ' ').c_str()); + line_len -= 2; + } + fprintf(scripty_data.sd_from_child, "\n"); + } + this->clear(); + } + fflush(scripty_data.sd_from_child); + } + + std::vector<int> get_m_params() + { + std::vector<int> retval; + size_t index = 1; + + while (index < this->tm_escape_buffer.size()) { + int val, last; + + if (sscanf(&this->tm_escape_buffer[index], "%d%n", &val, &last) + == 1) { + retval.push_back(val); + index += last; + if (this->tm_escape_buffer[index] != ';') { + break; + } + index += 1; + } else { + break; + } + } + + return retval; + } + + void new_user_input(char ch) + { + this->tm_user_input.push_back(ch); + } + + void new_input(char ch) + { + if (this->tm_unicode_remaining > 0) { + this->tm_unicode_buffer.push_back(ch); + this->tm_unicode_remaining -= 1; + if (this->tm_unicode_remaining == 0) { + this->tm_new_data = true; + this->tm_line[this->tm_cursor_x++] + = ww898::utf::utf8::read([this]() { + auto retval = this->tm_unicode_buffer.front(); + + this->tm_unicode_buffer.pop_front(); + return retval; + }); + } + return; + } else { + auto utfsize = ww898::utf::utf8::char_size( + [ch]() { return std::make_pair(ch, 16); }); + + if (utfsize.unwrap() > 1) { + this->tm_unicode_remaining = utfsize.unwrap() - 1; + this->tm_unicode_buffer.push_back(ch); + return; + } + } + + switch (this->tm_state) { + case state::NORMAL: { + switch (ch) { + case '\x1b': { + this->tm_escape_buffer.clear(); + this->tm_state = state::ESCAPE_START; + break; + } + default: { + this->write_char(ch); + break; + } + } + break; + } + case state::ESCAPE_START: { + switch (ch) { + case '[': { + this->tm_escape_buffer.push_back(ch); + this->tm_state = state::ESCAPE_VARIABLE_LENGTH; + break; + } + case ']': { + this->tm_escape_buffer.push_back(ch); + this->tm_state = state::ESCAPE_OSC; + break; + } + case '(': + case ')': + case '*': + case '+': { + this->tm_state = state::ESCAPE_FIXED_LENGTH; + this->tm_escape_buffer.push_back(ch); + this->tm_escape_expected_size = 2; + break; + } + default: { + this->flush_line(); + switch (ch) { + case '7': + fprintf(scripty_data.sd_from_child, + "CTRL save cursor\n"); + break; + case '8': + fprintf(scripty_data.sd_from_child, + "CTRL restore cursor\n"); + break; + case '>': + fprintf(scripty_data.sd_from_child, + "CTRL Normal keypad\n"); + break; + default: { + fprintf(scripty_data.sd_from_child, + "CTRL %c\n", + ch); + break; + } + } + this->tm_state = state::NORMAL; + break; + } + } + break; + } + case state::ESCAPE_FIXED_LENGTH: { + this->tm_escape_buffer.push_back(ch); + if (this->tm_escape_buffer.size() + == this->tm_escape_expected_size) { + auto iter = CSI_TO_DESC.find( + std::string(this->tm_escape_buffer.data(), + this->tm_escape_buffer.size())); + this->flush_line(); + if (iter == CSI_TO_DESC.end()) { + fprintf(scripty_data.sd_from_child, + "CTRL %.*s\n", + (int) this->tm_escape_buffer.size(), + this->tm_escape_buffer.data()); + } else { + fprintf(scripty_data.sd_from_child, + "CTRL %s\n", + iter->second.c_str()); + } + this->tm_state = state::NORMAL; + } + break; + } + case state::ESCAPE_VARIABLE_LENGTH: { + this->tm_escape_buffer.push_back(ch); + if (isalpha(ch)) { + auto iter = CSI_TO_DESC.find( + std::string(this->tm_escape_buffer.data(), + this->tm_escape_buffer.size())); + if (iter == CSI_TO_DESC.end()) { + this->tm_escape_buffer.push_back('\0'); + switch (ch) { + case 'A': { + auto amount = this->get_m_params(); + int count = 1; + + if (!amount.empty()) { + count = amount[0]; + } + this->flush_line(); + this->tm_cursor_y -= count; + if (this->tm_cursor_y < 0) { + this->tm_cursor_y = 0; + } + break; + } + case 'B': { + auto amount = this->get_m_params(); + int count = 1; + + if (!amount.empty()) { + count = amount[0]; + } + this->flush_line(); + this->tm_cursor_y += count; + break; + } + case 'C': { + auto amount = this->get_m_params(); + int count = 1; + + if (!amount.empty()) { + count = amount[0]; + } + this->tm_cursor_x += count; + break; + } + case 'J': { + auto param = this->get_m_params(); + + this->flush_line(); + + auto region = param.empty() ? 0 : param[0]; + switch (region) { + case 0: + fprintf(scripty_data.sd_from_child, + "CSI Erase Below\n"); + break; + case 1: + fprintf(scripty_data.sd_from_child, + "CSI Erase Above\n"); + break; + case 2: + fprintf(scripty_data.sd_from_child, + "CSI Erase All\n"); + break; + case 3: + fprintf(scripty_data.sd_from_child, + "CSI Erase Saved Lines\n"); + break; + } + break; + } + case 'K': { + auto param = this->get_m_params(); + + this->flush_line(); + + auto region = param.empty() ? 0 : param[0]; + switch (region) { + case 0: + fprintf(scripty_data.sd_from_child, + "CSI Erase to Right\n"); + break; + case 1: + fprintf(scripty_data.sd_from_child, + "CSI Erase to Left\n"); + break; + case 2: + fprintf(scripty_data.sd_from_child, + "CSI Erase All\n"); + break; + } + break; + } + case 'H': { + auto coords = this->get_m_params(); + + if (coords.empty()) { + coords = {1, 1}; + } + this->flush_line(); + this->tm_cursor_y = coords[0]; + this->tm_cursor_x = coords[1] - 1; + break; + } + case 'r': { + auto region = this->get_m_params(); + + this->flush_line(); + fprintf(scripty_data.sd_from_child, + "CSI set scrolling region %d-%d\n", + region[0], + region[1]); + break; + } + case 'm': { + auto attrs = this->get_m_params(); + + if (attrs.empty()) { + this->add_line_attr("normal"); + } else if ((30 <= attrs[0]) && (attrs[0] <= 37)) + { + auto xt = xterm_colors(); + + this->add_line_attr(fmt::format( + "fg({})", + xt->tc_palette[attrs[0] - 30].xc_hex)); + } else if (attrs[0] == 38) { + auto xt = xterm_colors(); + + require(attrs[1] == 5); + this->add_line_attr(fmt::format( + "fg({})", + xt->tc_palette[attrs[2]].xc_hex)); + } else if ((40 <= attrs[0]) && (attrs[0] <= 47)) + { + auto xt = xterm_colors(); + + this->add_line_attr(fmt::format( + "bg({})", + xt->tc_palette[attrs[0] - 40].xc_hex)); + } else if (attrs[0] == 48) { + auto xt = xterm_colors(); + + require(attrs[1] == 5); + this->add_line_attr(fmt::format( + "bg({})", + xt->tc_palette[attrs[2]].xc_hex)); + } else { + switch (attrs[0]) { + case 1: + this->add_line_attr("bold"); + break; + case 4: + this->add_line_attr("underline"); + break; + case 5: + this->add_line_attr("blink"); + break; + case 7: + this->add_line_attr("inverse"); + break; + default: + this->add_line_attr( + this->tm_escape_buffer.data()); + break; + } + } + break; + } + default: + fprintf(stderr, "%s:missed %c\n", tstamp(), ch); + this->add_line_attr( + this->tm_escape_buffer.data()); + break; + } + } else { + this->flush_line(); + fprintf(scripty_data.sd_from_child, + "CSI %s\n", + iter->second.c_str()); + } + this->tm_state = state::NORMAL; + } else { + } + break; + } + case state::ESCAPE_OSC: { + if (ch == '\a') { + this->tm_escape_buffer.push_back('\0'); + + auto num = this->get_m_params(); + auto semi_index + = strchr(this->tm_escape_buffer.data(), ';'); + + switch (num[0]) { + case 0: { + this->flush_line(); + fprintf(scripty_data.sd_from_child, + "OSC Set window title: %s\n", + semi_index + 1); + break; + } + case 999: { + this->flush_line(); + this->tm_waiting_on_input = true; + if (!scripty_data.sd_replay.empty()) { + const auto& cmd + = scripty_data.sd_replay.front(); + + this->tm_user_input = cmd.c_arg; + write(this->tm_child_term.get_fd(), + this->tm_user_input.data(), + this->tm_user_input.size()); + + scripty_data.sd_replay.pop_front(); + } + break; + } + } + + this->tm_state = state::NORMAL; + } else { + this->tm_escape_buffer.push_back(ch); + } + break; + } + } + } + + child_term& tm_child_term; + bool tm_waiting_on_input{false}; + state tm_state{state::NORMAL}; + std::vector<char> tm_escape_buffer; + std::deque<uint8_t> tm_unicode_buffer; + size_t tm_unicode_remaining{0}; + size_t tm_escape_expected_size{0}; + uint32_t tm_line[80]; + bool tm_new_data{false}; + size_t tm_cursor_x{0}; + int tm_cursor_y{-1}; + size_t tm_shift_start{0}; + std::vector<term_attr> tm_line_attrs; + + std::vector<char> tm_user_input; + + size_t tm_flush_count{0}; +}; + +static void +sigchld(int sig) +{ +} + +static void +sigpass(int sig) +{ + kill(scripty_data.sd_child_pid, sig); +} + +static void +usage() +{ + const char* usage_msg + = "usage: %s [-h] [-t to_child] [-f from_child] -- <cmd>\n" + "\n" + "Recorder for TTY I/O from a child process." + "\n" + "Options:\n" + " -h Print this message, then exit.\n" + " -n Do not pass the output to the console.\n" + " -i Pass stdin to the child process instead of connecting\n" + " the child to the tty.\n" + " -a <file> The file where the actual I/O from/to the child " + "process\n" + " should be stored.\n" + " -e <file> The file containing the expected I/O from/to the " + "child\n" + " process.\n" + "\n" + "Examples:\n" + " To record a session for playback later:\n" + " $ scripty -a output.0 -- myCursesApp\n" + "\n" + " To replay the recorded session:\n" + " $ scripty -e 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; + bool passout = true, passin = false, prompt = false; + auto_mem<FILE> file(fclose); + + scripty_data.sd_program_name = argv[0]; + scripty_data.sd_looping = true; + + while ((c = getopt(argc, argv, "ha:e:nip")) != -1) { + switch (c) { + case 'h': + usage(); + exit(retval); + break; + case 'a': + scripty_data.sd_actual_name = optarg; + break; + case 'e': + scripty_data.sd_expected_name = optarg; + if ((file = fopen(optarg, "r")) == nullptr) { + fprintf( + stderr, "%s:error: cannot open %s\n", tstamp(), optarg); + retval = EXIT_FAILURE; + } else { + char line[32 * 1024]; + + while (fgets(line, sizeof(line), file)) { + if (line[0] == 'K') { + struct command cmd; + + cmd.c_type = CT_WRITE; + cmd.c_arg = hex2bits(&line[2]); + scripty_data.sd_replay.push_back(cmd); + } + } + } + break; + case 'n': + passout = false; + break; + case 'i': + passin = true; + break; + case 'p': + prompt = true; + break; + default: + fprintf(stderr, "%s:error: unknown flag -- %c\n", tstamp(), c); + retval = EXIT_FAILURE; + break; + } + } + + argc -= optind; + argv += optind; + + if (!scripty_data.sd_expected_name.empty() + && scripty_data.sd_actual_name.empty()) + { + scripty_data.sd_actual_name = scripty_data.sd_expected_name.filename(); + scripty_data.sd_actual_name += ".tmp"; + } + + if (!scripty_data.sd_actual_name.empty()) { + if ((scripty_data.sd_from_child + = fopen(scripty_data.sd_actual_name.c_str(), "w")) + == nullptr) + { + fprintf(stderr, + "error: unable to open %s -- %s\n", + scripty_data.sd_actual_name.c_str(), + strerror(errno)); + retval = EXIT_FAILURE; + } + } + + if (scripty_data.sd_from_child != nullptr) { + fcntl(fileno(scripty_data.sd_from_child), F_SETFD, 1); + } + + if (retval != EXIT_FAILURE) { + guard_termios gt(STDOUT_FILENO); + fd = open("/tmp/scripty.err", O_WRONLY | O_CREAT | O_APPEND, 0666); + dup2(fd, STDERR_FILENO); + close(fd); + fprintf(stderr, "%s:startup\n", tstamp()); + + child_term ct(passin); + + if (ct.is_child()) { + execvp(argv[0], argv); + perror("execvp"); + exit(-1); + } else { + int maxfd; + struct timeval last, now; + fd_set read_fds; + term_machine tm(ct); + size_t last_replay_size = scripty_data.sd_replay.size(); + + scripty_data.sd_child_pid = ct.get_child_pid(); + signal(SIGINT, sigpass); + signal(SIGTERM, sigpass); + + signal(SIGCHLD, sigchld); + + gettimeofday(&now, nullptr); + last = now; + + FD_ZERO(&read_fds); + FD_SET(STDIN_FILENO, &read_fds); + FD_SET(ct.get_fd(), &read_fds); + + fprintf(stderr, "%s:goin in the loop\n", tstamp()); + + tty_raw(STDIN_FILENO); + + 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, nullptr, nullptr, &to); + gettimeofday(&now, nullptr); + timersub(&now, &last, &diff); + if (diff.tv_sec > 10) { + fprintf(stderr, "%s:replay timed out!\n", tstamp()); + scripty_data.sd_looping = false; + kill(ct.get_child_pid(), SIGKILL); + retval = EXIT_FAILURE; + break; + } + if (rc == 0) { + } else if (rc < 0) { + switch (errno) { + case EINTR: + break; + default: + fprintf(stderr, + "%s:select %s\n", + tstamp(), + strerror(errno)); + kill(ct.get_child_pid(), SIGKILL); + scripty_data.sd_looping = false; + break; + } + } else { + char buffer[1024]; + + fprintf(stderr, "%s:fds ready %d\n", tstamp(), rc); + 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 { + log_perror(write(ct.get_fd(), buffer, rc)); + + for (ssize_t lpc = 0; lpc < rc; lpc++) { + fprintf(stderr, + "%s:to-child %02x\n", + tstamp(), + buffer[lpc] & 0xff); + tm.new_user_input(buffer[lpc]); + } + } + last = now; + } + if (FD_ISSET(ct.get_fd(), &ready_rfds)) { + rc = read(ct.get_fd(), buffer, sizeof(buffer)); + fprintf(stderr, "%s:read rc %d\n", tstamp(), rc); + if (rc <= 0) { + scripty_data.sd_looping = false; + } else { + if (passout) { + log_perror(write(STDOUT_FILENO, buffer, rc)); + } + if (scripty_data.sd_from_child != nullptr) { + for (size_t lpc = 0; lpc < rc; lpc++) { +#if 0 + fprintf(stderr, "%s:from-child %02x\n", + tstamp(), + buffer[lpc] & 0xff); +#endif + tm.new_input(buffer[lpc]); + if (scripty_data.sd_replay.size() + != last_replay_size) { + last = now; + last_replay_size + = scripty_data.sd_replay.size(); + } + } + } + } + } + } + } + } + + retval = ct.wait_for_child() || retval; + } + + if (retval == EXIT_SUCCESS && !scripty_data.sd_expected_name.empty()) { + auto cmd = fmt::format("diff -ua {} {}", + scripty_data.sd_expected_name.string(), + scripty_data.sd_actual_name.string()); + auto rc = system(cmd.c_str()); + if (rc != 0) { + if (prompt) { + char resp[4]; + + printf("Would you like to update the original file? (y/N) "); + fflush(stdout); + log_perror(scanf("%3s", resp)); + if (strcasecmp(resp, "y") == 0) { + printf("Updating: %s -> %s\n", + scripty_data.sd_actual_name.c_str(), + scripty_data.sd_expected_name.c_str()); + + auto options + = ghc::filesystem::copy_options::overwrite_existing; + ghc::filesystem::copy_file(scripty_data.sd_actual_name, + scripty_data.sd_expected_name, + options); + } else { + retval = EXIT_FAILURE; + } + } else { + fprintf(stderr, "%s:error: mismatch\n", tstamp()); + retval = EXIT_FAILURE; + } + } + } + + return retval; +} |