Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lua API] Non-blocking subprocess runner #675

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ SRC = array.c \
vis-prompt.c \
vis-registers.c \
vis-text-objects.c \
vis-subprocess.c \
$(REGEX_SRC)

ELF = vis vis-menu vis-digraph
Expand Down
2 changes: 2 additions & 0 deletions lua/vis.lua
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ local events = {
WIN_OPEN = "Event::WIN_OPEN", -- see @{win_open}
WIN_STATUS = "Event::WIN_STATUS", -- see @{win_status}
TERM_CSI = "Event::TERM_CSI", -- see @{term_csi}
PROCESS_RESPONSE = "Event::PROCESS_RESPONSE", -- see @{process_response}
}

events.file_close = function(...) events.emit(events.FILE_CLOSE, ...) end
Expand All @@ -167,6 +168,7 @@ events.win_highlight = function(...) events.emit(events.WIN_HIGHLIGHT, ...) end
events.win_open = function(...) events.emit(events.WIN_OPEN, ...) end
events.win_status = function(...) events.emit(events.WIN_STATUS, ...) end
events.term_csi = function(...) events.emit(events.TERM_CSI, ...) end
events.process_response = function(...) events.emit(events.PROCESS_RESPONSE, ...) end

local handlers = {}

Expand Down
92 changes: 92 additions & 0 deletions vis-lua.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

#include "vis-lua.h"
#include "vis-core.h"
#include "vis-subprocess.h"
#include "text-motions.h"
#include "util.h"

Expand Down Expand Up @@ -162,6 +163,9 @@ void vis_lua_win_close(Vis *vis, Win *win) { }
void vis_lua_win_highlight(Vis *vis, Win *win) { }
void vis_lua_win_status(Vis *vis, Win *win) { window_status_update(vis, win); }
void vis_lua_term_csi(Vis *vis, const long *csi) { }
void vis_lua_process_response(Vis *vis, const char *name,
char *buffer, size_t len, ResponseType rtype) { }


#else

Expand Down Expand Up @@ -1367,6 +1371,56 @@ static int redraw(lua_State *L) {
vis_redraw(vis);
return 0;
}
/***
* Closes a stream returned by @{Vis:communicate}.
*
* @function close
rnpnr marked this conversation as resolved.
Show resolved Hide resolved
* @tparam io.file inputfd the stream to be closed
* @treturn bool identical to @{io.close}
*/
static int close_subprocess(lua_State *L) {
luaL_Stream *file = luaL_checkudata(L, -1, "FILE*");
rnpnr marked this conversation as resolved.
Show resolved Hide resolved
int result = fclose(file->f);
if (result == 0) {
file->f = NULL;
file->closef = NULL;
}
return luaL_fileresult(L, result == 0, NULL);
}
/***
* Open new process and return its input stream (stdin).
* If the stream is closed (by calling the close method or by being removed by a garbage collector)
* the spawned process will be killed by SIGTERM.
* When the process will quit or will output anything to stdout or stderr,
* the @{process_response} event will be fired.
*
* The editor core won't be blocked while the external process is running.
*
* @function communicate
* @tparam string name the name of subprocess (to distinguish processes in the @{process_response} event)
* @tparam string command the command to execute
* @return the file handle to write data to the process, in case of error the return values are equivalent to @{io.open} error values.
*/
static int communicate_func(lua_State *L) {

typedef struct {
/* Lua stream structure for the process input stream */
luaL_Stream stream;
Process *handler;
} ProcessStream;

Vis *vis = obj_ref_check(L, 1, "vis");
const char *name = luaL_checkstring(L, 2);
const char *cmd = luaL_checkstring(L, 3);
ProcessStream *inputfd = (ProcessStream *)lua_newuserdata(L, sizeof(ProcessStream));
luaL_setmetatable(L, LUA_FILEHANDLE);
inputfd->handler = vis_process_communicate(vis, name, cmd, (void**)(&(inputfd->stream.closef)));
if (inputfd->handler) {
inputfd->stream.f = fdopen(inputfd->handler->inpfd, "w");
inputfd->stream.closef = &close_subprocess;
}
return inputfd->stream.f ? 1 : luaL_fileresult(L, 0, name);
}
/***
* Currently active window.
* @tfield Window win
Expand Down Expand Up @@ -1524,6 +1578,7 @@ static const struct luaL_Reg vis_lua[] = {
{ "exit", exit_func },
{ "pipe", pipe_func },
{ "redraw", redraw },
{ "communicate", communicate_func },
{ "__index", vis_index },
{ "__newindex", vis_newindex },
{ NULL, NULL },
Expand Down Expand Up @@ -3148,5 +3203,42 @@ void vis_lua_term_csi(Vis *vis, const long *csi) {
}
lua_pop(L, 1);
}
/***
* The response received from the process started via @{Vis:communicate}.
* @function process_response
* @tparam string name the name of process given to @{Vis:communicate}
* @tparam string response_type can be "STDOUT" or "STDERR" if new output was received in corresponding channel, "SIGNAL" if the process was terminated by a signal or "EXIT" when the process terminated normally
* @tparam int the exit code number if response\_type is "EXIT", or the signal number if response\_type is "SIGNAL"
* @tparam string buffer the available content sent by process
*/
void vis_lua_process_response(Vis *vis, const char *name,
char *buffer, size_t len, ResponseType rtype) {
lua_State *L = vis->lua;
if (!L) {
return;
}
vis_lua_event_get(L, "process_response");
if (lua_isfunction(L, -1)) {
lua_pushstring(L, name);
switch (rtype){
case STDOUT: lua_pushstring(L, "STDOUT"); break;
case STDERR: lua_pushstring(L, "STDERR"); break;
case SIGNAL: lua_pushstring(L, "SIGNAL"); break;
case EXIT: lua_pushstring(L, "EXIT"); break;
}
switch (rtype) {
case EXIT:
case SIGNAL:
lua_pushinteger(L, len);
lua_pushlstring(L, buffer, 0);
break;
default:
lua_pushinteger(L, 0);
rnpnr marked this conversation as resolved.
Show resolved Hide resolved
lua_pushlstring(L, buffer, len);
}
pcall(vis, L, 4, 0);
}
lua_pop(L, 1);
}

