diff options
| author | xomachine <xomachiner@gmail.com> | 2018-02-25 01:55:48 +0300 |
|---|---|---|
| committer | Randy Palamar <palamar@ualberta.ca> | 2023-08-24 20:53:56 -0600 |
| commit | 0b015320382e74fcb385a46a81304f588ed27f77 (patch) | |
| tree | 1bf9cc0436d94f57aaa4e59421a750558e11594a /vis-subprocess.c | |
| parent | a35e7ea9619efbb8fb8655bd80374199911d8404 (diff) | |
| download | vis-0b015320382e74fcb385a46a81304f588ed27f77.tar.gz vis-0b015320382e74fcb385a46a81304f588ed27f77.tar.xz | |
Implementation of the non-blocking process running Lua API
Rationale
A modern text editor usually includes tools for helping user
to avoid mistakes in texts. Those tools include spell checkers and
programming language integrations. Though vis explicitly states
that the full featured IDE is not a goal, implementing some of
the tools might be achieved using its Lua API. Unfortunatelly
the API misses the ability to start a process and to perform
a communication with it without completely blocking the editor UI,
which is crucial for any tool that performs background tracking of
the inserted text (e. g. language servers).
Implementation details
New feature introduces new API method: communicate. The method
start a new process and returns a handle to communicate with
the process instantly. The patch inserts stderr and stdout
file descriptors of the process to the pselect call of the main loop
used for reading user input to track the process state without
blocking the main loop until the process is finished.
Any changes in the process state cause the iteration of the main loop
and are being exposed to the Lua API as new event: PROCESS_RESPONSE.
Diffstat (limited to 'vis-subprocess.c')
| -rw-r--r-- | vis-subprocess.c | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/vis-subprocess.c b/vis-subprocess.c new file mode 100644 index 0000000..5359820 --- /dev/null +++ b/vis-subprocess.c @@ -0,0 +1,218 @@ +#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 a string that contains a unique name for 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 the subprocess dies, it is set to NULL. + * If a caller sets the pointer to NULL the subprocess will be killed on the + * next main loop iteration. + */ +Process *vis_process_communicate(Vis *vis, const char *name, + const char *command, Invalidator **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, "Cannot create process: %s", strerror(errno)); + goto closeall; + } + new->name = strdup(name); + if (!new->name) { + vis_info_show(vis, "Cannot 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 maximum 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 descriptors of subprocesses from + * the pool. If so, it reads their data and fires corresponding events. + * Also checks if each subprocess from the pool is dead or needs to be + * killed then raises an 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) { + 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 = ¤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_process(pointer); + } +} |
