419 lines
14 KiB
Diff
419 lines
14 KiB
Diff
|
---
|
||
|
Makefile | 1
|
||
|
lua/vis.lua | 2
|
||
|
vis-lua.c | 82 +++++++++++++++++++++++++
|
||
|
vis-lua.h | 4 -
|
||
|
vis-subprocess.c | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||
|
vis-subprocess.h | 23 +++++++
|
||
|
vis.c | 5 +
|
||
|
7 files changed, 291 insertions(+), 2 deletions(-)
|
||
|
|
||
|
--- a/Makefile
|
||
|
+++ b/Makefile
|
||
|
@@ -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
|
||
|
--- a/lua/vis.lua
|
||
|
+++ b/lua/vis.lua
|
||
|
@@ -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
|
||
|
@@ -167,6 +168,7 @@ events.win_highlight = function(...) eve
|
||
|
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 = {}
|
||
|
|
||
|
--- a/vis-lua.c
|
||
|
+++ b/vis-lua.c
|
||
|
@@ -23,6 +23,7 @@
|
||
|
|
||
|
#include "vis-lua.h"
|
||
|
#include "vis-core.h"
|
||
|
+#include "vis-subprocess.h"
|
||
|
#include "text-motions.h"
|
||
|
#include "util.h"
|
||
|
|
||
|
@@ -52,6 +53,13 @@
|
||
|
#define debug(...) do { } while (0)
|
||
|
#endif
|
||
|
|
||
|
+typedef struct {
|
||
|
+ /* Lua stream structure for the process input stream */
|
||
|
+ FILE *f;
|
||
|
+ lua_CFunction closef;
|
||
|
+ Process *handler;
|
||
|
+} ProcessStream;
|
||
|
+
|
||
|
static void window_status_update(Vis *vis, Win *win) {
|
||
|
char left_parts[4][255] = { "", "", "", "" };
|
||
|
char right_parts[4][32] = { "", "", "", "" };
|
||
|
@@ -162,6 +170,9 @@ void vis_lua_win_close(Vis *vis, Win *wi
|
||
|
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
|
||
|
|
||
|
@@ -1368,6 +1379,47 @@ static int redraw(lua_State *L) {
|
||
|
return 0;
|
||
|
}
|
||
|
/***
|
||
|
+ * Closes a stream returned by @{Vis.communicate}.
|
||
|
+ *
|
||
|
+ * @function close
|
||
|
+ * @tparam io.file inputfd the stream to be closed
|
||
|
+ * @treturn bool the same with @{io.close}
|
||
|
+ */
|
||
|
+static int close_subprocess(lua_State *L) {
|
||
|
+ luaL_Stream *file = luaL_checkudata(L, -1, "FILE*");
|
||
|
+ 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 handler.
|
||
|
+ * 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) {
|
||
|
+ 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->closef)));
|
||
|
+ if (inputfd->handler) {
|
||
|
+ inputfd->f = fdopen(inputfd->handler->inpfd, "w");
|
||
|
+ inputfd->closef = &close_subprocess;
|
||
|
+ }
|
||
|
+ return inputfd->f ? 1 : luaL_fileresult(L, inputfd->f != NULL, name);
|
||
|
+}
|
||
|
+/***
|
||
|
* Currently active window.
|
||
|
* @tfield Window win
|
||
|
* @see windows
|
||
|
@@ -1524,6 +1576,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 },
|
||
|
@@ -3135,5 +3188,34 @@ void vis_lua_term_csi(Vis *vis, const lo
|
||
|
}
|
||
|
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 string|int buffer the available content sent by process; it becomes the exit code number if response\_type is "EXIT", or the signal number if response\_type is "SIGNAL"
|
||
|
+ */
|
||
|
+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);
|
||
|
+ if (rtype == EXIT || rtype == SIGNAL)
|
||
|
+ lua_pushinteger(L, len);
|
||
|
+ else
|
||
|
+ lua_pushlstring(L, buffer, len);
|
||
|
+ 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;
|
||
|
+ }
|
||
|
+ pcall(vis, L, 3, 0);
|
||
|
+ }
|
||
|
+ lua_pop(L, 1);
|
||
|
+}
|
||
|
|
||
|
#endif
|
||
|
--- a/vis-lua.h
|
||
|
+++ b/vis-lua.h
|
||
|
@@ -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
|
||
|
@@ -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
|
||
|
--- /dev/null
|
||
|
+++ b/vis-subprocess.c
|
||
|
@@ -0,0 +1,176 @@
|
||
|
+#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"
|
||
|
+
|
||
|
+/* Maximum amount of data what can be read from IPC pipe per event */
|
||
|
+#define MAXBUFFER 1024
|
||
|
+
|
||
|
+/* Pool of information about currently running subprocesses */
|
||
|
+static Process *process_pool;
|
||
|
+
|
||
|
+Process *new_in_pool() {
|
||
|
+ /* Adds new empty process information structure to the process pool and
|
||
|
+ * returns it */
|
||
|
+ Process *newprocess = (Process *)malloc(sizeof(Process));
|
||
|
+ if (!newprocess) return NULL;
|
||
|
+ newprocess->next = process_pool;
|
||
|
+ process_pool = newprocess;
|
||
|
+ return newprocess;
|
||
|
+}
|
||
|
+
|
||
|
+void destroy(Process **pointer) {
|
||
|
+ /* Removes the subprocess information from the pool, sets invalidator to NULL
|
||
|
+ * and frees resources. */
|
||
|
+ 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);
|
||
|
+}
|
||
|
+
|
||
|
+Process *vis_process_communicate(Vis *vis, const char *name,
|
||
|
+ const char *command, void **invalidator) {
|
||
|
+ /* 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.
|
||
|
+ * `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.
|
||
|
+ * `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. */
|
||
|
+ 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_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_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;
|
||
|
+}
|
||
|
+
|
||
|
+int vis_process_before_tick(fd_set *readfds) {
|
||
|
+ /* 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 */
|
||
|
+ Process **pointer = &process_pool;
|
||
|
+ int maxfd = 0;
|
||
|
+ while (*pointer) {
|
||
|
+ 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;
|
||
|
+ }
|
||
|
+ pointer = ¤t->next;
|
||
|
+ }
|
||
|
+ return maxfd;
|
||
|
+}
|
||
|
+
|
||
|
+void read_and_fire(Vis* vis, int fd, const char *name, ResponseType rtype) {
|
||
|
+ /* 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. */
|
||
|
+ static char buffer[MAXBUFFER];
|
||
|
+ size_t obtained = read(fd, &buffer, MAXBUFFER-1);
|
||
|
+ if (obtained > 0)
|
||
|
+ vis_lua_process_response(vis, name, buffer, obtained, rtype);
|
||
|
+}
|
||
|
+
|
||
|
+void vis_process_tick(Vis *vis, fd_set *readfds) {
|
||
|
+ /* 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. */
|
||
|
+ Process **pointer = &process_pool;
|
||
|
+ while (*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 = ¤t->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(pointer);
|
||
|
+ }
|
||
|
+}
|
||
|
--- /dev/null
|
||
|
+++ b/vis-subprocess.h
|
||
|
@@ -0,0 +1,23 @@
|
||
|
+#ifndef VIS_SUBPROCESS_H
|
||
|
+#define VIS_SUBPROCESS_H
|
||
|
+#include "vis-core.h"
|
||
|
+#include <sys/select.h>
|
||
|
+
|
||
|
+struct Process {
|
||
|
+ char *name;
|
||
|
+ int outfd;
|
||
|
+ int errfd;
|
||
|
+ int inpfd;
|
||
|
+ pid_t pid;
|
||
|
+ void **invalidator;
|
||
|
+ struct Process *next;
|
||
|
+};
|
||
|
+
|
||
|
+typedef struct Process Process;
|
||
|
+typedef enum { STDOUT, STDERR, SIGNAL, EXIT } ResponseType;
|
||
|
+
|
||
|
+Process *vis_process_communicate(Vis *, const char *command, const char *name,
|
||
|
+ void **invalidator);
|
||
|
+int vis_process_before_tick(fd_set *);
|
||
|
+void vis_process_tick(Vis *, fd_set *);
|
||
|
+#endif
|
||
|
--- a/vis.c
|
||
|
+++ b/vis.c
|
||
|
@@ -28,6 +28,7 @@
|
||
|
#include "vis-core.h"
|
||
|
#include "sam.h"
|
||
|
#include "ui.h"
|
||
|
+#include "vis-subprocess.h"
|
||
|
|
||
|
|
||
|
static void macro_replay(Vis *vis, const Macro *macro);
|
||
|
@@ -1412,7 +1413,8 @@ int vis_run(Vis *vis) {
|
||
|
|
||
|
vis_update(vis);
|
||
|
idle.tv_sec = vis->mode->idle_timeout;
|
||
|
- int r = pselect(1, &fds, NULL, NULL, timeout, &emptyset);
|
||
|
+ int r = pselect(vis_process_before_tick(&fds) + 1, &fds, NULL, NULL,
|
||
|
+ timeout, &emptyset);
|
||
|
if (r == -1 && errno == EINTR)
|
||
|
continue;
|
||
|
|
||
|
@@ -1420,6 +1422,7 @@ int vis_run(Vis *vis) {
|
||
|
/* TODO save all pending changes to a ~suffixed file */
|
||
|
vis_die(vis, "Error in mainloop: %s\n", strerror(errno));
|
||
|
}
|
||
|
+ vis_process_tick(vis, &fds);
|
||
|
|
||
|
if (!FD_ISSET(STDIN_FILENO, &fds)) {
|
||
|
if (vis->mode->idle)
|