#endif
4 changes: 3 additions & 1 deletion vis-lua.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
#include <lauxlib.h>
#else
typedef struct lua_State lua_State;
typedef void* lua_CFunction;
#endif

#include "vis.h"

#include "vis-subprocess.h"
/* add a directory to consider when loading lua files */
bool vis_lua_path_add(Vis*, const char *path);
/* get semicolon separated list of paths to load lua files
Expand Down Expand Up @@ -38,5 +39,6 @@ void vis_lua_win_close(Vis*, Win*);
void vis_lua_win_highlight(Vis*, Win*);
void vis_lua_win_status(Vis*, Win*);
void vis_lua_term_csi(Vis*, const long *);
void vis_lua_process_response(Vis *, const char *, char *, size_t, ResponseType);

#endif
219 changes: 219 additions & 0 deletions vis-subprocess.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
#include <fcntl.h>
#include <stdio.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
#include "vis-lua.h"
#include "vis-subprocess.h"
#include "util.h"

/* Pool of information about currently running subprocesses */
static Process *process_pool;

/**
* Adds new empty process information structure to the process pool and
* returns it
* @return a new Process instance
*/
Process *new_process_in_pool() {
Process *newprocess = malloc(sizeof(Process));
if (!newprocess) {
return NULL;
}
newprocess->next = process_pool;
process_pool = newprocess;
return newprocess;
}

/**
* Removes the subprocess information from the pool, sets invalidator to NULL
* and frees resources.
* @param a reference to a reference to the process to be removed
* @return the next process in the pool
*/
static void destroy_process(Process **pointer) {
Process *target = *pointer;
if (target->outfd != -1) {
close(target->outfd);
}
if (target->errfd != -1) {
close(target->errfd);
}
if (target->inpfd != -1) {
close(target->inpfd);
}
/* marking stream as closed for lua */
if (target->invalidator) {
*(target->invalidator) = NULL;
}
if (target->name) {
free(target->name);
}
*pointer = target->next;
free(target);
}

