lnav/test/scripty.cc
Timothy Stack 63dba408ec [stdin] Keep the stdin piper file in .lnav so that it can be reopened
When piping the output of a program into lnav, the data would
be dumped to the terminal on exit so that it would not be
lost.  Since that is a bit noisy, the temp file used to store
the data is now left in .lnav so that it can be reopened later.
Older stdin captures are automatically removed after a day.

Also took the opportunity to start using filesystem::path more.

Fixes #436
2019-07-29 22:18:32 -07:00

719 lines
22 KiB
C++

/**
* 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 <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <signal.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 <queue>
#include <algorithm>
#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;
};
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(),
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();
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(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<struct expect> 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<struct command> 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] -- <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"
" -t <file> The file where any input sent to the child process\n"
" should be stored.\n"
" -f <file> The file where any output from the child process\n"
" should be stored.\n"
" -r <file> The file containing the input to be sent to the child\n"
" process.\n"
" -e <file> 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, passin = false;
FILE *file;
scripty_data.sd_program_name = argv[0];
scripty_data.sd_looping = true;
while ((c = getopt(argc, argv, "ht:f:r:e:nsi")) != -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;
case 'i':
passin = true;
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(passin);
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 = false;
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;
}