diff options
Diffstat (limited to 'tools/wdye')
| -rw-r--r-- | tools/wdye/CMakeLists.txt | 24 | ||||
| -rw-r--r-- | tools/wdye/wdye.c | 303 | 
2 files changed, 327 insertions, 0 deletions
| 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; +} | 
