diff options
author | Přemysl Eric Janouch <p@janouch.name> | 2025-01-02 23:29:50 +0100 |
---|---|---|
committer | Přemysl Eric Janouch <p@janouch.name> | 2025-01-02 23:29:50 +0100 |
commit | 97faae7abe0ecf9ae2e9e80eba0234b4b7abe158 (patch) | |
tree | fd7dc3e4d7d954ecacd98c63a5ae930d2fa4d466 | |
parent | 21379d4c02e85ae82c1010ca3b91dceb7dfee514 (diff) | |
download | liberty-97faae7abe0ecf9ae2e9e80eba0234b4b7abe158.tar.gz liberty-97faae7abe0ecf9ae2e9e80eba0234b4b7abe158.tar.xz liberty-97faae7abe0ecf9ae2e9e80eba0234b4b7abe158.zip |
WIP: Add an Expect-like tool
-rw-r--r-- | README.adoc | 3 | ||||
-rw-r--r-- | tools/wdye/CMakeLists.txt | 24 | ||||
-rw-r--r-- | tools/wdye/wdye.c | 303 |
3 files changed, 330 insertions, 0 deletions
diff --git a/README.adoc b/README.adoc index 0d3d0de..5a87720 100644 --- a/README.adoc +++ b/README.adoc @@ -68,6 +68,9 @@ lxdrgen-mjs.awk:: lxdrgen-swift.awk:: LibertyXDR backend for the Swift programming language. +wdye:: + Compiled Lua-based Expect-like utility, intended purely for build checks. + Contributing and Support ------------------------ Use https://git.janouch.name/p/liberty to report any bugs, request features, diff --git a/tools/wdye/CMakeLists.txt b/tools/wdye/CMakeLists.txt new file mode 100644 index 0000000..73872ee --- /dev/null +++ b/tools/wdye/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required (VERSION 3.10) +project (wdye VERSION 1 DESCRIPTION "What did you expect?" LANGUAGES C) + +set (CMAKE_C_STANDARD 99) +set (CMAKE_C_STANDARD_REQUIRED ON) +set (CMAKE_C_EXTENSIONS OFF) + +# -Wunused-function is pretty annoying here, as everything is static +set (options -Wall -Wextra -Wno-unused-function) +add_compile_options ("$<$<CXX_COMPILER_ID:GNU>:${options}>") +add_compile_options ("$<$<CXX_COMPILER_ID:Clang>:${options}>") + +set (CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/../../cmake") + +find_package (Curses REQUIRED) +find_package (PkgConfig REQUIRED) +# TODO: Once written, check the lowest and highest usable version. +pkg_search_module (lua REQUIRED + lua53 lua5.3 lua-5.3 lua54 lua5.4 lua-5.4 lua>=5.3) + +add_executable (wdye wdye.c) +target_include_directories (wdye PUBLIC ${lua_INCLUDE_DIRS}) +target_link_directories (wdye PUBLIC ${lua_LIBRARY_DIRS}) +target_link_libraries (wdye PUBLIC ${CURSES_LIBRARIES} ${lua_LIBRARIES}) diff --git a/tools/wdye/wdye.c b/tools/wdye/wdye.c new file mode 100644 index 0000000..3b0f2b5 --- /dev/null +++ b/tools/wdye/wdye.c @@ -0,0 +1,303 @@ +/* + * 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. + * + */ + +#define LIBERTY_WANT_POLLER + +#define PROGRAM_NAME "wdye" +#define PROGRAM_VERSION "1" +#include "../../liberty.c" + +#include <lua.h> +#include <lualib.h> +#include <lauxlib.h> + +#include <term.h> + +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 } +}; + +// --- Library ----------------------------------------------------------------- + +static struct str_map +environ_map_make (void) +{ + struct str_map env = str_map_make (free); + for (char **p = environ; *p; p++) + { + const char *equals = strchr (*p, '='); + if (equals) + { + char *key = xstrndup (*p, equals - *p); + str_map_set (&env, key, xstrdup (equals + 1)); + free (key); + } + } + return env; +} + +// -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 struct strv +environ_map_serialize (struct str_map *env) +{ + struct strv envv = strv_make (); + 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)); + str_map_free (env); + return envv; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static int +xlua_spawn (lua_State *L) +{ + luaL_checktype (L, 1, LUA_TTABLE); + + (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. + lua_pop (L, 1); + + struct str_map env = environ_map_make (); + if (xlua_getfield (L, 1, "environ", LUA_TTABLE, true)) + { + environ_map_update (&env, L); + lua_pop (L, 1); + } + + // TODO(p): Make sure these do not get leaked. + // - Probably a protected call. + struct strv envv = environ_map_serialize (&env); + struct strv args = strv_make (); + + 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 (&args, arg); + lua_pop (L, 1); + } + if (args.len < 1) + return luaL_error (L, "missing argument"); + + // Keeping the process group for simplicity right now. + // TODO(p): Allocate a pty, pipes in and out. + pid_t child = fork (); + switch (child) + { + case -1: + // TODO(p): Consider luaL_error(). + print_error ("failed to spawn %s: %s", + args.vector[0], strerror (errno)); + break; + case 0: + execve (args.vector[0], args.vector, envv.vector); + print_error ("failed to spawn %s: %s", + args.vector[0], strerror (errno)); + _exit (EXIT_FAILURE); + default: + break; + } + + strv_free (&args); + strv_free (&envv); + + struct process *process = xlua_process_new (L); + process->pid = child; + 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); + } + + lua_close (g.L); + return 0; +} |