/* See license.txt in the root of this project. */ /* This is work in progress. We already have some parallel processing features but these use the regular \LUA\ popen feature. We don't want to use some library that adds too much to the binary so after asking around a bit for the basics we ended up here. It kind of set up like the serial module, with a userdatum and such. The interfaces should be the same for windows and posix and because posix wants the arguments split form the command, that is what gets in. We also want to be abel to discard the output because \TEX\ also produces a log file. The reason for thisa module is that I want to process the tets suite faster. The (in 2026) about 2200 files took 800 seconds on my 2018 laptop with the regular |popen| feature but some 450 with the non-blocking approach. The rewrite to a faster process handling involved (also at the \CONTEXT) efficient support for handling lines (using a callback) using a (growing) buffer for pending content. Of course interwoven output in a visible acceptable way is something to handle at the \LUA\ end; in |mtx-context| we can use colors. We might add some more features here. The code is a bit messy because snippets come from suggestions from e.g. Gemini as well as searching. We have a bit special interface wrt commands, polling, flushing but eventually we get there. This kind of platform specific code is not really nice to deal with. With the search engines trying to be intelligent it's harder to figure out these interfaces but oen can just ignore most. It actually is handy to query for the more obscure api calls but because answers differ one has to be on the edge: don't expect generated code to work. */ # include "luametatex.h" /*tex See |lmtinterface.h| for |PROCESS_METATABLE_INSTANCE|. */ // open ( command, nulled ) : process (userdata) // poll ( { process, ... }, timeout ) : output/exit (table with indices of first table) // read ( process, [callback] ) : string (continue) | false (continue) | true (eof/exit) // close ( process ) : exitcode (integer) /*tex A too large buffer can make pending output of other processes pile up. When we run multiple \TEX\ jobs in parallel, often the output interweaves nicely. This is why we introduced callbacks so that we can stay at the \CCODE\ end. So, 4096 *or even less) is plenty. */ # define buffersize 4096 // 16384 /*tex Contrary to the already present |popen| interface here we start a process with or without a pipe (which is again just a file). By nulling we avoid overhead in discarding and \TEX\ has it's own log file anyway. Once opened, we can poll what processes have output pending. We then either handle this in \LUA, or we stay at the \CCODE\ end and let a callback deal with lines. This saves copying strings and restarting a poll. When a process is ended, the poller returns so that we can close the process and start a new one at the \LUA\ end, where we do the housekeeping. There are subtle differences between POSIX and Windows, for instance in passing the commands. We assume proper quoting at the \LUA\ end because there it is known what we are dealing with. On Windows we pass a copy of the command (because the string can be messed with) and in POSIX we create an array of pointers, so there we need to be clever with quotes. Because (unless we null) we feedback lines we keep a buffer for pending output. On Windows we can emulate forking or we can be a bit more native. In any case, the spawn is less efficient so we never saturate the hardware. We keep both code paths so that at some point we can test a bit more. */ # ifdef _WIN32 # include typedef struct process_data { HANDLE read; HANDLE process; int closed; int nulled; char *partial; size_t length; size_t capacity; } process_data; # else # include # include # include # include # include typedef struct process_data { int read; pid_t process; int closed; int nulled; int exited; int status; char *partial; size_t length; size_t capacity; } process_data; # endif /*tex We share this one, because it doesn't set any values in the data structure. We could use pointer comparison here but the number of checks is not that large. */ static inline process_data * processlib_aux_valid(lua_State *L, int i) { process_data * process = lua_touserdata(L, i); if (process && lua_getmetatable(L, i)) { lua_get_metatablelua(process_instance); if (! lua_rawequal(L, -1, -2)) { process = NULL; } lua_pop(L, 2); } return process; } /*tex When we enable a terminal (see \CONTEXT\ usage) it makes sense to handle lines here instead of splitting and appending in \LUA, so here is a helper. */ static void processlib_callback( lua_State *L, process_data *process, const char *data, size_t len, int eof ) { /*tex We can have long lines, so we need to let the buffer grow on demand. */ if (len > 0) { size_t needed = process->length + len; if (needed > process->capacity) { size_t capacity = process->capacity ? process->capacity * 2 : buffersize; if (capacity < needed) { capacity = needed; } char *buffer = lmt_memory_realloc(process->partial, capacity); if (! buffer) { tex_formatted_warning("process lib", "out of memory in line buffering"); return; } process->partial = buffer; process->capacity = capacity; } memcpy(process->partial + process->length, data, len); process->length = needed; } /*tex We need to handle both line endings. The callback is the second argument to read. */ size_t start = 0; for (size_t i = 0; i < process->length; i++) { if (process->partial[i] == '\n') { size_t line_len = i - start; if (line_len > 0 && process->partial[start + line_len - 1] == '\r') { line_len--; } lua_pushvalue(L, 2); lua_pushlstring(L, process->partial + start, line_len); lua_call(L, 1, 0); start = i + 1; } } /*tex We now move the pending rest to the start. */ if (start < process->length) { size_t remaining = process->length - start; if (eof) { /*tex We flush pending text when we exit (eof). */ if (remaining > 0 && process->partial[start + remaining - 1] == '\r') { remaining--; } lua_pushvalue(L, 2); lua_pushlstring(L, process->partial + start, remaining); lua_call(L, 1, 0); process->length = 0; } else { /*tex We handle the rest next time (no blocking by waiting). */ memmove(process->partial, process->partial + start, remaining); process->length = remaining; } } else { process->length = 0; } } # ifdef _WIN32 # if 0 /* 1 = fork like, 0 = native non-blocking */ /* there is not much difference currently */ static BOOL process_create_pipe(PHANDLE hRead, PHANDLE hWrite, LPSECURITY_ATTRIBUTES lpSA) { if (CreatePipe(hRead, hWrite, lpSA, 0)) { SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0); return TRUE; } else { return FALSE; } } static int processlib_read(lua_State *L) { process_data *process = processlib_aux_valid(L, 1); int callback = lua_type(L, 2) == LUA_TFUNCTION; if (! process || process->closed || process->nulled || ! process->read) { if (callback && process && process->length > 0) { processlib_callback(L, process, NULL, 0, 1); } lua_pushboolean(L, 1); /* we're done */ return 1; } char buf[buffersize]; DWORD available = 0; DWORD bytes = 0; BOOL okay = PeekNamedPipe(process->read, NULL, 0, NULL, &available, NULL); if (! okay) { DWORD err = GetLastError(); if (err == ERROR_BROKEN_PIPE || err == ERROR_PIPE_NOT_CONNECTED) { if (callback && process->length > 0) { processlib_callback(L, process, NULL, 0, 1); } lua_pushboolean(L, 1); /* we're done */ } else { lua_pushboolean(L, 0); /* we continue */ } return 1; } if (available > 0) { if (ReadFile(process->read, buf, sizeof(buf), &bytes, NULL) && bytes > 0) { if (callback) { processlib_callback(L, process, buf, (size_t) bytes, 0); lua_pushboolean(L, 0); /* we continue */ } else { lua_pushlstring(L, buf, bytes); /* some data */ } return 1; } } DWORD exit_code; if (GetExitCodeProcess(process->process, &exit_code) && exit_code != STILL_ACTIVE) { PeekNamedPipe(process->read, NULL, 0, NULL, &available, NULL); if (available == 0) { if (callback && process->length > 0) { processlib_callback(L, process, NULL, 0, 1); } lua_pushboolean(L, 1); /* we're done */ return 1; } } lua_pushboolean(L, 0); /* we continue */ return 1; } # else static BOOL process_create_pipe(PHANDLE hRead, PHANDLE hWrite, LPSECURITY_ATTRIBUTES lpSA) /* non-blocking */ { if (! CreatePipe(hRead, hWrite, lpSA, 0)) { return FALSE; } else { /* prevent child process from inheriting the read end */ SetHandleInformation(*hRead, HANDLE_FLAG_INHERIT, 0); /* set pipe mode to NOWAIT (non-blocking I/O) */ DWORD mode = PIPE_READMODE_BYTE | PIPE_NOWAIT; if (! SetNamedPipeHandleState(*hRead, &mode, NULL, NULL)) { CloseHandle(*hRead); CloseHandle(*hWrite); *hRead = NULL; *hWrite = NULL; return FALSE; } else { return TRUE; } } } static int processlib_read(lua_State *L) { process_data *process = processlib_aux_valid(L, 1); int callback = lua_type(L, 2) == LUA_TFUNCTION; if (! process || process->closed || process->nulled || ! process->read) { if (callback && process && process->length > 0) { processlib_callback(L, process, NULL, 0, 1); } lua_pushboolean(L, 1); /* we're done */ return 1; } // /* loop until pipe is empty so child process is never blocked on stdout writes */ char buf[buffersize]; DWORD bytes_read = 0; // while (1) { BOOL ok = ReadFile(process->read, buf, sizeof(buf), &bytes_read, NULL); if (ok && bytes_read > 0) { if (callback) { processlib_callback(L, process, buf, (size_t) bytes_read, 0); lua_pushboolean(L, 0); /* we continue */ } else { lua_pushlstring(L, buf, bytes_read); /* some data */ } } else { DWORD err = GetLastError(); if (! ok && (err == ERROR_NO_DATA || err == ERROR_MORE_DATA)) { lua_pushboolean(L, 1); /* we continue */ } else { /* broken pipe or EOF */ if (callback && process->length > 0) { processlib_callback(L, process, NULL, 0, 1); } lua_pushboolean(L, 1); /* we're done */ } } return 1; // } } # endif /*tex We delegate quoting to the caller because we also have |--foo="bar bar"| like arguments. */ static int processlib_open(lua_State *L) { int nulled = lua_toboolean(L, 2); HANDLE hRead = NULL; HANDLE hWrite = NULL; HANDLE hNull = NULL; SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE }; if (nulled) { hNull = CreateFileA( "NUL", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if (hNull == INVALID_HANDLE_VALUE) { tex_formatted_warning("process lib", "opening nul device failed"); return 0; } } else { if (! process_create_pipe(&hRead, &hWrite, &sa)) { tex_formatted_warning("process lib", "creating pipe failed"); return 0; } } STARTUPINFOA si = { 0 }; PROCESS_INFORMATION pi = { 0 }; si.cb = sizeof(STARTUPINFOA); si.hStdOutput = nulled ? hNull : hWrite; si.hStdError = nulled ? hNull : hWrite; si.dwFlags |= STARTF_USESTDHANDLES; size_t len; const char *cmd_constant = lua_tolstring(L, 1, &len); char *cmd_mutable = lmt_memory_malloc(len + 1); if (! cmd_mutable) { if (hRead) CloseHandle(hRead); if (hWrite) CloseHandle(hWrite); if (hNull) CloseHandle(hNull); tex_formatted_warning("process lib", "out of memory"); return 0; } else { memcpy(cmd_mutable, cmd_constant, len + 1); } BOOL result = CreateProcessA( NULL, cmd_mutable, // has to be writable NULL, NULL, TRUE, // inherit handles CREATE_NO_WINDOW, // instead of 0 NULL, NULL, &si, &pi ); lmt_memory_free(cmd_mutable); if (! result) { if (hRead) CloseHandle(hRead); if (hWrite) CloseHandle(hWrite); if (hNull) CloseHandle(hNull); tex_formatted_warning("process lib", "creating process failed"); return 0; } if (hWrite) CloseHandle(hWrite); if (hNull) CloseHandle(hNull); if (pi.hThread) CloseHandle(pi.hThread); process_data *process = lua_newuserdatauv(L, sizeof(process_data), 0); lua_get_metatablelua(process_instance); lua_setmetatable(L, -2); process->read = hRead; process->process = pi.hProcess; process->closed = 0; process->nulled = nulled; process->partial = NULL; process->length = 0; process->capacity = 0; return 1; } /*tex We played with avoiding the fetching from the table but for our use case it doesn't really matters much. We need to allocate and free then (or impose a maximum). The not so accurate timer (rounds) has also some impact but it can be neglected. We can use the one in timerlib if needed. We have (in the archive) a variant that uses a specific windows feature: delegate the checking using an array of processes. However, it means more code. This is mostly used for parallel \CONTEXT\ runs, like the test suite and experiments gave the impression that we're talking noise here. Todo: test again, as it might matter when we don't null the output. Another thing to play with is collecting more because now the 4096 can end in the middle of a line. */ static int processlib_poll(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); ULONGLONG timeout = (ULONGLONG) luaL_optinteger(L, 2, -1); ULONGLONG start = GetTickCount64(); int count = (int) lua_rawlen(L, 1); int index = 1; lua_newtable(L); if (count == 0) { /*tex Normally we check before we call the poller, but just in case. */ return 1; } /*tex We could validate the userdata in the table once and then avoid repetitive checking of the userdata. */ while (1) { int ready = 0; for (int i = 1; i <= count; i++) { lua_rawgeti(L, 1, i); process_data *process = processlib_aux_valid(L, -1); lua_pop(L, 1); if (process && ! process->closed) { if (process->nulled) { /* nulled processes have no pipe; check if the process terminated */ if (WaitForSingleObject(process->process, 0) == WAIT_OBJECT_0) { lua_pushinteger(L, i); lua_rawseti(L, -2, index++); ready++; } continue; } DWORD available = 0; BOOL okay = PeekNamedPipe(process->read, NULL, 0, NULL, &available, NULL); /* Pipe has data to read */ if (okay && available > 0) { lua_pushinteger(L, i); lua_rawseti(L, -2, index++); ready++; continue; } /* check if pipe is broken (EOF) or process has terminated */ DWORD err = GetLastError(); BOOL broken = ! okay && (err == ERROR_BROKEN_PIPE || err == ERROR_PIPE_NOT_CONNECTED); BOOL dead = (WaitForSingleObject(process->process, 0) == WAIT_OBJECT_0); if (broken || dead) { /* mark as ready so caller knows to read (which will hit EOF) or close */ lua_pushinteger(L, i); lua_rawseti(L, -2, index++); ready++; } } } if (ready > 0 || timeout == 0) { break; } if (timeout != (ULONGLONG) -1 && (GetTickCount64() - start) >= timeout) { break; } Sleep(5); /* rounded to 15 ms */ } return 1; } static int processlib_close(lua_State *L) { process_data *process = processlib_aux_valid(L, 1); if (process && ! process->closed) { DWORD exitcode = 0; if (process->read) { CloseHandle(process->read); process->read = NULL; } if (process->process) { WaitForSingleObject(process->process, INFINITE); GetExitCodeProcess(process->process, &exitcode); CloseHandle(process->process); process->process = NULL; } lua_pushinteger(L, exitcode); if (process->partial) { lmt_memory_free(process->partial); process->partial = NULL; process->length = 0; process->capacity = 0; } process->closed = 1; } else { lua_pushnil(L); } return 1; } # else # define MAX_ARGS 64 /*tex Here we need to split the arguments, so context "foo.tex" --crap --foo="oof foo" has to become: context foo.tex --crap --foo="oof foo" but we need to check that. */ static int parse_command(char *cmd, char **argv) { int argc = 0; char *p = cmd; int in_quotes = 0; while (*p && argc < MAX_ARGS - 1) { /* we normally only have spaces in context but there can be multiple */ while (*p && (*p == ' ' || *p == '\t')) { p++; } if (*p == '\0') { break; } /* next argument */ argv[argc++] = p; char *dst = p; /* obey quotes after = */ int quote_is_literal = 0; while (*p) { if (*p == '"') { /* quote right after '=': treat the quotes as literal characters */ if (! in_quotes && dst > argv[argc - 1] && *(dst - 1) == '=') { quote_is_literal = 1; } if (quote_is_literal) { /* we keep the quote character */ *dst++ = *p++; in_quotes = ! in_quotes; if (! in_quotes) { /* we arrived at the closing quote */ quote_is_literal = 0; } } else { /* outer wrapper quote: strip it */ in_quotes = ! in_quotes; p++; } } else if ((*p == ' ' || *p == '\t') && ! in_quotes) { /* skip space */ p++; /* go one with the next argument */ break; } else { /* copy a regular character */ *dst++ = *p++; } } /* end the current argument */ *dst = '\0'; } argv[argc] = NULL; return argc; } static int processlib_open(lua_State *L) { char *argv[MAX_ARGS]; size_t len; const char *cmd_constant = lua_tolstring(L, 1, &len); char *cmd_mutable = lmt_memory_malloc(len + 1); if (! cmd_mutable) { tex_formatted_warning("process lib", "out of memory"); return 0; } else { memcpy(cmd_mutable, cmd_constant, len + 1); } int nulled = lua_toboolean(L, 2); int count = parse_command(cmd_mutable, argv); if (! count) { lmt_memory_free(cmd_mutable); return 0; } int pipefd[2] = { -1, -1 }; if (! nulled) { if (pipe(pipefd) == -1) { tex_formatted_warning("process lib", "creating pipe failed"); lmt_memory_free(cmd_mutable); return 0; } } pid_t pid = fork(); if (pid == -1) { /* fork failed */ if (! nulled) { close(pipefd[0]); close(pipefd[1]); } tex_formatted_warning("process lib", "fork failed"); lmt_memory_free(cmd_mutable); return 0; } /* child process */ if (pid == 0) { if (nulled) { int devnull = open("/dev/null", O_RDWR); if (devnull == -1) { _exit(127); } if (dup2(devnull, STDIN_FILENO) == -1 || dup2(devnull, STDOUT_FILENO) == -1 || dup2(devnull, STDERR_FILENO) == -1) { _exit(127); } if (devnull > STDERR_FILENO) { close(devnull); } } else { close(pipefd[0]); /* close unused read end */ if (dup2(pipefd[1], STDOUT_FILENO) == -1 || dup2(pipefd[1], STDERR_FILENO) == -1) { /* redirect standard output and standard error */ _exit(127); } close(pipefd[1]); /* close duplicate handle */ } execvp(argv[0], argv); /* replace process image */ _exit(127); } lmt_memory_free(cmd_mutable); if (! nulled) { close(pipefd[1]); /* set non-blocking mode */ int flags = fcntl(pipefd[0], F_GETFL, 0); fcntl(pipefd[0], F_SETFL, flags | O_NONBLOCK); } /* wrap state in userdata */ process_data *process = lua_newuserdatauv(L, sizeof(process_data), 0); lua_get_metatablelua(process_instance); lua_setmetatable(L, -2); process->read = nulled ? -1 : pipefd[0]; process->process = pid; process->closed = 0; process->nulled = nulled; process->exited = 0; process->status = -1; process->partial = NULL; process->length = 0; process->capacity = 0; return 1; } static int processlib_read(lua_State *L) { process_data *process = processlib_aux_valid(L, 1); int callback = lua_type(L, 2) == LUA_TFUNCTION; if (! process || process->closed || process->nulled || process->read < 0) { if (callback && process && process->length > 0) { processlib_callback(L, process, NULL, 0, 1); } lua_pushboolean(L, 1); /* we're done */ return 1; } char buf[buffersize]; ssize_t n; do { n = read(process->read, buf, sizeof(buf)); } while (n == -1 && errno == EINTR); if (n > 0) { if (callback) { processlib_callback(L, process, buf, (size_t) n, 0); lua_pushboolean(L, 0); /* we continue */ } else { lua_pushlstring(L, buf, n); } } else if (n == 0) { /* EOF reached */ if (callback && process->length > 0) { processlib_callback(L, process, NULL, 0, 1); } lua_pushboolean(L, 1); /* we're done */ } else if (errno == EAGAIN || errno == EWOULDBLOCK) { lua_pushboolean(L, 0); /* we continue */ } else { lua_pushboolean(L, 1); /* we're done */ } return 1; } static inline int processlib_aux_reap(process_data *process) { if (process->exited) { return 1; } else { int status; pid_t result; do { result = waitpid(process->process, &status, WNOHANG); } while (result == -1 && errno == EINTR); if (result == process->process) { process->status = status; process->exited = 1; } else if (result == -1) { /* There is no useful status left if waiting failed. */ process->status = -1; process->exited = 1; } return process->exited; } } static int processlib_poll(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); int timeout = (int) luaL_optinteger(L, 2, -1); int count = (int) lua_rawlen(L, 1); int index = 1; int ready = 0; int fd_count = 0; int nulled = 0; lua_newtable(L); if (count == 0) { return 1; } struct pollfd *fds = lmt_memory_malloc(sizeof(struct pollfd) * count); if (! fds) { tex_formatted_warning("process lib", "out of memory"); return 0; } for (int i = 1; i <= count; i++) { lua_rawgeti(L, 1, i); process_data *process = processlib_aux_valid(L, -1); lua_pop(L, 1); if (process && ! process->closed) { if (process->nulled || process->read < 0) { nulled++; } else { fds[fd_count].fd = process->read; fds[fd_count].events = POLLIN | POLLHUP | POLLERR; fd_count++; } } } if (fd_count == 0 && nulled == 0) { lmt_memory_free(fds); return 1; } int remaining = timeout; while (ready == 0) { if (nulled) { for (int i = 1; i <= count; i++) { lua_rawgeti(L, 1, i); process_data *process = processlib_aux_valid(L, -1); lua_pop(L, 1); if (process && ! process->closed && (process->nulled || process->read < 0) && processlib_aux_reap(process)) { lua_pushinteger(L, i); lua_rawseti(L, -2, index++); ready++; } } if (ready > 0) { break; } } if (remaining == 0) { break; } int wait = remaining; if (nulled && (wait < 0 || wait > 5)) { /* waitpid cannot be included in poll, so check it periodically */ wait = 5; } int ret; do { ret = poll(fds, fd_count, wait); } while (ret == -1 && errno == EINTR); if (ret > 0) { int fd_idx = 0; for (int i = 1; i <= count; i++) { lua_rawgeti(L, 1, i); process_data *process = processlib_aux_valid(L, -1); lua_pop(L, 1); if (process && ! process->closed && ! process->nulled && process->read >= 0) { /* Check if readable, hung up (EOF), or errored */ if (fds[fd_idx].revents & (POLLIN | POLLHUP | POLLERR)) { lua_pushinteger(L, i); lua_rawseti(L, -2, index++); ready++; } fd_idx++; } } break; } else if (ret < 0) { break; } if (remaining > 0) { remaining -= wait; } if (! nulled) { break; } } lmt_memory_free(fds); return 1; } static int processlib_close(lua_State *L) { process_data *process = processlib_aux_valid(L, 1); if (process && ! process->closed) { if (process->read >= 0) { close(process->read); process->read = -1; } if (! process->exited) { int status; pid_t p; do { p = waitpid(process->process, &status, 0); } while (p == -1 && errno == EINTR); if (p == process->process) { process->status = status; } else { process->status = -1; } process->exited = 1; } if (process->partial) { lmt_memory_free(process->partial); process->partial = NULL; process->length = 0; process->capacity = 0; } process->closed = 1; if (process->status >= 0 && WIFEXITED(process->status)) { lua_pushinteger(L, WEXITSTATUS(process->status)); } else if (process->status >= 0 && WIFSIGNALED(process->status)) { lua_pushinteger(L, -WTERMSIG(process->status)); } else { lua_pushinteger(L, -1); } } else { lua_pushnil(L); } return 1; } # endif static int processlib_tostring(lua_State *L) { process_data * process = processlib_aux_valid(L, 1); if (process) { // && ! process->closed) { lua_pushfstring(L, "", process); return 1; } else { return 0; } } static int processlib_gc(lua_State *L) { return processlib_close(L); } static const struct luaL_Reg processlib_function_list[] = { /* management */ { "open", processlib_open }, { "read", processlib_read }, { "poll", processlib_poll }, { "close", processlib_close }, /* */ { "tostring", processlib_tostring }, /* */ { "__gc", processlib_gc }, /* */ { NULL, NULL }, }; int luaopen_process(lua_State *L) { luaL_newmetatable(L, PROCESS_METATABLE_INSTANCE); luaL_setfuncs(L, processlib_function_list, 0); lua_pushliteral(L, "__index"); lua_pushvalue(L, -2); lua_settable(L, -3); lua_pushliteral(L, "__tostring"); lua_pushliteral(L, "tostring"); lua_gettable(L, -3); lua_settable(L, -3); lua_pushliteral(L, "__name"); lua_pushliteral(L, "process"); lua_settable(L, -3); return 1; }