aboutsummaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/wdye/CMakeLists.txt26
-rw-r--r--tools/wdye/test.lua31
-rw-r--r--tools/wdye/wdye.adoc142
-rw-r--r--tools/wdye/wdye.c915
4 files changed, 1011 insertions, 103 deletions
diff --git a/tools/wdye/CMakeLists.txt b/tools/wdye/CMakeLists.txt
index 73872ee..a69f7ee 100644
--- a/tools/wdye/CMakeLists.txt
+++ b/tools/wdye/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required (VERSION 3.10)
+cmake_minimum_required (VERSION 3.18)
project (wdye VERSION 1 DESCRIPTION "What did you expect?" LANGUAGES C)
set (CMAKE_C_STANDARD 99)
@@ -12,13 +12,31 @@ add_compile_options ("$<$<CXX_COMPILER_ID:Clang>:${options}>")
set (CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/../../cmake")
-find_package (Curses REQUIRED)
+find_package (Curses)
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)
+option (WITH_CURSES "Offer terminal sequences using Curses" "${CURSES_FOUND}")
+
+# -liconv may or may not be a part of libc
+find_path (iconv_INCLUDE_DIRS iconv.h)
+
+include_directories ("${PROJECT_BINARY_DIR}" ${iconv_INCLUDE_DIRS})
+file (CONFIGURE OUTPUT "${PROJECT_BINARY_DIR}/config.h" CONTENT [[
+#define PROGRAM_NAME "${PROJECT_NAME}"
+#define PROGRAM_VERSION "${PROJECT_VERSION}"
+#cmakedefine WITH_CURSES
+]])
+
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})
+target_link_libraries (wdye PUBLIC ${lua_LIBRARIES})
+if (WITH_CURSES)
+ target_include_directories (wdye PUBLIC ${CURSES_INCLUDE_DIRS})
+ target_link_libraries (wdye PUBLIC ${CURSES_LIBRARIES})
+endif ()
+
+add_test (NAME simple COMMAND wdye "${PROJECT_SOURCE_DIR}/test.lua")
+include (CTest)
diff --git a/tools/wdye/test.lua b/tools/wdye/test.lua
new file mode 100644
index 0000000..c255c72
--- /dev/null
+++ b/tools/wdye/test.lua
@@ -0,0 +1,31 @@
+for k, v in pairs(wdye) do _G[k] = v end
+
+-- The terminal echoes back, we don't want to read the same stuff twice.
+local cat = spawn {"sh", "-c", "cat > /dev/null", environ={TERM="xterm"}}
+assert(cat, "failed to spawn process")
+assert(cat.term.key_left, "bad terminfo")
+
+cat:send("Hello\r")
+local m = expect(cat:exact {"Hello\r", function (p) return p[0] end})
+assert(m == "Hello\r", "exact match failed, or value expansion mismatch")
+
+local t = table.pack(expect(timeout {.5, 42}))
+assert(#t == 1 and t[1] == 42, "timeout match failed, or value mismatch")
+
+cat:send("abc123\r")
+expect(cat:regex {"A(.*)3", nocase=true, function (p)
+ assert(p[0] == "abc123", "wrong regex group #0")
+ assert(p[1] == "bc12", "wrong regex group #1")
+end})
+
+assert(not cat:wait (true), "process reports exiting early")
+
+-- Send EOF (^D), test method chaining.
+cat:send("Closing...\r"):send("\004")
+local v = expect(cat:eof {true},
+ cat:default {.5, function (p) error "expected EOF, got a timeout" end})
+
+local s1, exit, signal = cat:wait ()
+assert(s1 == 0 and exit == 0 and not signal, "unexpected exit status")
+local s2 = cat:wait (true)
+assert(s1 == s2, "exit status not remembered")
diff --git a/tools/wdye/wdye.adoc b/tools/wdye/wdye.adoc
new file mode 100644
index 0000000..98e251e
--- /dev/null
+++ b/tools/wdye/wdye.adoc
@@ -0,0 +1,142 @@
+wdye(1)
+=======
+:doctype: manpage
+:manmanual: wdye Manual
+:mansource: wdye {release-version}
+
+Name
+----
+wdye - what did you expect: Lua-based Expect tool
+
+Synopsis
+--------
+*wdye* _program.lua_
+
+Description
+-----------
+*wdye* executes a Lua script, providing an *expect*(1)-like API targeted
+at application testing.
+
+API
+---
+This list is logically ordered. Uppercase names represent object types.
+
+wdye.spawn {file [, arg1, ...] [, environ=env]}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Creates a new pseudoterminal, spawns the given program in it,
+and returns a _process_ object. When *file* doesn't contain slashes,
+the program will be searched for in _PATH_.
+
+The *env* map may be used to override environment variables, notably _TERM_.
+Variables evaluating to _false_ will be removed from the environment.
+
+The program's whole process group receives SIGKILL when the _process_
+is garbage-collected.
+
+wdye.expect ([pattern1, ...])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Waits until any pattern is ready, in order.
+When no *timeout* (or *default*) patterns are included, one is added implicitly.
+
+The function returns the matching _pattern_'s values, while replacing
+any included functions with the results of their immediate evaluation,
+passing the matching _pattern_ as their sole argument.
+
+wdye.timeout {[timeout, ] [value1, ...]}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Returns a new timeout _pattern_. When no *timeout* is given, which is specified
+in seconds, a default timeout value is assumed. Any further values
+are remembered to be later processed by *expect*.
+
+wdye.continue ()
+~~~~~~~~~~~~~~~~
+Raises a _nil_ error, which is interpreted by *expect* as a signal to restart
+all processing.
+
+PROCESS.buffer
+~~~~~~~~~~~~~~
+A string with the _process_' current read buffer contents.
+
+PROCESS.term
+~~~~~~~~~~~~
+A table with the _process_' *terminfo*(5) capabilities,
+notably containing all **key_...** codes.
+This functionality may not be enabled, then this table will always be empty.
+
+PROCESS:send ([string, ...])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Writes the given strings to the _process_' terminal slave,
+and returns the _process_ for method chaining.
+
+Beware of echoing and deadlocks, as only *expect* can read from the _process_,
+and thus consume the terminal slave's output queue.
+
+PROCESS:regex {pattern [, nocase=true] [, notransfer=true] [, value1, ...]}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Returns a new regular expression _pattern_. The *pattern* is a POSIX
+Extended Regular Expression. Whether it can match NUL bytes depends on your
+system C library.
+
+When the *nocase* option is _true_, the expression will be matched
+case-insensitively.
+
+Unless the *notransfer* option is _true_, all data up until the end of the match
+will be erased from the _process_' read buffer upon a successful match.
+
+PROCESS:exact {literal [, nocase=true] [, notransfer=true] [, value1, ...]}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Returns a new literal string _pattern_. This behaves as if the *literal*
+had its ERE special characters quoted, and was then passed to *regex*.
+This _pattern_ can always match NUL bytes.
+
+PROCESS:eof {[notransfer=true, ] [value1, ...]}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Returns a new end-of-file _pattern_, which matches the entire read buffer
+contents once the child process closes the terminal.
+
+PROCESS:wait ([nowait])
+~~~~~~~~~~~~~~~~~~~~~~~
+Waits for the program to terminate, and returns three values:
+a combined status as used by `$?` in shells,
+an exit status, and a termination signal number.
+One of the latter two values will be _nil_, as appropriate.
+
+When the *nowait* option is _true_, the function returns immediately.
+If the process hasn't terminated yet, the function then returns no values.
+
+PROCESS:default {[timeout, ] [notransfer=true, ] [value1, ...]}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Returns a new _pattern_ combining *wdye.timeout* with *eof*.
+
+PATTERN.process
+~~~~~~~~~~~~~~~
+A reference to the _pattern_'s respective process, or _nil_.
+
+PATTERN[group]
+~~~~~~~~~~~~~~
+For patterns that can match data, the zeroth group will be the whole matched
+input sequence.
+For *regex* patterns, positive groups relate to regular expression subgroups.
+Missing groups evaluate to _nil_.
+
+Example
+-------
+ for k, v in pairs(wdye) do _G[k] = v end
+ local rot13 = spawn {"tr", "A-Za-z", "N-ZA-Mn-za-m", environ={TERM="dumb"}}
+ rot13:send "Hello\r"
+ expect(rot13:exact {"Uryyb\r"})
+
+Environment
+-----------
+*WDYE_LOGGING*::
+ When this environment variable is present, *wdye* produces asciicast v2
+ files for every spawned program, in the current working directory.
+
+Reporting bugs
+--------------
+Use https://git.janouch.name/p/liberty to report bugs, request features,
+or submit pull requests.
+
+See also
+--------
+*expect*(1), *terminfo*(5), *regex*(7)
diff --git a/tools/wdye/wdye.c b/tools/wdye/wdye.c
index a827732..b7fccd6 100644
--- a/tools/wdye/wdye.c
+++ b/tools/wdye/wdye.c
@@ -16,23 +16,95 @@
*
*/
-#define PROGRAM_NAME "wdye"
-#define PROGRAM_VERSION "1"
-
-#define LIBERTY_WANT_POLLER
+#include "config.h"
#include "../../liberty.c"
+#include <math.h>
+
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
+#include <sys/ioctl.h>
#include <termios.h>
-#if defined SOLARIS
+#ifdef SOLARIS
#include <stropts.h>
#endif
+#ifdef WITH_CURSES
#include <curses.h>
#include <term.h>
+#endif
+
+static int64_t
+clock_msec (void)
+{
+#ifdef _POSIX_TIMERS
+ struct timespec tp;
+ hard_assert (clock_gettime (CLOCK_BEST, &tp) != -1);
+ return (int64_t) tp.tv_sec * 1000 + tp.tv_nsec / 1000000;
+#else
+ struct timeval tp;
+ hard_assert (gettimeofday (&tp, NULL) != -1);
+ return (int64_t) tp.tv_sec * 1000 + *msec = tp.tv_usec / 1000;
+#endif
+}
+
+// execvpe is a GNU extension, reimplement it.
+static int
+execvpe (const char *file, char *const argv[], char *const envp[])
+{
+ const char *path = getenv ("PATH");
+ if (strchr (file, '/') || !path)
+ return execve (file, argv, envp);
+
+ struct strv dirs = strv_make ();
+ cstr_split (path, ":", false, &dirs);
+ char *name = NULL;
+ for (size_t i = 0; i < dirs.len; i++)
+ {
+ cstr_set (&name, xstrdup_printf ("%s/%s",
+ *dirs.vector[i] ? dirs.vector[i] : ".", file));
+ execve (name, argv, envp);
+ }
+ strv_free (&dirs);
+ return -1;
+}
+
+// This is a particularly inefficient algorithm, but it can match binary data.
+static const char *
+str_memmem (const struct str *haystack, const struct str *needle, bool nocase)
+{
+ if (haystack->len < needle->len)
+ return NULL;
+
+ char *xhaystack = xmalloc (haystack->len + 1);
+ char *xneedle = xmalloc (needle->len + 1);
+ if (nocase)
+ {
+ for (size_t i = 0; i <= haystack->len; i++)
+ xhaystack[i] = tolower ((uint8_t) haystack->str[i]);
+ for (size_t i = 0; i <= needle->len; i++)
+ xneedle[i] = tolower ((uint8_t) needle->str[i]);
+ }
+ else
+ {
+ memcpy (xhaystack, haystack->str, haystack->len + 1);
+ memcpy (xneedle, needle->str, needle->len + 1);
+ }
+
+ const char *result = NULL;
+ for (size_t i = 0, end = haystack->len - needle->len; i <= end; i++)
+ if (!memcmp (xhaystack + i, xneedle, needle->len))
+ {
+ result = haystack->str + i;
+ break;
+ }
+
+ free (xhaystack);
+ free (xneedle);
+ return result;
+}
// --- Pseudoterminal ----------------------------------------------------------
// This is largely taken from Advanced Programming in the UNIX® Environment,
@@ -66,12 +138,12 @@ static int
ptys_open (const char *pts_name)
{
int fds = -1;
-#if defined SOLARIS
+#ifdef SOLARIS
int err = 0, setup = 0;
#endif
if ((fds = open (pts_name, O_RDWR)) < 0)
return -1;
-#if defined SOLARIS
+#ifdef SOLARIS
if ((setup = ioctl (fds, I_FIND, "ldterm")) < 0)
goto errout;
if (setup == 0)
@@ -150,18 +222,67 @@ pty_fork (int *ptrfdm, char **slave_name,
return pid;
}
+// --- JSON --------------------------------------------------------------------
+
+static void
+write_json_string (FILE *output, const char *s, size_t len)
+{
+ fputc ('"', output);
+ for (const char *last = s, *end = s + len; s != end; last = s)
+ {
+ // Here is where you realize the asciicast format is retarded for using
+ // JSON at all. (Consider multibyte characters at read() boundaries.)
+ int32_t codepoint = utf8_decode (&s, end - s);
+ if (codepoint < 0)
+ {
+ s++;
+ fprintf (output, "\\uFFFD");
+ continue;
+ }
+
+ switch (codepoint)
+ {
+ break; case '"': fprintf (output, "\\\"");
+ break; case '\\': fprintf (output, "\\\\");
+ break; case '\b': fprintf (output, "\\b");
+ break; case '\f': fprintf (output, "\\f");
+ break; case '\n': fprintf (output, "\\n");
+ break; case '\r': fprintf (output, "\\r");
+ break; case '\t': fprintf (output, "\\t");
+ break; default:
+ if (!utf8_validate_cp (codepoint))
+ fprintf (output, "\\uFFFD");
+ else if (codepoint < 32)
+ fprintf (output, "\\u%04X", codepoint);
+ else
+ fwrite (last, 1, s - last, output);
+ }
+ }
+ fputc ('"', output);
+}
+
// --- Global state ------------------------------------------------------------
static struct
{
lua_State *L; ///< Lua state
+ lua_Number default_timeout; ///< Default expect timeout (s)
}
-g;
+g =
+{
+ .default_timeout = 10.,
+};
static int
xlua_error_handler (lua_State *L)
{
- luaL_traceback (L, L, luaL_checkstring (L, 1), 1);
+ // Don't add tracebacks when there's already one, and pass nil through.
+ const char *string = luaL_optstring (L, 1, NULL);
+ if (string && !strchr (string, '\n'))
+ {
+ luaL_traceback (L, L, string, 1);
+ lua_remove (L, 1);
+ }
return 1;
}
@@ -203,32 +324,36 @@ xlua_newtablecopy (lua_State *L, int idx, int first, int last)
enum pattern_kind
{
- PATTERN_RE, ///< Regular expression match
+ PATTERN_REGEX, ///< Regular expression match
+ PATTERN_EXACT, ///< Literal string match
PATTERN_TIMEOUT, ///< Timeout
PATTERN_EOF, ///< EOF condition
- PATTERN_DEFAULT, ///< Either timeout or EOF
+ PATTERN_DEFAULT, ///< Either timeout or EOF condition
};
struct pattern
{
enum pattern_kind kind; ///< Tag
- int ref_process; ///< Process for RE/EOF/DEFAULT
- regex_t *re; ///< Regular expression for RE
- int64_t timeout; ///< Timeout for TIMEOUT/DEFAULT
+ int ref_process; ///< Process for all except TIMEOUT
+ struct process *process; ///< Weak pointer to the process
+ regex_t *regex; ///< Regular expression for REGEX
+ struct str exact; ///< Exact match literal for EXACT
+ lua_Number timeout; ///< Timeout for TIMEOUT/DEFAULT (s)
+ bool nocase; ///< Case insensitive search
bool notransfer; ///< Do not consume process buffer
int ref_values; ///< Return values as a table reference
- struct error *e; ///< Error buffer
+ // Patterns are constructed in place, used once, and forgotten,
+ // so we can just shove anything extra in here.
- // TODO(p):
- // - ref_values is a LUA_TTABLE with arbitrary values.
- // - https://www.lua.org/source/5.3/ltablib.c.html#unpack
- // to then return everything as multiple values.
- // - Function values will be executed and expanded.
+ struct error *e; ///< Error buffer
+ struct str input; ///< Matched input
+ regmatch_t *matches; ///< Match indexes within the input
+ bool eof; ///< End of file seen
};
static struct pattern *
-xlua_pattern_new (lua_State *L, enum pattern_kind kind)
+pattern_new (lua_State *L, enum pattern_kind kind, int idx_process)
{
struct pattern *self = lua_newuserdata (L, sizeof *self);
luaL_setmetatable (L, XLUA_PATTERN_METATABLE);
@@ -236,8 +361,17 @@ xlua_pattern_new (lua_State *L, enum pattern_kind kind)
self->kind = kind;
self->ref_process = LUA_NOREF;
- self->timeout = -1;
+ self->exact = str_make ();
+ self->timeout = -1.;
self->ref_values = LUA_NOREF;
+ self->input = str_make ();
+
+ if (idx_process)
+ {
+ lua_pushvalue (L, idx_process);
+ self->process = lua_touserdata (L, -1);
+ self->ref_process = luaL_ref (L, LUA_REGISTRYINDEX);
+ }
return self;
}
@@ -246,29 +380,86 @@ xlua_pattern_gc (lua_State *L)
{
struct pattern *self = luaL_checkudata (L, 1, XLUA_PATTERN_METATABLE);
luaL_unref (L, LUA_REGISTRYINDEX, self->ref_process);
- if (self->re)
- regex_free (self->re);
+ if (self->regex)
+ regex_free (self->regex);
+ str_free (&self->exact);
luaL_unref (L, LUA_REGISTRYINDEX, self->ref_values);
if (self->e)
error_free (self->e);
+ str_free (&self->input);
+ free (self->matches);
return 0;
}
+static int
+xlua_pattern_index (lua_State *L)
+{
+ struct pattern *self = luaL_checkudata (L, 1, XLUA_PATTERN_METATABLE);
+ if (!lua_isinteger (L, 2))
+ {
+ const char *key = luaL_checkstring (L, 2);
+ if (!strcmp (key, "process"))
+ lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_process);
+ else
+ return luaL_argerror (L, 2, "not a readable property");
+ return 1;
+ }
+
+ lua_Integer group = lua_tointeger (L, 2);
+ switch (self->kind)
+ {
+ case PATTERN_REGEX:
+ {
+ const regmatch_t *m = self->matches + group;
+ if (group < 0 || group > self->regex->re_nsub
+ || m->rm_so < 0 || m->rm_eo < 0 || m->rm_eo > self->input.len)
+ lua_pushnil (L);
+ else
+ lua_pushlstring (L,
+ self->input.str + m->rm_so, m->rm_eo - m->rm_so);
+ return 1;
+ }
+ case PATTERN_EXACT:
+ case PATTERN_EOF:
+ case PATTERN_DEFAULT:
+ if (group != 0)
+ lua_pushnil (L);
+ else
+ lua_pushlstring (L, self->input.str, self->input.len);
+ return 1;
+ default:
+ return luaL_argerror (L, 2, "indexing unavailable for this pattern");
+ }
+}
+
static bool
-xlua_pattern_readtimeout (struct pattern *self, lua_State *L, int idx)
+pattern_readtimeout (struct pattern *self, lua_State *L, int idx)
{
lua_rawgeti (L, idx, 1);
- bool ok = lua_isinteger (L, -1);
- lua_Integer v = lua_tointeger (L, -1);
+ bool ok = lua_isnumber (L, -1);
+ lua_Number v = lua_tonumber (L, -1);
lua_pop (L, 1);
+ if (v != v)
+ luaL_error (L, "timeout is not a number");
if (ok)
self->timeout = v;
return ok;
}
+static void
+pattern_readflags (struct pattern *self, lua_State *L, int idx)
+{
+ lua_getfield (L, idx, "nocase");
+ self->nocase = lua_toboolean (L, -1);
+ lua_getfield (L, idx, "notransfer");
+ self->notransfer = lua_toboolean (L, -1);
+ lua_pop (L, 2);
+}
+
static luaL_Reg xlua_pattern_table[] =
{
{ "__gc", xlua_pattern_gc },
+ { "__index", xlua_pattern_index },
{ NULL, NULL }
};
@@ -279,12 +470,17 @@ static luaL_Reg xlua_pattern_table[] =
struct process
{
int terminal_fd; ///< Process stdin/stdout/stderr
- pid_t pid; ///< Process ID
+ pid_t pid; ///< Process ID or -1 if collected
int ref_term; ///< Terminal information
+ struct str buffer; ///< Terminal input buffer
+ int status; ///< Process status iff pid is -1
+
+ int64_t start; ///< Start timestamp (Unix msec)
+ FILE *asciicast; ///< asciicast script dump
};
static struct process *
-xlua_process_new (lua_State *L)
+process_new (lua_State *L)
{
struct process *self = lua_newuserdata (L, sizeof *self);
luaL_setmetatable (L, XLUA_PROCESS_METATABLE);
@@ -293,6 +489,7 @@ xlua_process_new (lua_State *L)
self->terminal_fd = -1;
self->pid = -1;
self->ref_term = LUA_NOREF;
+ self->buffer = str_make ();
return self;
}
@@ -303,12 +500,33 @@ xlua_process_gc (lua_State *L)
if (self->terminal_fd != -1)
xclose (self->terminal_fd);
if (self->pid != -1)
- kill (self->pid, SIGKILL);
+ // The slave is in its own process group.
+ kill (-self->pid, SIGKILL);
luaL_unref (L, LUA_REGISTRYINDEX, self->ref_term);
+ str_free (&self->buffer);
+ if (self->asciicast)
+ fclose (self->asciicast);
return 0;
}
static int
+xlua_process_index (lua_State *L)
+{
+ struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
+ const char *key = luaL_checkstring (L, 2);
+ if (*key != '_' && luaL_getmetafield (L, 1, key))
+ return 1;
+
+ if (!strcmp (key, "buffer"))
+ lua_pushlstring (L, self->buffer.str, self->buffer.len);
+ else if (!strcmp (key, "term"))
+ lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_term);
+ else
+ return luaL_argerror (L, 2, "not a readable property");
+ return 1;
+}
+
+static int
xlua_process_send (lua_State *L)
{
struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
@@ -326,95 +544,285 @@ xlua_process_send (lua_State *L)
return luaL_error (L, "write failed: %s", strerror (errno));
else if (written != len)
return luaL_error (L, "write failed: %s", "short write");
+
+ if (self->asciicast)
+ {
+ double timestamp = (clock_msec () - self->start) / 1000.;
+ fprintf (self->asciicast, "[%f, \"i\", ", timestamp);
+ write_json_string (self->asciicast, arg, len);
+ fprintf (self->asciicast, "]\n");
+ }
}
lua_pushvalue (L, 1);
return 1;
}
static int
-xlua_process_re (lua_State *L)
+xlua_process_regex (lua_State *L)
{
struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
luaL_checktype (L, 2, LUA_TTABLE);
if (lua_gettop (L) != 2)
return luaL_error (L, "too many arguments");
- struct pattern *pattern = xlua_pattern_new (L, PATTERN_RE);
+ struct pattern *pattern = pattern_new (L, PATTERN_REGEX, 1);
+ pattern_readflags (pattern, L, 2);
- lua_getfield (L, 2, "nocase");
int flags = REG_EXTENDED;
- if (lua_toboolean (L, -1))
+ if (pattern->nocase)
flags |= REG_ICASE;
- lua_getfield (L, 2, "notransfer");
- if (lua_toboolean (L, -1))
- pattern->notransfer = true;
-
lua_rawgeti (L, 2, 1);
if (!lua_isstring (L, -1))
return luaL_error (L, "expected regular expression");
size_t len = 0;
const char *re = lua_tolstring (L, -1, &len);
- if (!(pattern->re = regex_compile (re, flags, &pattern->e)))
+ if (!(pattern->regex = regex_compile (re, flags, &pattern->e)))
return luaL_error (L, "%s", pattern->e->message);
- lua_pop (L, 3);
+ lua_pop (L, 1);
+
+ pattern->matches =
+ xcalloc (pattern->regex->re_nsub + 1, sizeof *pattern->matches);
+
+ xlua_newtablecopy (L, 2, 2, lua_rawlen (L, 2));
+ pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
+ return 1;
+}
+
+static int
+xlua_process_exact (lua_State *L)
+{
+ struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
+ luaL_checktype (L, 2, LUA_TTABLE);
+ if (lua_gettop (L) != 2)
+ return luaL_error (L, "too many arguments");
+
+ struct pattern *pattern = pattern_new (L, PATTERN_EXACT, 1);
+ pattern_readflags (pattern, L, 2);
+
+ lua_rawgeti (L, 2, 1);
+ if (!lua_isstring (L, -1))
+ return luaL_error (L, "expected string literal");
+
+ size_t len = 0;
+ const char *literal = lua_tolstring (L, -1, &len);
+ str_append_data (&pattern->exact, literal, len);
+ lua_pop (L, 1);
xlua_newtablecopy (L, 2, 2, lua_rawlen (L, 2));
pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
return 1;
}
+static int
+xlua_process_eof (lua_State *L)
+{
+ struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
+ luaL_checktype (L, 2, LUA_TTABLE);
+ if (lua_gettop (L) != 2)
+ return luaL_error (L, "too many arguments");
+
+ struct pattern *pattern = pattern_new (L, PATTERN_EOF, 1);
+ pattern_readflags (pattern, L, 2);
+
+ xlua_newtablecopy (L, 2, 1, lua_rawlen (L, 2));
+ pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
+ return 1;
+}
+
+static int
+xlua_process_default (lua_State *L)
+{
+ struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
+ luaL_checktype (L, 2, LUA_TTABLE);
+ if (lua_gettop (L) != 2)
+ return luaL_error (L, "too many arguments");
+
+ struct pattern *pattern = pattern_new (L, PATTERN_DEFAULT, 1);
+ pattern_readflags (pattern, L, 2);
+
+ int first = 1, last = lua_rawlen (L, 2);
+ if (pattern_readtimeout (pattern, L, 2))
+ first++;
+
+ xlua_newtablecopy (L, 2, first, last);
+ pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
+ return 1;
+}
+
+static int
+xlua_process_wait (lua_State *L)
+{
+ struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
+ bool nowait = luaL_opt(L, lua_toboolean, 2, false);
+ if (lua_gettop (L) > 2)
+ return luaL_error (L, "too many arguments");
+
+ int status = self->status;
+restart:
+ if (self->pid != -1)
+ {
+ int options = 0;
+ if (nowait)
+ options |= WNOHANG;
+
+ pid_t pid = waitpid (self->pid, &status, options);
+ if (!pid)
+ return 0;
+
+ if (pid < 0)
+ {
+ if (errno == EINTR)
+ goto restart;
+ return luaL_error (L, "waitpid: %s", strerror (errno));
+ }
+
+ // We lose the ability to reliably kill the whole process group.
+ self->status = status;
+ self->pid = -1;
+ }
+ if (WIFEXITED (status))
+ {
+ lua_pushinteger (L, WEXITSTATUS (status));
+ lua_pushinteger (L, WEXITSTATUS (status));
+ lua_pushnil (L);
+ return 3;
+ }
+ if (WIFSIGNALED (status))
+ {
+ lua_pushinteger (L, 128 + WTERMSIG (status));
+ lua_pushnil (L);
+ lua_pushinteger (L, WTERMSIG (status));
+ return 3;
+ }
+ return 0;
+}
+
+static bool
+process_feed (struct process *self)
+{
+ // Let's do this without O_NONBLOCK for now.
+ char buf[BUFSIZ] = "";
+ ssize_t n = read (self->terminal_fd, buf, sizeof buf);
+ if (n < 0)
+ {
+ if (errno == EINTR)
+ return true;
+#ifdef __linux__
+ // https://unix.stackexchange.com/a/538271
+ if (errno == EIO)
+ return false;
+#endif
+
+ print_warning ("read: %s", strerror (errno));
+ return false;
+ }
+
+ if (self->asciicast)
+ {
+ double timestamp = (clock_msec () - self->start) / 1000.;
+ fprintf (self->asciicast, "[%f, \"o\", ", timestamp);
+ write_json_string (self->asciicast, buf, n);
+ fprintf (self->asciicast, "]\n");
+ }
+
+ // TODO(p): Add match_max processing, limiting the buffer size.
+ str_append_data (&self->buffer, buf, n);
+ return n > 0;
+}
+
static luaL_Reg xlua_process_table[] =
{
{ "__gc", xlua_process_gc },
+ { "__index", xlua_process_index },
{ "send", xlua_process_send },
- { "re", xlua_process_re },
-// { "eof", xlua_process_eof },
-// { "default", xlua_process_default },
+ { "regex", xlua_process_regex },
+ { "exact", xlua_process_exact },
+ { "eof", xlua_process_eof },
+ { "default", xlua_process_default },
+ { "wait", xlua_process_wait },
{ NULL, NULL }
};
// --- Terminal ----------------------------------------------------------------
+struct terminfo_entry
+{
+ enum { TERMINFO_BOOLEAN, TERMINFO_NUMERIC, TERMINFO_STRING } kind;
+ unsigned numeric;
+ char string[];
+};
+
+#ifdef WITH_CURSES
+
static bool
load_terminfo (const char *term, struct str_map *strings)
{
- // TODO(p): See if we can get away with using a bogus FD.
+ // Neither ncurses nor NetBSD curses need an actual terminal FD passed.
+ // We don't want them to read out the winsize, we just read the database.
int err = 0;
TERMINAL *saved_term = set_curterm (NULL);
- if (setupterm ((char *) term, STDOUT_FILENO, &err) != OK)
+ if (setupterm ((char *) term, -1, &err) != OK)
{
set_curterm (saved_term);
return false;
}
+ for (size_t i = 0; boolfnames[i]; i++)
+ {
+ int flag = tigetflag (boolnames[i]);
+ if (flag <= 0)
+ continue;
+
+ struct terminfo_entry *entry = xcalloc (1, sizeof *entry + 1);
+ *entry = (struct terminfo_entry) { TERMINFO_BOOLEAN, true };
+ str_map_set (strings, boolfnames[i], entry);
+ }
+ for (size_t i = 0; numfnames[i]; i++)
+ {
+ int num = tigetnum (numnames[i]);
+ if (num < 0)
+ continue;
+
+ struct terminfo_entry *entry = xcalloc (1, sizeof *entry + 1);
+ *entry = (struct terminfo_entry) { TERMINFO_NUMERIC, num };
+ str_map_set (strings, numfnames[i], entry);
+ }
for (size_t i = 0; strfnames[i]; i++)
{
- const char *value = tigetstr (strnames[i]);
- if (value && value != (char *) -1)
- str_map_set (strings, strfnames[i], xstrdup (value));
+ const char *str = tigetstr (strnames[i]);
+ if (!str || str == (const char *) -1)
+ continue;
+
+ size_t len = strlen (str) + 1;
+ struct terminfo_entry *entry = xcalloc (1, sizeof *entry + len);
+ *entry = (struct terminfo_entry) { TERMINFO_STRING, 0 };
+ memcpy (entry + 1, str, len);
+ str_map_set (strings, strfnames[i], entry);
}
del_curterm (set_curterm (saved_term));
return true;
}
+#endif
+
// --- Library -----------------------------------------------------------------
-struct xlua_spawn_context
+struct spawn_context
{
struct str_map env; ///< Subprocess environment map
- struct str_map term; ///< terminfo database strings
+ struct str_map term; ///< terminfo database
struct strv envv; ///< Subprocess environment vector
struct strv argv; ///< Subprocess argument vector
struct error *error; ///< Error
};
-static struct xlua_spawn_context
-xlua_spawn_context_make (void)
+static struct spawn_context
+spawn_context_make (void)
{
- struct xlua_spawn_context self = {};
+ struct spawn_context self = {};
self.env = str_map_make (free);
self.term = str_map_make (free);
@@ -435,7 +843,7 @@ xlua_spawn_context_make (void)
}
static void
-xlua_spawn_context_free (struct xlua_spawn_context *self)
+spawn_context_free (struct spawn_context *self)
{
str_map_free (&self->env);
str_map_free (&self->term);
@@ -456,7 +864,9 @@ environ_map_update (struct str_map *env, lua_State *L)
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)));
+ const char *value = lua_tostring (L, -1);
+ str_map_set (env, lua_tostring (L, -2),
+ value ? xstrdup (value) : NULL);
lua_pop (L, 1);
}
}
@@ -476,12 +886,12 @@ environ_map_serialize (struct str_map *env, struct strv *envv)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
-xlua_spawn_protected (lua_State *L)
+spawn_protected (lua_State *L)
{
- struct xlua_spawn_context *ctx = lua_touserdata (L, 1);
+ struct spawn_context *ctx = lua_touserdata (L, 1);
// Step 1: Prepare process environment.
- if (xlua_getfield (L, 1, "environ", LUA_TTABLE, true))
+ if (xlua_getfield (L, 2, "environ", LUA_TTABLE, true))
{
environ_map_update (&ctx->env, L);
lua_pop (L, 1);
@@ -494,16 +904,18 @@ xlua_spawn_protected (lua_State *L)
}
environ_map_serialize (&ctx->env, &ctx->envv);
+#ifdef WITH_CURSES
// Step 2: Load terminal information.
if (!load_terminfo (term, &ctx->term))
luaL_error (L, "failed to initialize terminfo for %s", term);
+#endif
// Step 3: Prepare process command line.
- size_t argc = lua_rawlen (L, 1);
+ size_t argc = lua_rawlen (L, 2);
for (size_t i = 1; i <= argc; i++)
{
lua_pushinteger (L, i);
- lua_rawget (L, 1);
+ lua_rawget (L, 2);
const char *arg = lua_tostring (L, -1);
if (!arg)
return luaL_error (L, "spawn arguments must be strings");
@@ -516,23 +928,37 @@ xlua_spawn_protected (lua_State *L)
// Step 4: Create a process object.
// This will get garbage collected as appropriate on failure.
- struct process *process = xlua_process_new (L);
+ struct process *process = process_new (L);
// This could be made into an object that can adjust winsize/termios.
lua_createtable (L, 0, ctx->term.len);
struct str_map_iter iter = str_map_iter_make (&ctx->term);
- const char *value = NULL;
- while ((value = str_map_iter_next (&iter)))
+ const struct terminfo_entry *entry = NULL;
+ while ((entry = str_map_iter_next (&iter)))
{
lua_pushstring (L, iter.link->key);
- lua_pushstring (L, value);
+ switch (entry->kind)
+ {
+ break; case TERMINFO_BOOLEAN: lua_pushboolean (L, true);
+ break; case TERMINFO_NUMERIC: lua_pushinteger (L, entry->numeric);
+ break; case TERMINFO_STRING: lua_pushstring (L, entry->string);
+ break; default: lua_pushnil (L);
+ }
lua_settable (L, -3);
}
process->ref_term = luaL_ref (L, LUA_REGISTRYINDEX);
+ struct winsize ws = { .ws_row = 24, .ws_col = 80 };
+ if ((entry = str_map_find (&ctx->term, "lines"))
+ && entry->kind == TERMINFO_NUMERIC)
+ ws.ws_row = entry->numeric;
+ if ((entry = str_map_find (&ctx->term, "columns"))
+ && entry->kind == TERMINFO_NUMERIC)
+ ws.ws_col = entry->numeric;
+
// Step 5: Spawn the process, which gets a new process group.
process->pid =
- pty_fork (&process->terminal_fd, NULL, NULL, NULL, &ctx->error);
+ pty_fork (&process->terminal_fd, NULL, NULL, &ws, &ctx->error);
if (process->pid < 0)
{
return luaL_error (L, "failed to spawn %s: %s",
@@ -540,11 +966,36 @@ xlua_spawn_protected (lua_State *L)
}
if (!process->pid)
{
- execve (ctx->argv.vector[0], ctx->argv.vector, ctx->envv.vector);
+ execvpe (ctx->argv.vector[0], ctx->argv.vector, ctx->envv.vector);
print_error ("failed to spawn %s: %s",
ctx->argv.vector[0], strerror (errno));
+ // Or we could figure out when exactly to use statuses 126 and 127.
_exit (EXIT_FAILURE);
}
+
+ // Step 6: Create a log file.
+ if (getenv ("WDYE_LOGGING"))
+ {
+ const char *name = ctx->argv.vector[0];
+ const char *last_slash = strrchr (name, '/');
+ if (last_slash)
+ name = last_slash + 1;
+
+ char *path = xstrdup_printf ("%s-%s.%d.cast",
+ PROGRAM_NAME, name, (int) process->pid);
+ if (!(process->asciicast = fopen (path, "w")))
+ print_warning ("%s: %s", path, strerror (errno));
+ free (path);
+ }
+ process->start = clock_msec ();
+ if (process->asciicast)
+ {
+ fprintf (process->asciicast, "{\"version\": 2, "
+ "\"width\": %u, \"height\": %u, \"env\": {\"TERM\": \"%s\"}}\n",
+ ws.ws_col, ws.ws_row, term);
+ }
+
+ set_cloexec (process->terminal_fd);
return 1;
}
@@ -554,16 +1005,17 @@ xlua_spawn (lua_State *L)
luaL_checktype (L, 1, LUA_TTABLE);
lua_pushcfunction (L, xlua_error_handler);
- lua_pushcfunction (L, xlua_spawn_protected);
+ lua_pushcfunction (L, 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 ();
+ struct spawn_context ctx = spawn_context_make ();
lua_pushlightuserdata (L, &ctx);
- int result = lua_pcall (L, 1, 1, -3);
- xlua_spawn_context_free (&ctx);
+ lua_rotate (L, 1, -1);
+ int result = lua_pcall (L, 2, 1, -4);
+ spawn_context_free (&ctx);
if (result)
- lua_error (L);
+ return lua_error (L);
// Remove the error handler ("good programming practice").
lua_remove (L, -2);
@@ -572,35 +1024,290 @@ xlua_spawn (lua_State *L)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-static int
-xlua_expect (lua_State *L)
+struct expect_context
{
- int nargs = lua_gettop (L);
- for (int i = 1; i <= nargs; i++)
- luaL_checkudata (L, i, XLUA_PATTERN_METATABLE);
+ size_t patterns_len; ///< Number of patterns
+ struct pattern **patterns; ///< Pattern array
+ size_t pfds_len; ///< Number of distinct poll FDs
+ struct pollfd *pfds; ///< Distinct poll FDs
+
+ lua_Number first_timeout; ///< Nearest timeout value
+ lua_Number timeout; ///< Actually used timeout value
+};
+
+static void
+expect_context_free (struct expect_context *self)
+{
+ free (self->patterns);
+ free (self->pfds);
+}
+
+static bool
+expect_has_fd (struct expect_context *ctx, int fd)
+{
+ for (size_t i = 0; i < ctx->pfds_len; i++)
+ if (ctx->pfds[i].fd == fd)
+ return true;
+ return false;
+}
+
+static struct process *
+expect_fd_to_process (struct expect_context *ctx, int fd)
+{
+ for (size_t i = 0; i < ctx->patterns_len; i++)
+ {
+ struct pattern *p = ctx->patterns[i];
+ if (p->process
+ && p->process->terminal_fd == fd)
+ return p->process;
+ }
+ return NULL;
+}
+
+static void
+expect_set_fd_eof (struct expect_context *ctx, int fd)
+{
+ for (size_t i = 0; i < ctx->patterns_len; i++)
+ {
+ struct pattern *p = ctx->patterns[i];
+ if (p->process
+ && p->process->terminal_fd == fd)
+ p->eof = true;
+ }
+}
+
+static void
+expect_prepare_pattern (struct expect_context *ctx, struct pattern *p)
+{
+ str_reset (&p->input);
+ if (p->kind == PATTERN_REGEX)
+ for (int i = 0; i <= p->regex->re_nsub; i++)
+ p->matches[i] = (regmatch_t) { .rm_so = -1, .rm_eo = -1 };
+
+ if (p->kind == PATTERN_REGEX
+ || p->kind == PATTERN_EXACT
+ || p->kind == PATTERN_EOF
+ || p->kind == PATTERN_DEFAULT)
+ {
+ p->eof = false;
+ if (!expect_has_fd (ctx, p->process->terminal_fd))
+ ctx->pfds[ctx->pfds_len++] = (struct pollfd)
+ { .fd = p->process->terminal_fd, .events = POLLIN };
+ }
+ if (p->kind == PATTERN_TIMEOUT
+ || p->kind == PATTERN_DEFAULT)
+ {
+ lua_Number v = p->timeout >= 0 ? p->timeout : g.default_timeout;
+ if (ctx->first_timeout != ctx->first_timeout)
+ ctx->first_timeout = v;
+ else
+ ctx->first_timeout = MIN (ctx->first_timeout, v);
+ }
+}
+
+static void
+expect_prepare (struct expect_context *ctx)
+{
+ // The liberty poller is not particularly appropriate for this use case.
+ ctx->pfds_len = 0;
+ ctx->pfds = xcalloc (ctx->patterns_len, sizeof *ctx->pfds);
+
+ ctx->first_timeout = NAN;
+ for (size_t i = 0; i < ctx->patterns_len; i++)
+ expect_prepare_pattern (ctx, ctx->patterns[i]);
+
+ // There is always at least a default timeout.
+ ctx->timeout = g.default_timeout;
+ if (ctx->first_timeout == ctx->first_timeout)
+ ctx->timeout = ctx->first_timeout;
+}
- struct poller poller;
- poller_init (&poller);
+static struct pattern *
+expect_match_timeout (struct expect_context *ctx)
+{
+ for (size_t i = 0; i < ctx->patterns_len; i++)
+ {
+ struct pattern *p = ctx->patterns[i];
+ if (p->kind != PATTERN_TIMEOUT
+ && p->kind != PATTERN_DEFAULT)
+ continue;
- // TODO(p): Add pattern terminals to an event loop,
- // add timers, run the loop...
+ if (p->timeout <= ctx->first_timeout)
+ return p;
+ }
+ return NULL;
+}
- for (int i = 1; i <= nargs; i++)
+static bool
+pattern_match (struct pattern *self)
+{
+ struct process *process = self->process;
+ struct str *buffer = process ? &process->buffer : NULL;
+
+ str_reset (&self->input);
+ switch (self->kind)
+ {
+ case PATTERN_EOF:
+ case PATTERN_DEFAULT:
{
- struct pattern *pattern =
- luaL_checkudata (L, i, XLUA_PATTERN_METATABLE);
- switch (pattern->kind)
+ if (!self->eof)
+ return false;
+
+ str_append_str (&self->input, &process->buffer);
+ if (!self->notransfer)
+ str_reset (&process->buffer);
+ return true;
+ }
+ case PATTERN_REGEX:
+ {
+ int flags = 0;
+#ifdef REG_STARTEND
+ self->matches[0] = (regmatch_t) { .rm_so = 0, .rm_eo = buffer->len };
+ flags |= REG_STARTEND;
+#endif
+ if (regexec (self->regex, buffer->str,
+ self->regex->re_nsub + 1, self->matches, flags))
{
+ for (int i = 0; i <= self->regex->re_nsub; i++)
+ self->matches[i] = (regmatch_t) { .rm_so = -1, .rm_eo = -1 };
+ return false;
}
+
+ str_append_data (&self->input, buffer->str, self->matches[0].rm_eo);
+ if (!self->notransfer)
+ str_remove_slice (buffer, 0, self->matches[0].rm_eo);
+ return true;
}
+ case PATTERN_EXACT:
+ {
+ const char *match = str_memmem (buffer, &self->exact, self->nocase);
+ if (!match)
+ return false;
- // TODO(p): Keep reading input into a buffer,
- // until there's a match.
- // - It seems we need to keep the buffer within the process object,
- // because we can read too much.
+ str_append_data (&self->input, match, self->exact.len);
+ if (!self->notransfer)
+ str_remove_slice (buffer, 0, match - buffer->str + self->exact.len);
+ return true;
+ }
+ default:
+ return false;
+ }
+}
- poller_free (&poller);
- return 0;
+static struct pattern *
+expect_match_data (struct expect_context *ctx)
+{
+ for (size_t i = 0; i < ctx->patterns_len; i++)
+ {
+ struct pattern *p = ctx->patterns[i];
+ if (pattern_match (p))
+ return p;
+ }
+ return NULL;
+}
+
+static int
+expect_protected (lua_State *L)
+{
+ struct expect_context *ctx = lua_touserdata (L, lua_upvalueindex (1));
+ ctx->patterns_len = lua_gettop (L);
+ ctx->patterns = xcalloc (ctx->patterns_len, sizeof *ctx->patterns);
+ for (size_t i = 0; i < ctx->patterns_len; i++)
+ ctx->patterns[i] = luaL_checkudata (L, i + 1, XLUA_PATTERN_METATABLE);
+ expect_prepare (ctx);
+
+ int64_t deadline = 0;
+ struct pattern *match = NULL;
+restart:
+ // A "continue" statement means we start anew with a new timeout.
+ // TODO(p): We should detect deadline > INT64_MAX, and wait indefinitely.
+ deadline = clock_msec () + ctx->timeout * 1000;
+
+ // First, check if anything matches already,
+ // so that we don't need to wait for /even more/ data.
+ match = expect_match_data (ctx);
+
+ while (!match)
+ {
+ int64_t until_deadline = deadline - clock_msec ();
+ int n = poll (ctx->pfds, ctx->pfds_len, MAX (0, until_deadline));
+ if (n < 0)
+ return luaL_error (L, "poll: %s", strerror (errno));
+
+ for (int i = 0; i < n; i++)
+ {
+ struct pollfd *pfd = ctx->pfds + i;
+ hard_assert (!(pfd->revents & POLLNVAL));
+ if (!(pfd->revents & (POLLIN | POLLHUP | POLLERR)))
+ continue;
+
+ struct process *process = expect_fd_to_process (ctx, pfd->fd);
+ hard_assert (process != NULL);
+ if (!process_feed (process))
+ {
+ expect_set_fd_eof (ctx, pfd->fd);
+ // Otherwise we would loop around this descriptor.
+ pfd->fd = -1;
+ }
+ }
+
+ if (n > 0)
+ match = expect_match_data (ctx);
+ else if (!(match = expect_match_timeout (ctx)))
+ return 0;
+ }
+
+ // Resolve the matching pattern back to its Lua full userdata.
+ int match_idx = 0;
+ for (size_t i = 0; i < ctx->patterns_len; i++)
+ if (ctx->patterns[i] == match)
+ match_idx = i + 1;
+
+ // Filter the values table by executing any functions with the pattern.
+ lua_rawgeti (L, LUA_REGISTRYINDEX, match->ref_values);
+ int values_idx = lua_gettop (L);
+ int values_len = lua_rawlen (L, values_idx);
+ lua_checkstack (L, values_len);
+
+ lua_pushcfunction (L, xlua_error_handler);
+ int handler_idx = lua_gettop (L);
+ for (int i = 1; i <= values_len; i++)
+ {
+ lua_rawgeti (L, values_idx, i);
+ if (!lua_isfunction (L, -1))
+ continue;
+
+ lua_pushvalue (L, match_idx);
+ if (!lua_pcall (L, 1, LUA_MULTRET, handler_idx))
+ continue;
+ if (!lua_isnil (L, -1))
+ return lua_error (L);
+
+ lua_pop (L, lua_gettop (L) - values_idx + 1);
+ goto restart;
+ }
+ return lua_gettop (L) - handler_idx;
+}
+
+static int
+xlua_expect (lua_State *L)
+{
+ lua_pushcfunction (L, xlua_error_handler);
+ lua_insert (L, 1);
+
+ struct expect_context ctx = {};
+ lua_pushlightuserdata (L, &ctx);
+ lua_pushcclosure (L, expect_protected, 1);
+ lua_insert (L, 2);
+
+ int result = lua_pcall (L, lua_gettop (L) - 2, LUA_MULTRET, 1);
+ expect_context_free (&ctx);
+ if (result)
+ return lua_error (L);
+
+ // Remove the error handler ("good programming practice").
+ lua_remove (L, 1);
+ return lua_gettop (L);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -612,9 +1319,10 @@ xlua_timeout (lua_State *L)
if (lua_gettop (L) != 1)
return luaL_error (L, "too many arguments");
- struct pattern *pattern = xlua_pattern_new (L, PATTERN_TIMEOUT);
+ struct pattern *pattern = pattern_new (L, PATTERN_TIMEOUT, 0);
+
int first = 1, last = lua_rawlen (L, 1);
- if (xlua_pattern_readtimeout (pattern, L, 1))
+ if (pattern_readtimeout (pattern, L, 1))
first++;
xlua_newtablecopy (L, 1, first, last);
@@ -622,12 +1330,20 @@ xlua_timeout (lua_State *L)
return 1;
}
+static int
+xlua_continue (lua_State *L)
+{
+ // xlua_expect() handles this specially.
+ lua_pushnil (L);
+ return lua_error (L);
+}
+
static luaL_Reg xlua_library[] =
{
{ "spawn", xlua_spawn },
{ "expect", xlua_expect },
{ "timeout", xlua_timeout },
-// { "continue", xlua_continue },
+ { "continue", xlua_continue },
{ NULL, NULL }
};
@@ -670,10 +1386,6 @@ main (int argc, char *argv[])
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);
@@ -681,15 +1393,20 @@ main (int argc, char *argv[])
luaL_setfuncs (g.L, xlua_process_table, 0);
lua_pop (g.L, 1);
+ luaL_newmetatable (g.L, XLUA_PATTERN_METATABLE);
+ luaL_setfuncs (g.L, xlua_pattern_table, 0);
+ lua_pop (g.L, 1);
+
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));
+ print_error ("%s", lua_tostring (g.L, -1));
lua_pop (g.L, 1);
+ lua_close (g.L);
+ return 1;
}
- lua_pop (g.L, 1);
lua_close (g.L);
return 0;
}