/**
* Starts new subprocess by passing the `command` to the shell and
* returns the subprocess information structure, containing file descriptors
* of the process.
* Also stores the subprocess information to the internal pool to track
* its status and responses.
* @param name the string than should contain an unique name of the subprocess.
* This name will be passed to the PROCESS_RESPONSE event handler
* to distinguish running subprocesses.
* @param command a command to be executed to spawn a process
* @param invalidator a pointer to the pointer which shows that the subprocess
* is invalid when set to NULL. When subprocess dies, it is being set to NULL.
* If the pointer is set to NULL by an external code, the subprocess will be
* killed on the next main loop iteration.
*/
Process *vis_process_communicate(Vis *vis, const char *name,
const char *command, void **invalidator) {
int pin[2], pout[2], perr[2];
pid_t pid = (pid_t)-1;
if (pipe(perr) == -1) {
goto closeerr;
}
if (pipe(pout) == -1) {
goto closeouterr;
}
if (pipe(pin) == -1) {
goto closeall;
}
pid = fork();
if (pid == -1) {
vis_info_show(vis, "fork failed: %s", strerror(errno));
} else if (pid == 0) { /* child process */
sigset_t sigterm_mask;
sigemptyset(&sigterm_mask);
sigaddset(&sigterm_mask, SIGTERM);
if (sigprocmask(SIG_UNBLOCK, &sigterm_mask, NULL) == -1) {
fprintf(stderr, "failed to reset signal mask");
exit(EXIT_FAILURE);
}
dup2(pin[0], STDIN_FILENO);
dup2(pout[1], STDOUT_FILENO);
dup2(perr[1], STDERR_FILENO);
} else { /* main process */
Process *new = new_process_in_pool();
if (!new) {
vis_info_show(vis, "Can not create process: %s", strerror(errno));
goto closeall;
}
new->name = strdup(name);
if (!new->name) {
vis_info_show(vis, "Can not copy process name: %s", strerror(errno));
/* pop top element (which is `new`) from the pool */
destroy_process(&process_pool);
goto closeall;
}
new->outfd = pout[0];
new->errfd = perr[0];
new->inpfd = pin[1];
new->pid = pid;
new->invalidator = invalidator;
close(pin[0]);
close(pout[1]);
close(perr[1]);
return new;
}
closeall:
close(pin[0]);
close(pin[1]);
closeouterr:
close(pout[0]);
close(pout[1]);
closeerr:
close(perr[0]);
close(perr[1]);
if (pid == 0) { /* start command in child process */
execlp(vis->shell, vis->shell, "-c", command, (char*)NULL);
fprintf(stderr, "exec failed: %s(%d)\n", strerror(errno), errno);
exit(1);
} else {
vis_info_show(vis, "process creation failed: %s", strerror(errno));
}
return NULL;
}

/**
* Adds file descriptors of currently running subprocesses to the `readfds`
* to track their readiness and returns maximum file descriptor value
* to pass it to the `pselect` call
* @param readfds the structure for `pselect` call to fill
* @return maxium file descriptor number in the readfds structure
*/
int vis_process_before_tick(fd_set *readfds) {
int maxfd = 0;
for (Process **pointer = &process_pool; *pointer; pointer = &((*pointer)->next)) {
Process *current = *pointer;
if (current->outfd != -1) {
FD_SET(current->outfd, readfds);
maxfd = maxfd < current->outfd ? current->outfd : maxfd;
}
if (current->errfd != -1) {
FD_SET(current->errfd, readfds);
maxfd = maxfd < current->errfd ? current->errfd : maxfd;
}
}
return maxfd;
}

/**
* Reads data from the given subprocess file descriptor `fd` and fires
* the PROCESS_RESPONSE event in Lua with given subprocess `name`,
* `rtype` and the read data as arguments.
* @param fd the file descriptor to read data from
* @param name a name of the subprocess
* @param rtype a type of file descriptor where the new data is found
*/
static void read_and_fire(Vis* vis, int fd, const char *name, ResponseType rtype) {
static char buffer[PIPE_BUF];
size_t obtained = read(fd, &buffer, PIPE_BUF-1);
if (obtained > 0) {
vis_lua_process_response(vis, name, buffer, obtained, rtype);
}
}

/**
* Checks if `readfds` contains file discriptors of subprocesses from
* the pool. If so, reads the data from them and fires corresponding events.
* Also checks if subprocesses from pool is dead or need to be killed then
* raises event or kills it if necessary.
* @param readfds the structure for `pselect` call with file descriptors
*/
void vis_process_tick(Vis *vis, fd_set *readfds) {
Process **pointer = &process_pool;
for (Process **pointer = &process_pool; *pointer; ) {
Process *current = *pointer;
if (current->outfd != -1 && FD_ISSET(current->outfd, readfds)) {
read_and_fire(vis, current->outfd, current->name, STDOUT);
}
if (current->errfd != -1 && FD_ISSET(current->errfd, readfds)) {
read_and_fire(vis, current->errfd, current->name, STDERR);
}
int status;
pid_t wpid = waitpid(current->pid, &status, WNOHANG);
if (wpid == -1) {
vis_message_show(vis, strerror(errno));
} else if (wpid == current->pid) {
goto just_destroy;
} else if (!*(current->invalidator)) {
goto kill_and_destroy;
}
pointer = &current->next;
continue;
kill_and_destroy:
kill(current->pid, SIGTERM);
waitpid(current->pid, &status, 0);
just_destroy:
if (WIFSIGNALED(status)) {
vis_lua_process_response(vis, current->name, NULL, WTERMSIG(status), SIGNAL);
} else {
vis_lua_process_response(vis, current->name, NULL, WEXITSTATUS(status), EXIT);
}
destroy_process(pointer);
}
}
Loading