1415 lines
36 KiB
C
1415 lines
36 KiB
C
/*
|
|
* wdye.c: what did you expect: Lua-based Expect tool
|
|
*
|
|
* Copyright (c) 2025, Přemysl Eric Janouch <p@janouch.name>
|
|
*
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
* purpose with or without fee is hereby granted.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*
|
|
*/
|
|
|
|
#include "config.h"
|
|
#include "../../liberty.c"
|
|
|
|
#include <math.h>
|
|
|
|
#include <lua.h>
|
|
#include <lualib.h>
|
|
#include <lauxlib.h>
|
|
|
|
#include <sys/ioctl.h>
|
|
#include <termios.h>
|
|
#ifdef SOLARIS
|
|
#include <stropts.h>
|
|
#endif
|
|
|
|
#ifdef WITH_CURSES
|
|
#include <curses.h>
|
|
#include <term.h>
|
|
#endif
|
|
|
|
static int64_t
|
|
clock_msec (void)
|
|
{
|
|
#ifdef _POSIX_TIMERS
|
|
struct timespec tp;
|
|
hard_assert (clock_gettime (CLOCK_BEST, &tp) != -1);
|
|
return (int64_t) tp.tv_sec * 1000 + tp.tv_nsec / 1000000;
|
|
#else
|
|
struct timeval tp;
|
|
hard_assert (gettimeofday (&tp, NULL) != -1);
|
|
return (int64_t) tp.tv_sec * 1000 + *msec = tp.tv_usec / 1000;
|
|
#endif
|
|
}
|
|
|
|
// execvpe is a GNU extension, reimplement it.
|
|
static int
|
|
execvpe (const char *file, char *const argv[], char *const envp[])
|
|
{
|
|
const char *path = getenv ("PATH");
|
|
if (strchr (file, '/') || !path)
|
|
return execve (file, argv, envp);
|
|
|
|
struct strv dirs = strv_make ();
|
|
cstr_split (path, ":", false, &dirs);
|
|
char *name = NULL;
|
|
for (size_t i = 0; i < dirs.len; i++)
|
|
{
|
|
cstr_set (&name, xstrdup_printf ("%s/%s",
|
|
*dirs.vector[i] ? dirs.vector[i] : ".", file));
|
|
execve (name, argv, envp);
|
|
}
|
|
strv_free (&dirs);
|
|
return -1;
|
|
}
|
|
|
|
// This is a particularly inefficient algorithm, but it can match binary data.
|
|
static const char *
|
|
str_memmem (const struct str *haystack, const struct str *needle, bool nocase)
|
|
{
|
|
if (haystack->len < needle->len)
|
|
return NULL;
|
|
|
|
char *xhaystack = xmalloc (haystack->len + 1);
|
|
char *xneedle = xmalloc (needle->len + 1);
|
|
if (nocase)
|
|
{
|
|
for (size_t i = 0; i <= haystack->len; i++)
|
|
xhaystack[i] = tolower ((uint8_t) haystack->str[i]);
|
|
for (size_t i = 0; i <= needle->len; i++)
|
|
xneedle[i] = tolower ((uint8_t) needle->str[i]);
|
|
}
|
|
else
|
|
{
|
|
memcpy (xhaystack, haystack->str, haystack->len + 1);
|
|
memcpy (xneedle, needle->str, needle->len + 1);
|
|
}
|
|
|
|
const char *result = NULL;
|
|
for (size_t i = 0, end = haystack->len - needle->len; i <= end; i++)
|
|
if (!memcmp (xhaystack + i, xneedle, needle->len))
|
|
{
|
|
result = haystack->str + i;
|
|
break;
|
|
}
|
|
|
|
free (xhaystack);
|
|
free (xneedle);
|
|
return result;
|
|
}
|
|
|
|
// --- Pseudoterminal ----------------------------------------------------------
|
|
// This is largely taken from Advanced Programming in the UNIX® Environment,
|
|
// just without a bunch of bugs.
|
|
|
|
static int
|
|
ptym_open (char **pts_name)
|
|
{
|
|
int fdm = -1, err = 0;
|
|
if ((fdm = posix_openpt (O_RDWR | O_NOCTTY)) < 0)
|
|
return -1;
|
|
if (grantpt (fdm) < 0
|
|
|| unlockpt (fdm) < 0)
|
|
goto errout;
|
|
|
|
char *ptr = NULL;
|
|
if ((ptr = ptsname (fdm)) == NULL)
|
|
goto errout;
|
|
|
|
cstr_set (pts_name, xstrdup (ptr));
|
|
return fdm;
|
|
|
|
errout:
|
|
err = errno;
|
|
xclose (fdm);
|
|
errno = err;
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
ptys_open (const char *pts_name)
|
|
{
|
|
int fds = -1;
|
|
#ifdef SOLARIS
|
|
int err = 0, setup = 0;
|
|
#endif
|
|
if ((fds = open (pts_name, O_RDWR)) < 0)
|
|
return -1;
|
|
#ifdef SOLARIS
|
|
if ((setup = ioctl (fds, I_FIND, "ldterm")) < 0)
|
|
goto errout;
|
|
if (setup == 0)
|
|
{
|
|
if (ioctl (fds, I_PUSH, "ptem") < 0
|
|
|| ioctl (fds, I_PUSH, "ldterm") < 0)
|
|
goto errout;
|
|
|
|
if (ioctl (fds, I_PUSH, "ttcompat") < 0)
|
|
{
|
|
errout:
|
|
err = errno;
|
|
xclose (fds);
|
|
errno = err;
|
|
return -1;
|
|
}
|
|
}
|
|
#endif
|
|
return fds;
|
|
}
|
|
|
|
static pid_t
|
|
pty_fork (int *ptrfdm, char **slave_name,
|
|
const struct termios *slave_termios, const struct winsize *slave_winsize,
|
|
struct error **e)
|
|
{
|
|
int fdm = -1, fds = -1;
|
|
|
|
char *pts_name = NULL;
|
|
if ((fdm = ptym_open (&pts_name)) < 0)
|
|
{
|
|
error_set (e, "can't open master pty: %s", strerror (errno));
|
|
return -1;
|
|
}
|
|
if (slave_name != NULL)
|
|
cstr_set (slave_name, xstrdup (pts_name));
|
|
|
|
pid_t pid = fork ();
|
|
if (pid < 0)
|
|
{
|
|
error_set (e, "fork: %s", strerror (errno));
|
|
xclose (fdm);
|
|
}
|
|
else if (pid != 0)
|
|
*ptrfdm = fdm;
|
|
else
|
|
{
|
|
if (setsid () < 0)
|
|
exit_fatal ("setsid: %s", strerror (errno));
|
|
if ((fds = ptys_open (pts_name)) < 0)
|
|
exit_fatal ("can't open slave pty: %s", strerror (errno));
|
|
xclose (fdm);
|
|
|
|
#if defined BSD
|
|
if (ioctl (fds, TIOCSCTTY, (char *) 0) < 0)
|
|
exit_fatal ("TIOCSCTTY: %s", strerror (errno));
|
|
#endif
|
|
|
|
if (slave_termios != NULL
|
|
&& tcsetattr (fds, TCSANOW, slave_termios) < 0)
|
|
exit_fatal ("tcsetattr error on slave pty: %s", strerror (errno));
|
|
if (slave_winsize != NULL
|
|
&& ioctl (fds, TIOCSWINSZ, slave_winsize) < 0)
|
|
exit_fatal ("TIOCSWINSZ error on slave pty: %s", strerror (errno));
|
|
|
|
if (dup2 (fds, STDIN_FILENO) != STDIN_FILENO)
|
|
exit_fatal ("dup2 error to stdin");
|
|
if (dup2 (fds, STDOUT_FILENO) != STDOUT_FILENO)
|
|
exit_fatal ("dup2 error to stdout");
|
|
if (dup2 (fds, STDERR_FILENO) != STDERR_FILENO)
|
|
exit_fatal ("dup2 error to stderr");
|
|
if (fds != STDIN_FILENO && fds != STDOUT_FILENO && fds != STDERR_FILENO)
|
|
xclose (fds);
|
|
}
|
|
free (pts_name);
|
|
return pid;
|
|
}
|
|
|
|
// --- JSON --------------------------------------------------------------------
|
|
|
|
static void
|
|
write_json_string (FILE *output, const char *s, size_t len)
|
|
{
|
|
fputc ('"', output);
|
|
for (const char *last = s, *end = s + len; s != end; last = s)
|
|
{
|
|
// Here is where you realize the asciicast format is retarded for using
|
|
// JSON at all. (Consider multibyte characters at read() boundaries.)
|
|
int32_t codepoint = utf8_decode (&s, end - s);
|
|
if (codepoint < 0)
|
|
{
|
|
s++;
|
|
fprintf (output, "\\uFFFD");
|
|
continue;
|
|
}
|
|
|
|
switch (codepoint)
|
|
{
|
|
break; case '"': fprintf (output, "\\\"");
|
|
break; case '\\': fprintf (output, "\\\\");
|
|
break; case '\b': fprintf (output, "\\b");
|
|
break; case '\f': fprintf (output, "\\f");
|
|
break; case '\n': fprintf (output, "\\n");
|
|
break; case '\r': fprintf (output, "\\r");
|
|
break; case '\t': fprintf (output, "\\t");
|
|
break; default:
|
|
if (!utf8_validate_cp (codepoint))
|
|
fprintf (output, "\\uFFFD");
|
|
else if (codepoint < 32)
|
|
fprintf (output, "\\u%04X", codepoint);
|
|
else
|
|
fwrite (last, 1, s - last, output);
|
|
}
|
|
}
|
|
fputc ('"', output);
|
|
}
|
|
|
|
// --- Global state ------------------------------------------------------------
|
|
|
|
static struct
|
|
{
|
|
lua_State *L; ///< Lua state
|
|
lua_Number default_timeout; ///< Default expect timeout (s)
|
|
}
|
|
g =
|
|
{
|
|
.default_timeout = 10.,
|
|
};
|
|
|
|
static int
|
|
xlua_error_handler (lua_State *L)
|
|
{
|
|
// Don't add tracebacks when there's already one, and pass nil through.
|
|
const char *string = luaL_optstring (L, 1, NULL);
|
|
if (string && !strchr (string, '\n'))
|
|
{
|
|
luaL_traceback (L, L, string, 1);
|
|
lua_remove (L, 1);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static bool
|
|
xlua_getfield (lua_State *L, int idx, const char *name,
|
|
int expected, bool optional)
|
|
{
|
|
int found = lua_getfield (L, idx, name);
|
|
if (found == expected)
|
|
return true;
|
|
if (optional && found == LUA_TNIL)
|
|
return false;
|
|
|
|
const char *message = optional
|
|
? "invalid field \"%s\" (found: %s, expected: %s or nil)"
|
|
: "invalid or missing field \"%s\" (found: %s, expected: %s)";
|
|
return luaL_error (L, message, name,
|
|
lua_typename (L, found), lua_typename (L, expected));
|
|
}
|
|
|
|
static void
|
|
xlua_newtablecopy (lua_State *L, int idx, int first, int last)
|
|
{
|
|
int len = last - first + 1;
|
|
lua_createtable (L, len, 0);
|
|
if (idx < 0)
|
|
idx--;
|
|
|
|
for (lua_Integer i = 0; i < len; i++)
|
|
{
|
|
lua_rawgeti (L, idx, first + i);
|
|
lua_rawseti (L, -2, 1 + i);
|
|
}
|
|
}
|
|
|
|
// --- Patterns ----------------------------------------------------------------
|
|
|
|
#define XLUA_PATTERN_METATABLE "pattern"
|
|
|
|
enum pattern_kind
|
|
{
|
|
PATTERN_REGEX, ///< Regular expression match
|
|
PATTERN_EXACT, ///< Literal string match
|
|
PATTERN_TIMEOUT, ///< Timeout
|
|
PATTERN_EOF, ///< EOF condition
|
|
PATTERN_DEFAULT, ///< Either timeout or EOF condition
|
|
};
|
|
|
|
struct pattern
|
|
{
|
|
enum pattern_kind kind; ///< Tag
|
|
int ref_process; ///< Process for all except TIMEOUT
|
|
struct process *process; ///< Weak pointer to the process
|
|
regex_t *regex; ///< Regular expression for REGEX
|
|
struct str exact; ///< Exact match literal for EXACT
|
|
lua_Number timeout; ///< Timeout for TIMEOUT/DEFAULT (s)
|
|
bool nocase; ///< Case insensitive search
|
|
bool notransfer; ///< Do not consume process buffer
|
|
int ref_values; ///< Return values as a table reference
|
|
|
|
// Patterns are constructed in place, used once, and forgotten,
|
|
// so we can just shove anything extra in here.
|
|
|
|
struct error *e; ///< Error buffer
|
|
struct str input; ///< Matched input
|
|
regmatch_t *matches; ///< Match indexes within the input
|
|
bool eof; ///< End of file seen
|
|
};
|
|
|
|
static struct pattern *
|
|
pattern_new (lua_State *L, enum pattern_kind kind, int idx_process)
|
|
{
|
|
struct pattern *self = lua_newuserdata (L, sizeof *self);
|
|
luaL_setmetatable (L, XLUA_PATTERN_METATABLE);
|
|
memset (self, 0, sizeof *self);
|
|
|
|
self->kind = kind;
|
|
self->ref_process = LUA_NOREF;
|
|
self->exact = str_make ();
|
|
self->timeout = -1.;
|
|
self->ref_values = LUA_NOREF;
|
|
self->input = str_make ();
|
|
|
|
if (idx_process)
|
|
{
|
|
lua_pushvalue (L, idx_process);
|
|
self->process = lua_touserdata (L, -1);
|
|
self->ref_process = luaL_ref (L, LUA_REGISTRYINDEX);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
static int
|
|
xlua_pattern_gc (lua_State *L)
|
|
{
|
|
struct pattern *self = luaL_checkudata (L, 1, XLUA_PATTERN_METATABLE);
|
|
luaL_unref (L, LUA_REGISTRYINDEX, self->ref_process);
|
|
if (self->regex)
|
|
regex_free (self->regex);
|
|
str_free (&self->exact);
|
|
luaL_unref (L, LUA_REGISTRYINDEX, self->ref_values);
|
|
if (self->e)
|
|
error_free (self->e);
|
|
str_free (&self->input);
|
|
free (self->matches);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
xlua_pattern_index (lua_State *L)
|
|
{
|
|
struct pattern *self = luaL_checkudata (L, 1, XLUA_PATTERN_METATABLE);
|
|
if (!lua_isinteger (L, 2))
|
|
{
|
|
const char *key = luaL_checkstring (L, 2);
|
|
if (!strcmp (key, "process"))
|
|
lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_process);
|
|
else
|
|
return luaL_error (L, "not a readable property: %s", key);
|
|
return 1;
|
|
}
|
|
|
|
lua_Integer group = lua_tointeger (L, 2);
|
|
switch (self->kind)
|
|
{
|
|
case PATTERN_REGEX:
|
|
{
|
|
const regmatch_t *m = self->matches + group;
|
|
if (group < 0 || (size_t) group > self->regex->re_nsub
|
|
|| m->rm_so < 0 || m->rm_eo < 0 || (size_t) m->rm_eo > self->input.len)
|
|
lua_pushnil (L);
|
|
else
|
|
lua_pushlstring (L,
|
|
self->input.str + m->rm_so, m->rm_eo - m->rm_so);
|
|
return 1;
|
|
}
|
|
case PATTERN_EXACT:
|
|
case PATTERN_EOF:
|
|
case PATTERN_DEFAULT:
|
|
if (group != 0)
|
|
lua_pushnil (L);
|
|
else
|
|
lua_pushlstring (L, self->input.str, self->input.len);
|
|
return 1;
|
|
default:
|
|
return luaL_argerror (L, 1, "indexing unavailable for this pattern");
|
|
}
|
|
}
|
|
|
|
static bool
|
|
pattern_readtimeout (struct pattern *self, lua_State *L, int idx)
|
|
{
|
|
lua_rawgeti (L, idx, 1);
|
|
bool ok = lua_isnumber (L, -1);
|
|
lua_Number v = lua_tonumber (L, -1);
|
|
lua_pop (L, 1);
|
|
if (v != v)
|
|
luaL_error (L, "timeout is not a number");
|
|
if (ok)
|
|
self->timeout = v;
|
|
return ok;
|
|
}
|
|
|
|
static void
|
|
pattern_readflags (struct pattern *self, lua_State *L, int idx)
|
|
{
|
|
lua_getfield (L, idx, "nocase");
|
|
self->nocase = lua_toboolean (L, -1);
|
|
lua_getfield (L, idx, "notransfer");
|
|
self->notransfer = lua_toboolean (L, -1);
|
|
lua_pop (L, 2);
|
|
}
|
|
|
|
static luaL_Reg xlua_pattern_table[] =
|
|
{
|
|
{ "__gc", xlua_pattern_gc },
|
|
{ "__index", xlua_pattern_index },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
// --- Process -----------------------------------------------------------------
|
|
|
|
#define XLUA_PROCESS_METATABLE "process"
|
|
|
|
struct process
|
|
{
|
|
int terminal_fd; ///< Process stdin/stdout/stderr
|
|
pid_t pid; ///< Process ID or -1 if collected
|
|
int ref_term; ///< Terminal information
|
|
struct str buffer; ///< Terminal input buffer
|
|
int status; ///< Process status iff pid is -1
|
|
|
|
int64_t start; ///< Start timestamp (Unix msec)
|
|
FILE *asciicast; ///< asciicast script dump
|
|
};
|
|
|
|
static struct process *
|
|
process_new (lua_State *L)
|
|
{
|
|
struct process *self = lua_newuserdata (L, sizeof *self);
|
|
luaL_setmetatable (L, XLUA_PROCESS_METATABLE);
|
|
memset (self, 0, sizeof *self);
|
|
|
|
self->terminal_fd = -1;
|
|
self->pid = -1;
|
|
self->ref_term = LUA_NOREF;
|
|
self->buffer = str_make ();
|
|
return self;
|
|
}
|
|
|
|
static int
|
|
xlua_process_gc (lua_State *L)
|
|
{
|
|
struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
|
|
if (self->terminal_fd != -1)
|
|
xclose (self->terminal_fd);
|
|
if (self->pid != -1)
|
|
// The slave is in its own process group.
|
|
kill (-self->pid, SIGKILL);
|
|
luaL_unref (L, LUA_REGISTRYINDEX, self->ref_term);
|
|
str_free (&self->buffer);
|
|
if (self->asciicast)
|
|
fclose (self->asciicast);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
xlua_process_index (lua_State *L)
|
|
{
|
|
struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
|
|
const char *key = luaL_checkstring (L, 2);
|
|
if (*key != '_' && luaL_getmetafield (L, 1, key))
|
|
return 1;
|
|
|
|
if (!strcmp (key, "buffer"))
|
|
lua_pushlstring (L, self->buffer.str, self->buffer.len);
|
|
else if (!strcmp (key, "pid"))
|
|
lua_pushinteger (L, self->pid);
|
|
else if (!strcmp (key, "term"))
|
|
lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_term);
|
|
else
|
|
return luaL_error (L, "not a readable property: %s", key);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
xlua_process_send (lua_State *L)
|
|
{
|
|
struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
|
|
int nargs = lua_gettop (L);
|
|
for (int i = 2; i <= nargs; i++)
|
|
if (!lua_isstring (L, i))
|
|
return luaL_argerror (L, i, "need string arguments");
|
|
|
|
for (int i = 2; i <= nargs; i++)
|
|
{
|
|
size_t len = 0;
|
|
const char *arg = lua_tolstring (L, i, &len);
|
|
ssize_t written = write (self->terminal_fd, arg, len);
|
|
if (written == -1)
|
|
return luaL_error (L, "write failed: %s", strerror (errno));
|
|
else if (written != (ssize_t) len)
|
|
return luaL_error (L, "write failed: %s", "short write");
|
|
|
|
if (self->asciicast)
|
|
{
|
|
double timestamp = (clock_msec () - self->start) / 1000.;
|
|
fprintf (self->asciicast, "[%f, \"i\", ", timestamp);
|
|
write_json_string (self->asciicast, arg, len);
|
|
fprintf (self->asciicast, "]\n");
|
|
}
|
|
}
|
|
lua_pushvalue (L, 1);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
xlua_process_regex (lua_State *L)
|
|
{
|
|
(void) luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
|
|
luaL_checktype (L, 2, LUA_TTABLE);
|
|
if (lua_gettop (L) != 2)
|
|
return luaL_error (L, "too many arguments");
|
|
|
|
struct pattern *pattern = pattern_new (L, PATTERN_REGEX, 1);
|
|
pattern_readflags (pattern, L, 2);
|
|
|
|
int flags = REG_EXTENDED;
|
|
if (pattern->nocase)
|
|
flags |= REG_ICASE;
|
|
|
|
lua_rawgeti (L, 2, 1);
|
|
if (!lua_isstring (L, -1))
|
|
return luaL_error (L, "expected regular expression");
|
|
|
|
size_t len = 0;
|
|
const char *re = lua_tolstring (L, -1, &len);
|
|
if (!(pattern->regex = regex_compile (re, flags, &pattern->e)))
|
|
return luaL_error (L, "%s", pattern->e->message);
|
|
lua_pop (L, 1);
|
|
|
|
pattern->matches =
|
|
xcalloc (pattern->regex->re_nsub + 1, sizeof *pattern->matches);
|
|
|
|
xlua_newtablecopy (L, 2, 2, lua_rawlen (L, 2));
|
|
pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
xlua_process_exact (lua_State *L)
|
|
{
|
|
(void) luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
|
|
luaL_checktype (L, 2, LUA_TTABLE);
|
|
if (lua_gettop (L) != 2)
|
|
return luaL_error (L, "too many arguments");
|
|
|
|
struct pattern *pattern = pattern_new (L, PATTERN_EXACT, 1);
|
|
pattern_readflags (pattern, L, 2);
|
|
|
|
lua_rawgeti (L, 2, 1);
|
|
if (!lua_isstring (L, -1))
|
|
return luaL_error (L, "expected string literal");
|
|
|
|
size_t len = 0;
|
|
const char *literal = lua_tolstring (L, -1, &len);
|
|
str_append_data (&pattern->exact, literal, len);
|
|
lua_pop (L, 1);
|
|
|
|
xlua_newtablecopy (L, 2, 2, lua_rawlen (L, 2));
|
|
pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
xlua_process_eof (lua_State *L)
|
|
{
|
|
(void) luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
|
|
luaL_checktype (L, 2, LUA_TTABLE);
|
|
if (lua_gettop (L) != 2)
|
|
return luaL_error (L, "too many arguments");
|
|
|
|
struct pattern *pattern = pattern_new (L, PATTERN_EOF, 1);
|
|
pattern_readflags (pattern, L, 2);
|
|
|
|
xlua_newtablecopy (L, 2, 1, lua_rawlen (L, 2));
|
|
pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
xlua_process_default (lua_State *L)
|
|
{
|
|
(void) luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
|
|
luaL_checktype (L, 2, LUA_TTABLE);
|
|
if (lua_gettop (L) != 2)
|
|
return luaL_error (L, "too many arguments");
|
|
|
|
struct pattern *pattern = pattern_new (L, PATTERN_DEFAULT, 1);
|
|
pattern_readflags (pattern, L, 2);
|
|
|
|
int first = 1, last = lua_rawlen (L, 2);
|
|
if (pattern_readtimeout (pattern, L, 2))
|
|
first++;
|
|
|
|
xlua_newtablecopy (L, 2, first, last);
|
|
pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
xlua_process_wait (lua_State *L)
|
|
{
|
|
struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
|
|
bool nowait = luaL_opt(L, lua_toboolean, 2, false);
|
|
if (lua_gettop (L) > 2)
|
|
return luaL_error (L, "too many arguments");
|
|
|
|
int status = self->status;
|
|
restart:
|
|
if (self->pid != -1)
|
|
{
|
|
int options = 0;
|
|
if (nowait)
|
|
options |= WNOHANG;
|
|
|
|
pid_t pid = waitpid (self->pid, &status, options);
|
|
if (!pid)
|
|
return 0;
|
|
|
|
if (pid < 0)
|
|
{
|
|
if (errno == EINTR)
|
|
goto restart;
|
|
return luaL_error (L, "waitpid: %s", strerror (errno));
|
|
}
|
|
|
|
// We lose the ability to reliably kill the whole process group.
|
|
self->status = status;
|
|
self->pid = -1;
|
|
}
|
|
if (WIFEXITED (status))
|
|
{
|
|
lua_pushinteger (L, WEXITSTATUS (status));
|
|
lua_pushinteger (L, WEXITSTATUS (status));
|
|
lua_pushnil (L);
|
|
return 3;
|
|
}
|
|
if (WIFSIGNALED (status))
|
|
{
|
|
lua_pushinteger (L, 128 + WTERMSIG (status));
|
|
lua_pushnil (L);
|
|
lua_pushinteger (L, WTERMSIG (status));
|
|
return 3;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static bool
|
|
process_feed (struct process *self)
|
|
{
|
|
// Let's do this without O_NONBLOCK for now.
|
|
char buf[BUFSIZ] = "";
|
|
ssize_t n = read (self->terminal_fd, buf, sizeof buf);
|
|
if (n < 0)
|
|
{
|
|
if (errno == EINTR)
|
|
return true;
|
|
#ifdef __linux__
|
|
// https://unix.stackexchange.com/a/538271
|
|
if (errno == EIO)
|
|
return false;
|
|
#endif
|
|
|
|
print_warning ("read: %s", strerror (errno));
|
|
return false;
|
|
}
|
|
|
|
if (self->asciicast)
|
|
{
|
|
double timestamp = (clock_msec () - self->start) / 1000.;
|
|
fprintf (self->asciicast, "[%f, \"o\", ", timestamp);
|
|
write_json_string (self->asciicast, buf, n);
|
|
fprintf (self->asciicast, "]\n");
|
|
}
|
|
|
|
// TODO(p): Add match_max processing, limiting the buffer size.
|
|
str_append_data (&self->buffer, buf, n);
|
|
return n > 0;
|
|
}
|
|
|
|
static luaL_Reg xlua_process_table[] =
|
|
{
|
|
{ "__gc", xlua_process_gc },
|
|
{ "__index", xlua_process_index },
|
|
{ "send", xlua_process_send },
|
|
{ "regex", xlua_process_regex },
|
|
{ "exact", xlua_process_exact },
|
|
{ "eof", xlua_process_eof },
|
|
{ "default", xlua_process_default },
|
|
{ "wait", xlua_process_wait },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
// --- Terminal ----------------------------------------------------------------
|
|
|
|
struct terminfo_entry
|
|
{
|
|
enum { TERMINFO_BOOLEAN, TERMINFO_NUMERIC, TERMINFO_STRING } kind;
|
|
unsigned numeric;
|
|
char string[];
|
|
};
|
|
|
|
#ifdef WITH_CURSES
|
|
|
|
static bool
|
|
load_terminfo (const char *term, struct str_map *strings)
|
|
{
|
|
// Neither ncurses nor NetBSD curses need an actual terminal FD passed.
|
|
// We don't want them to read out the winsize, we just read the database.
|
|
int err = 0;
|
|
TERMINAL *saved_term = set_curterm (NULL);
|
|
if (setupterm ((char *) term, -1, &err) != OK)
|
|
{
|
|
set_curterm (saved_term);
|
|
return false;
|
|
}
|
|
|
|
for (size_t i = 0; boolfnames[i]; i++)
|
|
{
|
|
int flag = tigetflag (boolnames[i]);
|
|
if (flag <= 0)
|
|
continue;
|
|
|
|
struct terminfo_entry *entry = xcalloc (1, sizeof *entry + 1);
|
|
*entry = (struct terminfo_entry) { TERMINFO_BOOLEAN, true };
|
|
str_map_set (strings, boolfnames[i], entry);
|
|
}
|
|
for (size_t i = 0; numfnames[i]; i++)
|
|
{
|
|
int num = tigetnum (numnames[i]);
|
|
if (num < 0)
|
|
continue;
|
|
|
|
struct terminfo_entry *entry = xcalloc (1, sizeof *entry + 1);
|
|
*entry = (struct terminfo_entry) { TERMINFO_NUMERIC, num };
|
|
str_map_set (strings, numfnames[i], entry);
|
|
}
|
|
for (size_t i = 0; strfnames[i]; i++)
|
|
{
|
|
const char *str = tigetstr (strnames[i]);
|
|
if (!str || str == (const char *) -1)
|
|
continue;
|
|
|
|
size_t len = strlen (str) + 1;
|
|
struct terminfo_entry *entry = xcalloc (1, sizeof *entry + len);
|
|
*entry = (struct terminfo_entry) { TERMINFO_STRING, 0 };
|
|
memcpy (entry + 1, str, len);
|
|
str_map_set (strings, strfnames[i], entry);
|
|
}
|
|
del_curterm (set_curterm (saved_term));
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
|
|
// --- Library -----------------------------------------------------------------
|
|
|
|
struct spawn_context
|
|
{
|
|
struct str_map env; ///< Subprocess environment map
|
|
struct str_map term; ///< terminfo database
|
|
struct strv envv; ///< Subprocess environment vector
|
|
struct strv argv; ///< Subprocess argument vector
|
|
|
|
struct error *error; ///< Error
|
|
};
|
|
|
|
static struct spawn_context
|
|
spawn_context_make (void)
|
|
{
|
|
struct spawn_context self = {};
|
|
self.env = str_map_make (free);
|
|
self.term = str_map_make (free);
|
|
|
|
// XXX: It might make sense to enable starting from an empty environment.
|
|
for (char **p = environ; *p; p++)
|
|
{
|
|
const char *equals = strchr (*p, '=');
|
|
if (!equals)
|
|
continue;
|
|
|
|
char *key = xstrndup (*p, equals - *p);
|
|
str_map_set (&self.env, key, xstrdup (equals + 1));
|
|
free (key);
|
|
}
|
|
self.envv = strv_make ();
|
|
self.argv = strv_make ();
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
spawn_context_free (struct spawn_context *self)
|
|
{
|
|
str_map_free (&self->env);
|
|
str_map_free (&self->term);
|
|
strv_free (&self->envv);
|
|
strv_free (&self->argv);
|
|
|
|
if (self->error)
|
|
error_free (self->error);
|
|
}
|
|
|
|
// -0, +0, e
|
|
static void
|
|
environ_map_update (struct str_map *env, lua_State *L)
|
|
{
|
|
lua_pushnil (L);
|
|
while (lua_next (L, -2))
|
|
{
|
|
if (lua_type (L, -2) != LUA_TSTRING)
|
|
luaL_error (L, "environment maps must be keyed by strings");
|
|
|
|
const char *value = lua_tostring (L, -1);
|
|
str_map_set (env, lua_tostring (L, -2),
|
|
value ? xstrdup (value) : NULL);
|
|
lua_pop (L, 1);
|
|
}
|
|
}
|
|
|
|
// The environment will get pseudo-randomly reordered,
|
|
// which is fine by POSIX.
|
|
static void
|
|
environ_map_serialize (struct str_map *env, struct strv *envv)
|
|
{
|
|
struct str_map_iter iter = str_map_iter_make (env);
|
|
const char *value;
|
|
while ((value = str_map_iter_next (&iter)))
|
|
strv_append_owned (envv,
|
|
xstrdup_printf ("%s=%s", iter.link->key, value));
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static int
|
|
spawn_protected (lua_State *L)
|
|
{
|
|
struct spawn_context *ctx = lua_touserdata (L, 1);
|
|
|
|
// Step 1: Prepare process environment.
|
|
if (xlua_getfield (L, 2, "environ", LUA_TTABLE, true))
|
|
{
|
|
environ_map_update (&ctx->env, L);
|
|
lua_pop (L, 1);
|
|
}
|
|
char *term = str_map_find (&ctx->env, "TERM");
|
|
if (!term)
|
|
{
|
|
print_debug ("setting a default TERM");
|
|
str_map_set (&ctx->env, "TERM", (term = xstrdup ("dumb")));
|
|
}
|
|
environ_map_serialize (&ctx->env, &ctx->envv);
|
|
|
|
#ifdef WITH_CURSES
|
|
// Step 2: Load terminal information.
|
|
if (!load_terminfo (term, &ctx->term))
|
|
luaL_error (L, "failed to initialize terminfo for %s", term);
|
|
#endif
|
|
|
|
// Step 3: Prepare process command line.
|
|
size_t argc = lua_rawlen (L, 2);
|
|
for (size_t i = 1; i <= argc; i++)
|
|
{
|
|
lua_pushinteger (L, i);
|
|
lua_rawget (L, 2);
|
|
const char *arg = lua_tostring (L, -1);
|
|
if (!arg)
|
|
return luaL_error (L, "spawn arguments must be strings");
|
|
|
|
strv_append (&ctx->argv, arg);
|
|
lua_pop (L, 1);
|
|
}
|
|
if (ctx->argv.len < 1)
|
|
return luaL_error (L, "missing argument");
|
|
|
|
// Step 4: Create a process object.
|
|
// This will get garbage collected as appropriate on failure.
|
|
struct process *process = process_new (L);
|
|
|
|
// This could be made into an object that can adjust winsize/termios.
|
|
lua_createtable (L, 0, ctx->term.len);
|
|
struct str_map_iter iter = str_map_iter_make (&ctx->term);
|
|
const struct terminfo_entry *entry = NULL;
|
|
while ((entry = str_map_iter_next (&iter)))
|
|
{
|
|
lua_pushstring (L, iter.link->key);
|
|
switch (entry->kind)
|
|
{
|
|
break; case TERMINFO_BOOLEAN: lua_pushboolean (L, true);
|
|
break; case TERMINFO_NUMERIC: lua_pushinteger (L, entry->numeric);
|
|
break; case TERMINFO_STRING: lua_pushstring (L, entry->string);
|
|
break; default: lua_pushnil (L);
|
|
}
|
|
lua_settable (L, -3);
|
|
}
|
|
process->ref_term = luaL_ref (L, LUA_REGISTRYINDEX);
|
|
|
|
struct winsize ws = { .ws_row = 24, .ws_col = 80 };
|
|
if ((entry = str_map_find (&ctx->term, "lines"))
|
|
&& entry->kind == TERMINFO_NUMERIC)
|
|
ws.ws_row = entry->numeric;
|
|
if ((entry = str_map_find (&ctx->term, "columns"))
|
|
&& entry->kind == TERMINFO_NUMERIC)
|
|
ws.ws_col = entry->numeric;
|
|
|
|
// Step 5: Spawn the process, which gets a new process group.
|
|
process->pid =
|
|
pty_fork (&process->terminal_fd, NULL, NULL, &ws, &ctx->error);
|
|
if (process->pid < 0)
|
|
{
|
|
return luaL_error (L, "failed to spawn %s: %s",
|
|
ctx->argv.vector[0], ctx->error->message);
|
|
}
|
|
if (!process->pid)
|
|
{
|
|
execvpe (ctx->argv.vector[0], ctx->argv.vector, ctx->envv.vector);
|
|
print_error ("failed to spawn %s: %s",
|
|
ctx->argv.vector[0], strerror (errno));
|
|
// Or we could figure out when exactly to use statuses 126 and 127.
|
|
_exit (EXIT_FAILURE);
|
|
}
|
|
|
|
// Step 6: Create a log file.
|
|
if (getenv ("WDYE_LOGGING"))
|
|
{
|
|
const char *name = ctx->argv.vector[0];
|
|
const char *last_slash = strrchr (name, '/');
|
|
if (last_slash)
|
|
name = last_slash + 1;
|
|
|
|
char *path = xstrdup_printf ("%s-%s.%d.cast",
|
|
PROGRAM_NAME, name, (int) process->pid);
|
|
if (!(process->asciicast = fopen (path, "w")))
|
|
print_warning ("%s: %s", path, strerror (errno));
|
|
free (path);
|
|
}
|
|
process->start = clock_msec ();
|
|
if (process->asciicast)
|
|
{
|
|
fprintf (process->asciicast, "{\"version\": 2, "
|
|
"\"width\": %u, \"height\": %u, \"env\": {\"TERM\": \"%s\"}}\n",
|
|
ws.ws_col, ws.ws_row, term);
|
|
}
|
|
|
|
set_cloexec (process->terminal_fd);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
xlua_spawn (lua_State *L)
|
|
{
|
|
luaL_checktype (L, 1, LUA_TTABLE);
|
|
|
|
lua_pushcfunction (L, xlua_error_handler);
|
|
lua_pushcfunction (L, spawn_protected);
|
|
|
|
// There are way too many opportunities for Lua to throw,
|
|
// so maintain a context to clean up in one go.
|
|
struct spawn_context ctx = spawn_context_make ();
|
|
lua_pushlightuserdata (L, &ctx);
|
|
lua_rotate (L, 1, -1);
|
|
int result = lua_pcall (L, 2, 1, -4);
|
|
spawn_context_free (&ctx);
|
|
if (result)
|
|
return lua_error (L);
|
|
|
|
// Remove the error handler ("good programming practice").
|
|
lua_remove (L, -2);
|
|
return 1;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
struct expect_context
|
|
{
|
|
size_t patterns_len; ///< Number of patterns
|
|
struct pattern **patterns; ///< Pattern array
|
|
size_t pfds_len; ///< Number of distinct poll FDs
|
|
struct pollfd *pfds; ///< Distinct poll FDs
|
|
|
|
lua_Number first_timeout; ///< Nearest timeout value
|
|
lua_Number timeout; ///< Actually used timeout value
|
|
};
|
|
|
|
static void
|
|
expect_context_free (struct expect_context *self)
|
|
{
|
|
free (self->patterns);
|
|
free (self->pfds);
|
|
}
|
|
|
|
static bool
|
|
expect_has_fd (struct expect_context *ctx, int fd)
|
|
{
|
|
for (size_t i = 0; i < ctx->pfds_len; i++)
|
|
if (ctx->pfds[i].fd == fd)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
static struct process *
|
|
expect_fd_to_process (struct expect_context *ctx, int fd)
|
|
{
|
|
for (size_t i = 0; i < ctx->patterns_len; i++)
|
|
{
|
|
struct pattern *p = ctx->patterns[i];
|
|
if (p->process
|
|
&& p->process->terminal_fd == fd)
|
|
return p->process;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
expect_set_fd_eof (struct expect_context *ctx, int fd)
|
|
{
|
|
for (size_t i = 0; i < ctx->patterns_len; i++)
|
|
{
|
|
struct pattern *p = ctx->patterns[i];
|
|
if (p->process
|
|
&& p->process->terminal_fd == fd)
|
|
p->eof = true;
|
|
}
|
|
}
|
|
|
|
static void
|
|
expect_prepare_pattern (struct expect_context *ctx, struct pattern *p)
|
|
{
|
|
str_reset (&p->input);
|
|
if (p->kind == PATTERN_REGEX)
|
|
for (size_t i = 0; i <= p->regex->re_nsub; i++)
|
|
p->matches[i] = (regmatch_t) { .rm_so = -1, .rm_eo = -1 };
|
|
|
|
if (p->kind == PATTERN_REGEX
|
|
|| p->kind == PATTERN_EXACT
|
|
|| p->kind == PATTERN_EOF
|
|
|| p->kind == PATTERN_DEFAULT)
|
|
{
|
|
p->eof = false;
|
|
if (!expect_has_fd (ctx, p->process->terminal_fd))
|
|
ctx->pfds[ctx->pfds_len++] = (struct pollfd)
|
|
{ .fd = p->process->terminal_fd, .events = POLLIN };
|
|
}
|
|
if (p->kind == PATTERN_TIMEOUT
|
|
|| p->kind == PATTERN_DEFAULT)
|
|
{
|
|
lua_Number v = p->timeout >= 0 ? p->timeout : g.default_timeout;
|
|
if (ctx->first_timeout != ctx->first_timeout)
|
|
ctx->first_timeout = v;
|
|
else
|
|
ctx->first_timeout = MIN (ctx->first_timeout, v);
|
|
}
|
|
}
|
|
|
|
static void
|
|
expect_prepare (struct expect_context *ctx)
|
|
{
|
|
// The liberty poller is not particularly appropriate for this use case.
|
|
ctx->pfds_len = 0;
|
|
ctx->pfds = xcalloc (ctx->patterns_len, sizeof *ctx->pfds);
|
|
|
|
ctx->first_timeout = NAN;
|
|
for (size_t i = 0; i < ctx->patterns_len; i++)
|
|
expect_prepare_pattern (ctx, ctx->patterns[i]);
|
|
|
|
// There is always at least a default timeout.
|
|
ctx->timeout = g.default_timeout;
|
|
if (ctx->first_timeout == ctx->first_timeout)
|
|
ctx->timeout = ctx->first_timeout;
|
|
}
|
|
|
|
static struct pattern *
|
|
expect_match_timeout (struct expect_context *ctx)
|
|
{
|
|
for (size_t i = 0; i < ctx->patterns_len; i++)
|
|
{
|
|
struct pattern *p = ctx->patterns[i];
|
|
if (p->kind != PATTERN_TIMEOUT
|
|
&& p->kind != PATTERN_DEFAULT)
|
|
continue;
|
|
|
|
if (p->timeout <= ctx->first_timeout)
|
|
return p;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static bool
|
|
pattern_match (struct pattern *self)
|
|
{
|
|
struct process *process = self->process;
|
|
struct str *buffer = process ? &process->buffer : NULL;
|
|
|
|
str_reset (&self->input);
|
|
switch (self->kind)
|
|
{
|
|
case PATTERN_EOF:
|
|
case PATTERN_DEFAULT:
|
|
{
|
|
if (!self->eof)
|
|
return false;
|
|
|
|
str_append_str (&self->input, &process->buffer);
|
|
if (!self->notransfer)
|
|
str_reset (&process->buffer);
|
|
return true;
|
|
}
|
|
case PATTERN_REGEX:
|
|
{
|
|
int flags = 0;
|
|
#ifdef REG_STARTEND
|
|
self->matches[0] = (regmatch_t) { .rm_so = 0, .rm_eo = buffer->len };
|
|
flags |= REG_STARTEND;
|
|
#endif
|
|
if (regexec (self->regex, buffer->str,
|
|
self->regex->re_nsub + 1, self->matches, flags))
|
|
{
|
|
for (size_t i = 0; i <= self->regex->re_nsub; i++)
|
|
self->matches[i] = (regmatch_t) { .rm_so = -1, .rm_eo = -1 };
|
|
return false;
|
|
}
|
|
|
|
str_append_data (&self->input, buffer->str, self->matches[0].rm_eo);
|
|
if (!self->notransfer)
|
|
str_remove_slice (buffer, 0, self->matches[0].rm_eo);
|
|
return true;
|
|
}
|
|
case PATTERN_EXACT:
|
|
{
|
|
const char *match = str_memmem (buffer, &self->exact, self->nocase);
|
|
if (!match)
|
|
return false;
|
|
|
|
str_append_data (&self->input, match, self->exact.len);
|
|
if (!self->notransfer)
|
|
str_remove_slice (buffer, 0, match - buffer->str + self->exact.len);
|
|
return true;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static struct pattern *
|
|
expect_match_data (struct expect_context *ctx)
|
|
{
|
|
for (size_t i = 0; i < ctx->patterns_len; i++)
|
|
{
|
|
struct pattern *p = ctx->patterns[i];
|
|
if (pattern_match (p))
|
|
return p;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int
|
|
expect_protected (lua_State *L)
|
|
{
|
|
struct expect_context *ctx = lua_touserdata (L, lua_upvalueindex (1));
|
|
ctx->patterns_len = lua_gettop (L);
|
|
ctx->patterns = xcalloc (ctx->patterns_len, sizeof *ctx->patterns);
|
|
for (size_t i = 0; i < ctx->patterns_len; i++)
|
|
ctx->patterns[i] = luaL_checkudata (L, i + 1, XLUA_PATTERN_METATABLE);
|
|
expect_prepare (ctx);
|
|
|
|
int64_t deadline = 0;
|
|
struct pattern *match = NULL;
|
|
restart:
|
|
// A "continue" statement means we start anew with a new timeout.
|
|
// TODO(p): We should detect deadline > INT64_MAX, and wait indefinitely.
|
|
deadline = clock_msec () + ctx->timeout * 1000;
|
|
|
|
// First, check if anything matches already,
|
|
// so that we don't need to wait for /even more/ data.
|
|
match = expect_match_data (ctx);
|
|
|
|
while (!match)
|
|
{
|
|
int64_t until_deadline = deadline - clock_msec ();
|
|
int n = poll (ctx->pfds, ctx->pfds_len, MAX (0, until_deadline));
|
|
if (n < 0)
|
|
return luaL_error (L, "poll: %s", strerror (errno));
|
|
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
struct pollfd *pfd = ctx->pfds + i;
|
|
hard_assert (!(pfd->revents & POLLNVAL));
|
|
if (!(pfd->revents & (POLLIN | POLLHUP | POLLERR)))
|
|
continue;
|
|
|
|
struct process *process = expect_fd_to_process (ctx, pfd->fd);
|
|
hard_assert (process != NULL);
|
|
if (!process_feed (process))
|
|
{
|
|
expect_set_fd_eof (ctx, pfd->fd);
|
|
// Otherwise we would loop around this descriptor.
|
|
pfd->fd = -1;
|
|
}
|
|
}
|
|
|
|
if (n > 0)
|
|
match = expect_match_data (ctx);
|
|
else if (!(match = expect_match_timeout (ctx)))
|
|
return 0;
|
|
}
|
|
|
|
// Resolve the matching pattern back to its Lua full userdata.
|
|
int match_idx = 0;
|
|
for (size_t i = 0; i < ctx->patterns_len; i++)
|
|
if (ctx->patterns[i] == match)
|
|
match_idx = i + 1;
|
|
|
|
// Filter the values table by executing any functions with the pattern.
|
|
lua_rawgeti (L, LUA_REGISTRYINDEX, match->ref_values);
|
|
int values_idx = lua_gettop (L);
|
|
int values_len = lua_rawlen (L, values_idx);
|
|
lua_checkstack (L, values_len);
|
|
|
|
lua_pushcfunction (L, xlua_error_handler);
|
|
int handler_idx = lua_gettop (L);
|
|
for (int i = 1; i <= values_len; i++)
|
|
{
|
|
lua_rawgeti (L, values_idx, i);
|
|
if (!lua_isfunction (L, -1))
|
|
continue;
|
|
|
|
lua_pushvalue (L, match_idx);
|
|
if (!lua_pcall (L, 1, LUA_MULTRET, handler_idx))
|
|
continue;
|
|
if (!lua_isnil (L, -1))
|
|
return lua_error (L);
|
|
|
|
lua_pop (L, lua_gettop (L) - values_idx + 1);
|
|
goto restart;
|
|
}
|
|
return lua_gettop (L) - handler_idx;
|
|
}
|
|
|
|
static int
|
|
xlua_expect (lua_State *L)
|
|
{
|
|
lua_pushcfunction (L, xlua_error_handler);
|
|
lua_insert (L, 1);
|
|
|
|
struct expect_context ctx = {};
|
|
lua_pushlightuserdata (L, &ctx);
|
|
lua_pushcclosure (L, expect_protected, 1);
|
|
lua_insert (L, 2);
|
|
|
|
int result = lua_pcall (L, lua_gettop (L) - 2, LUA_MULTRET, 1);
|
|
expect_context_free (&ctx);
|
|
if (result)
|
|
return lua_error (L);
|
|
|
|
// Remove the error handler ("good programming practice").
|
|
lua_remove (L, 1);
|
|
return lua_gettop (L);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static int
|
|
xlua_timeout (lua_State *L)
|
|
{
|
|
luaL_checktype (L, 1, LUA_TTABLE);
|
|
if (lua_gettop (L) != 1)
|
|
return luaL_error (L, "too many arguments");
|
|
|
|
struct pattern *pattern = pattern_new (L, PATTERN_TIMEOUT, 0);
|
|
|
|
int first = 1, last = lua_rawlen (L, 1);
|
|
if (pattern_readtimeout (pattern, L, 1))
|
|
first++;
|
|
|
|
xlua_newtablecopy (L, 1, first, last);
|
|
pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
xlua_continue (lua_State *L)
|
|
{
|
|
// xlua_expect() handles this specially.
|
|
lua_pushnil (L);
|
|
return lua_error (L);
|
|
}
|
|
|
|
static luaL_Reg xlua_library[] =
|
|
{
|
|
{ "spawn", xlua_spawn },
|
|
{ "expect", xlua_expect },
|
|
{ "timeout", xlua_timeout },
|
|
{ "continue", xlua_continue },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
// --- Initialisation, event handling ------------------------------------------
|
|
|
|
static void *
|
|
xlua_alloc (void *ud, void *ptr, size_t o_size, size_t n_size)
|
|
{
|
|
(void) ud;
|
|
(void) o_size;
|
|
|
|
if (n_size)
|
|
return realloc (ptr, n_size);
|
|
|
|
free (ptr);
|
|
return NULL;
|
|
}
|
|
|
|
static int
|
|
xlua_panic (lua_State *L)
|
|
{
|
|
print_fatal ("Lua panicked: %s", lua_tostring (L, -1));
|
|
lua_close (L);
|
|
exit (EXIT_FAILURE);
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
main (int argc, char *argv[])
|
|
{
|
|
if (argc != 2)
|
|
{
|
|
fprintf (stderr, "Usage: %s program.lua\n", argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
if (!(g.L = lua_newstate (xlua_alloc, NULL)))
|
|
exit_fatal ("Lua initialization failed");
|
|
lua_atpanic (g.L, xlua_panic);
|
|
luaL_openlibs (g.L);
|
|
luaL_checkversion (g.L);
|
|
|
|
luaL_newlib (g.L, xlua_library);
|
|
lua_setglobal (g.L, PROGRAM_NAME);
|
|
|
|
luaL_newmetatable (g.L, XLUA_PROCESS_METATABLE);
|
|
luaL_setfuncs (g.L, xlua_process_table, 0);
|
|
lua_pop (g.L, 1);
|
|
|
|
luaL_newmetatable (g.L, XLUA_PATTERN_METATABLE);
|
|
luaL_setfuncs (g.L, xlua_pattern_table, 0);
|
|
lua_pop (g.L, 1);
|
|
|
|
const char *path = argv[1];
|
|
lua_pushcfunction (g.L, xlua_error_handler);
|
|
if (luaL_loadfile (g.L, path)
|
|
|| lua_pcall (g.L, 0, 0, -2))
|
|
{
|
|
print_error ("%s", lua_tostring (g.L, -1));
|
|
lua_pop (g.L, 1);
|
|
lua_close (g.L);
|
|
return 1;
|
|
}
|
|
lua_close (g.L);
|
|
return 0;
|
|
}
|