From 97faae7abe0ecf9ae2e9e80eba0234b4b7abe158 Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch 
Date: Thu, 2 Jan 2025 23:29:50 +0100
Subject: WIP: Add an Expect-like tool
---
 tools/wdye/CMakeLists.txt |  24 ++++
 tools/wdye/wdye.c         | 303 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 327 insertions(+)
 create mode 100644 tools/wdye/CMakeLists.txt
 create mode 100644 tools/wdye/wdye.c
(limited to 'tools/wdye')
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 ("$<$:${options}>")
+add_compile_options ("$<$:${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 
+ *
+ * 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                  }
+};
+
+// --- 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;
+}
-- 
cgit v1.2.3-70-g09d2