/*
* 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 LIBERTY_WANT_POLLER
#define PROGRAM_NAME "wdye"
#define PROGRAM_VERSION "1"
#include "../../liberty.c"
#include
#include
#include
#include
static struct
{
lua_State *L; ///< Lua state
struct poller poller; ///< Event loop
}
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));
}
// --- Process -----------------------------------------------------------------
#define XLUA_PROCESS_METATABLE "process"
struct process
{
pid_t pid; ///< Process ID
// TODO(p): File descriptors.
// TODO(p): Either `TERMINAL *` or read-out values.
};
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);
return self;
}
static int
xlua_process_gc (lua_State *L)
{
struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
// TODO(p): Clean up file descriptors.
if (self->pid != -1)
kill (self->pid, SIGKILL);
return 0;
}
static int
xlua_process_expect (lua_State *L)
{
struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
// TODO(p)
return 0;
}
static int
xlua_process_send (lua_State *L)
{
struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
// TODO(p)
return 0;
}
static luaL_Reg xlua_process_table[] =
{
{ "__gc", xlua_process_gc },
// { "__index", xlua_process_index },
// { "__newindex", xlua_process_newindex },
{ "expect", xlua_process_expect },
{ "send", xlua_process_send },
{ NULL, NULL }
};
// --- Terminal ----------------------------------------------------------------
// TODO(p): Probably create an object to interact with tinfo.
// --- Library -----------------------------------------------------------------
struct xlua_spawn_context
{
struct str_map env; ///< Subprocess environment map
struct strv envv; ///< Subprocess environment vector
struct strv argv; ///< Subprocess argument vector
pid_t child; ///< Child process ID
};
static struct xlua_spawn_context
xlua_spawn_context_make (void)
{
struct xlua_spawn_context self = {};
self.env = str_map_make (free);
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 ();
self.child = -1;
return self;
}
static void
xlua_spawn_context_free (struct xlua_spawn_context *self)
{
str_map_free (&self->env);
strv_free (&self->envv);
strv_free (&self->argv);
if (self->child != -1)
kill (self->child, SIGKILL);
}
// -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 a terminal object.
(void) xlua_getfield (L, 1, "term", LUA_TSTRING, true);
const char *term = lua_tostring (L, -1);
// TODO(p): Process the terminal name,
// possibly by creating another indexable Lua object/table.
// - Remember to use a default when `term == NULL`.
lua_pop (L, 1);
// Step 2: Prepare process environment.
if (xlua_getfield (L, 1, "environ", LUA_TTABLE, true))
{
environ_map_update (&ctx->env, L);
lua_pop (L, 1);
}
environ_map_serialize (&ctx->env, &ctx->envv);
// 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: Spawn the process.
// Keeping the same process group for simplicity right now.
// TODO(p): Allocate a pty, pipes in and out.
// See Advanced Programming in the UNIX® Environment.
switch ((ctx->child = fork ()))
{
case -1:
// TODO(p): Consider luaL_error().
print_error ("failed to spawn %s: %s",
ctx->argv.vector[0], strerror (errno));
break;
case 0:
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);
default:
break;
}
// Step 5: Return a process object.
struct process *process = xlua_process_new (L);
process->pid = ctx->child;
ctx->child = -1;
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;
}
static luaL_Reg xlua_library[] =
{
{ "spawn", xlua_spawn },
{ 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);
poller_init (&g.poller);
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);
}
// TODO(p): Do we need to pop the error handler?
lua_close (g.L);
return 0;
}