1
1
mirror of https://github.com/tstack/lnav.git synced 2024-09-19 08:48:02 +03:00
lnav/test/scripty.cc
Suresh Sundriyal e62172fb63 [-Wformat] Fix scripty.cc again.
Seems like timeval is defined differently on OS X than on Linux. So
fixing the format specifier on one breaks it on the other. Explicitly
cast the value being passed in to fix the problem on both the platforms.
2015-04-02 19:06:46 -07:00

711 lines
16 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;
};
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 (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"
" -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;
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;
}