/* * wdye.c: what did you expect: Lua-based Expect tool * * Copyright (c) 2025, Přemysl Eric Janouch * * 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. * */ #define PROGRAM_NAME "wdye" #define PROGRAM_VERSION "1" #include "../../liberty.c" #include #include #include #include #if defined SOLARIS #include #endif #include #include 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 } // --- 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; #if defined SOLARIS int err = 0, setup = 0; #endif if ((fds = open (pts_name, O_RDWR)) < 0) return -1; #if defined 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; } // --- Global state ------------------------------------------------------------ static struct { lua_State *L; ///< Lua state } g; static int xlua_error_handler (lua_State *L) { luaL_traceback (L, L, luaL_checkstring (L, 1), 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_RE, ///< Regular expression match PATTERN_TIMEOUT, ///< Timeout PATTERN_EOF, ///< EOF condition PATTERN_DEFAULT, ///< Either timeout or EOF }; struct pattern { enum pattern_kind kind; ///< Tag int ref_process; ///< Process for RE/EOF/DEFAULT struct process *process; ///< Weak pointer to the process regex_t *re; ///< Regular expression for RE int64_t timeout; ///< Timeout for TIMEOUT/DEFAULT (s) 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 regmatch_t *matches; ///< Match indexes within input buffer int64_t deadline; ///< Absolute timeout (Unix epoch) bool eof; ///< End of file seen // TODO(p): // - ref_values is a LUA_TTABLE with arbitrary values. // - https://www.lua.org/source/5.3/ltablib.c.html#unpack // to then return everything as multiple values. // - Function values will be executed and expanded. }; static struct pattern * xlua_pattern_new (lua_State *L, enum pattern_kind kind) { 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->timeout = -1; self->ref_values = LUA_NOREF; self->deadline = -1; 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->re) regex_free (self->re); luaL_unref (L, LUA_REGISTRYINDEX, self->ref_values); if (self->e) error_free (self->e); free (self->matches); return 0; } static bool xlua_pattern_readtimeout (struct pattern *self, lua_State *L, int idx) { lua_rawgeti (L, idx, 1); bool ok = lua_isinteger (L, -1); lua_Integer v = lua_tointeger (L, -1); lua_pop (L, 1); if (ok) self->timeout = v; return ok; } static luaL_Reg xlua_pattern_table[] = { { "__gc", xlua_pattern_gc }, { NULL, NULL } }; // --- Process ----------------------------------------------------------------- #define XLUA_PROCESS_METATABLE "process" struct process { int terminal_fd; ///< Process stdin/stdout/stderr pid_t pid; ///< Process ID int ref_term; ///< Terminal information struct str buffer; ///< Terminal input buffer }; static struct process * xlua_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) kill (self->pid, SIGKILL); luaL_unref (L, LUA_REGISTRYINDEX, self->ref_term); str_free (&self->buffer); return 0; } 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 != len) return luaL_error (L, "write failed: %s", "short write"); } lua_pushvalue (L, 1); return 1; } static int xlua_process_re (lua_State *L) { struct process *self = 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 = xlua_pattern_new (L, PATTERN_RE); lua_pushvalue (L, 1); pattern->ref_process = luaL_ref (L, LUA_REGISTRYINDEX); pattern->process = self; // TODO(p): Try to use REG_STARTEND, when defined. lua_getfield (L, 2, "nocase"); int flags = REG_EXTENDED; if (lua_toboolean (L, -1)) flags |= REG_ICASE; lua_getfield (L, 2, "notransfer"); if (lua_toboolean (L, -1)) pattern->notransfer = true; 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->re = regex_compile (re, flags, &pattern->e))) return luaL_error (L, "%s", pattern->e->message); pattern->matches = xcalloc (pattern->re->re_nsub, sizeof *pattern->matches); lua_pop (L, 3); xlua_newtablecopy (L, 2, 2, lua_rawlen (L, 2)); pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX); return 1; } static luaL_Reg xlua_process_table[] = { { "__gc", xlua_process_gc }, // TODO(p): __index should include the "term" object through ref_terminal. { "send", xlua_process_send }, { "re", xlua_process_re }, // { "eof", xlua_process_eof }, // { "default", xlua_process_default }, { NULL, NULL } }; // --- Terminal ---------------------------------------------------------------- static bool load_terminfo (const char *term, struct str_map *strings) { // TODO(p): See if we can get away with using a bogus FD. int err = 0; TERMINAL *saved_term = set_curterm (NULL); if (setupterm ((char *) term, STDOUT_FILENO, &err) != OK) { set_curterm (saved_term); return false; } for (size_t i = 0; strfnames[i]; i++) { const char *value = tigetstr (strnames[i]); if (value && value != (char *) -1) str_map_set (strings, strfnames[i], xstrdup (value)); } del_curterm (set_curterm (saved_term)); return true; } // --- Library ----------------------------------------------------------------- struct xlua_spawn_context { struct str_map env; ///< Subprocess environment map struct str_map term; ///< terminfo database strings struct strv envv; ///< Subprocess environment vector struct strv argv; ///< Subprocess argument vector struct error *error; ///< Error }; static struct xlua_spawn_context xlua_spawn_context_make (void) { struct xlua_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 xlua_spawn_context_free (struct xlua_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"); str_map_set (env, lua_tostring (L, -2), xstrdup (lua_tostring (L, -1))); 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 xlua_spawn_protected (lua_State *L) { struct xlua_spawn_context *ctx = lua_touserdata (L, 1); // Step 1: Prepare process environment. if (xlua_getfield (L, 1, "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); // Step 2: Load terminal information. if (!load_terminfo (term, &ctx->term)) luaL_error (L, "failed to initialize terminfo for %s", term); // Step 3: Prepare process command line. size_t argc = lua_rawlen (L, 1); for (size_t i = 1; i <= argc; i++) { lua_pushinteger (L, i); lua_rawget (L, 1); 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 = xlua_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 char *value = NULL; while ((value = str_map_iter_next (&iter))) { lua_pushstring (L, iter.link->key); lua_pushstring (L, value); lua_settable (L, -3); } process->ref_term = luaL_ref (L, LUA_REGISTRYINDEX); // Step 5: Spawn the process, which gets a new process group. process->pid = pty_fork (&process->terminal_fd, NULL, NULL, NULL, &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) { execve (ctx->argv.vector[0], ctx->argv.vector, ctx->envv.vector); print_error ("failed to spawn %s: %s", ctx->argv.vector[0], strerror (errno)); _exit (EXIT_FAILURE); } return 1; } static int xlua_spawn (lua_State *L) { luaL_checktype (L, 1, LUA_TTABLE); lua_pushcfunction (L, xlua_error_handler); lua_pushcfunction (L, xlua_spawn_protected); // There are way too many opportunities for Lua to throw, // so maintain a context to clean up in one go. struct xlua_spawn_context ctx = xlua_spawn_context_make (); lua_pushlightuserdata (L, &ctx); int result = lua_pcall (L, 1, 1, -3); xlua_spawn_context_free (&ctx); if (result) lua_error (L); // Remove the error handler ("good programming practice"). lua_remove (L, -2); return 1; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #define DEFAULT_TIMEOUT 10 static bool xlua_match (struct pattern *self, int64_t now, struct pollfd *pfds, size_t pdfs_len) { // PATTERN_RE will want to provide regexp matches to Lua... what do? // Perhaps just remember submatch positions. return false; } static int xlua_expect (lua_State *L) { int nargs = lua_gettop (L); for (int i = 1; i <= nargs; i++) luaL_checkudata (L, i, XLUA_PATTERN_METATABLE); size_t patterns_len = nargs; struct pattern **patterns = xcalloc (patterns_len, sizeof *patterns); for (int i = 1; i <= nargs; i++) patterns[i - 1] = luaL_checkudata (L, i, XLUA_PATTERN_METATABLE); // TODO(p): Clear any relevant pattern fields, // such as match indexes, or eof (should eof be within process?) // The liberty poller is not particularly appropriate for this use case. struct pollfd *pfds = xcalloc (nargs, sizeof *pfds); size_t pfds_len = 0; int64_t first_timeout = INT64_MAX; for (size_t i = 0; i < patterns_len; i++) { struct pattern *pattern = patterns[i]; if (pattern->kind == PATTERN_RE || pattern->kind == PATTERN_EOF || pattern->kind == PATTERN_DEFAULT) { // TODO(p): If a process has already seen EOF, // don't add it to polling, or clear its .events. // Further, once it starts seeing EOF, also clear its .events. pattern->eof = false; bool found = false; for (size_t i = 0; i < pfds_len; i++) if (pfds[i].fd == pattern->process->terminal_fd) found = true; if (!found) pfds[pfds_len++] = (struct pollfd) { .fd = pattern->process->terminal_fd, .events = POLLIN }; } if (pattern->kind == PATTERN_TIMEOUT || pattern->kind == PATTERN_DEFAULT) { if (pattern->timeout >= 0) first_timeout = MIN (first_timeout, pattern->timeout); else first_timeout = MIN (first_timeout, DEFAULT_TIMEOUT); } } // There is always at least a default timeout. int64_t timeout = first_timeout != INT64_MAX ? first_timeout : DEFAULT_TIMEOUT; // TODO(p): First, check if anything matches already. // - The result of matching is an index to the matching pattern. // TODO(p): We actually need to track elapsed time better, // because succeeding in reading more data doesn't mean we succeed // in matching anything. int n = 0; int64_t deadline = clock_msec () + timeout * 1000; restart: n = poll (pfds, pfds_len, timeout * 1000); if (n < 0) { // TODO(p): On error conditions, carefully destroy everything. } if (n == 0) { // TODO(p): This will match all pattern->timeout <= timeout, // for any PATTERN_TIMEOUT or PATTERN_DEFAULT. // If nothing matches, assume a null action. } else { for (size_t i = 0; i < patterns_len; i++) { struct pattern *pattern = patterns[i]; if (pattern->kind != PATTERN_RE && pattern->kind != PATTERN_EOF && pattern->kind != PATTERN_DEFAULT) continue; bool found = false; for (size_t i = 0; i < pfds_len; i++) if (pfds[i].fd == pattern->process->terminal_fd) found = true; if (!found) continue; // TODO(p): Read more process data. } } // TODO(p): See if anything non-timeout matches now. // If something matches, pcall-filter-execute (have to, anyway) // pattern->ref_values. free (pfds); free (patterns); return 0; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 = xlua_pattern_new (L, PATTERN_TIMEOUT); int first = 1, last = lua_rawlen (L, 1); if (xlua_pattern_readtimeout (pattern, L, 1)) first++; xlua_newtablecopy (L, 1, first, last); pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX); return 1; } 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); // TODO(p): Analyse and set up SIGCHLD processing. // - What would this even be useful for? // - For having a global timeout while waiting on a process. 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); 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: %s", path, lua_tostring (g.L, -1)); lua_pop (g.L, 1); } lua_pop (g.L, 1); lua_close (g.L); return 0; }