aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2025-01-02 23:29:50 +0100
committerPřemysl Eric Janouch <p@janouch.name>2025-01-02 23:29:50 +0100
commit97faae7abe0ecf9ae2e9e80eba0234b4b7abe158 (patch)
treefd7dc3e4d7d954ecacd98c63a5ae930d2fa4d466
parent21379d4c02e85ae82c1010ca3b91dceb7dfee514 (diff)
downloadliberty-97faae7abe0ecf9ae2e9e80eba0234b4b7abe158.tar.gz
liberty-97faae7abe0ecf9ae2e9e80eba0234b4b7abe158.tar.xz
liberty-97faae7abe0ecf9ae2e9e80eba0234b4b7abe158.zip
WIP: Add an Expect-like tool
-rw-r--r--README.adoc3
-rw-r--r--tools/wdye/CMakeLists.txt24
-rw-r--r--tools/wdye/wdye.c303
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;
+}