vis

a vi-like editor based on Plan 9's structural regular expressions

git clone https://9o.is/git/vis.git

vis-subprocess.c

(7571B)


      1 #include <fcntl.h>
      2 #include <stdio.h>
      3 #include <stdbool.h>
      4 #include <errno.h>
      5 #include <string.h>
      6 #include <sys/wait.h>
      7 #include "vis-lua.h"
      8 #include "vis-subprocess.h"
      9 #include "util.h"
     10 
     11 /* Pool of information about currently running subprocesses */
     12 static Process *process_pool;
     13 
     14 /**
     15  * Adds new empty process information structure to the process pool and
     16  * returns it
     17  * @return a new Process instance
     18  */
     19 static Process *new_process_in_pool(void) {
     20 	Process *newprocess = malloc(sizeof(Process));
     21 	if (!newprocess) {
     22 		return NULL;
     23 	}
     24 	newprocess->next = process_pool;
     25 	process_pool = newprocess;
     26 	return newprocess;
     27 }
     28 
     29 /**
     30  * Removes the subprocess information from the pool, sets invalidator to NULL
     31  * and frees resources.
     32  * @param target reference to the process to be removed
     33  * @return the next process in the pool
     34  */
     35 static Process *destroy_process(Process *target) {
     36 	if (target->outfd != -1) {
     37 		close(target->outfd);
     38 	}
     39 	if (target->errfd != -1) {
     40 		close(target->errfd);
     41 	}
     42 	if (target->inpfd != -1) {
     43 		close(target->inpfd);
     44 	}
     45 	/* marking stream as closed for lua */
     46 	if (target->invalidator) {
     47 		*(target->invalidator) = NULL;
     48 	}
     49 	Process *next = target->next;
     50 	free(target->name);
     51 	free(target);
     52 
     53 	return next;
     54 }
     55 
     56 /**
     57  * Starts new subprocess by passing the `command` to the shell and
     58  * returns the subprocess information structure, containing file descriptors
     59  * of the process.
     60  * Also stores the subprocess information to the internal pool to track
     61  * its status and responses.
     62  * @param vis the editor instance
     63  * @param name a string that contains a unique name for the subprocess.
     64  * This name will be passed to the PROCESS_RESPONSE event handler
     65  * to distinguish running subprocesses.
     66  * @param command a command to be executed to spawn a process
     67  * @param invalidator a pointer to the pointer which shows that the subprocess
     68  * is invalid when set to NULL. When the subprocess dies, it is set to NULL.
     69  * If a caller sets the pointer to NULL the subprocess will be killed on the
     70  * next main loop iteration.
     71  */
     72 Process *vis_process_communicate(Vis *vis, const char *name,
     73                                  const char *command, Invalidator **invalidator) {
     74 	int pin[2], pout[2], perr[2];
     75 	pid_t pid = (pid_t)-1;
     76 	if (pipe(perr) == -1) {
     77 		goto closeerr;
     78 	}
     79 	if (pipe(pout) == -1) {
     80 		goto closeouterr;
     81 	}
     82 	if (pipe(pin) == -1) {
     83 		goto closeall;
     84 	}
     85 	pid = fork();
     86 	if (pid == -1) {
     87 		vis_info_show(vis, "fork failed: %s", strerror(errno));
     88 	} else if (pid == 0) { /* child process */
     89 		sigset_t sigterm_mask;
     90 		sigemptyset(&sigterm_mask);
     91 		sigaddset(&sigterm_mask, SIGTERM);
     92 		if (sigprocmask(SIG_UNBLOCK, &sigterm_mask, NULL) == -1) {
     93 			fprintf(stderr, "failed to reset signal mask");
     94 			exit(EXIT_FAILURE);
     95 		}
     96 		dup2(pin[0], STDIN_FILENO);
     97 		dup2(pout[1], STDOUT_FILENO);
     98 		dup2(perr[1], STDERR_FILENO);
     99 	} else { /* main process */
    100 		Process *new = new_process_in_pool();
    101 		if (!new) {
    102 			vis_info_show(vis, "Cannot create process: %s", strerror(errno));
    103 			goto closeall;
    104 		}
    105 		new->name = strdup(name);
    106 		if (!new->name) {
    107 			vis_info_show(vis, "Cannot copy process name: %s", strerror(errno));
    108 			/* pop top element (which is `new`) from the pool */
    109 			process_pool = destroy_process(process_pool);
    110 			goto closeall;
    111 		}
    112 		new->outfd = pout[0];
    113 		new->errfd = perr[0];
    114 		new->inpfd = pin[1];
    115 		new->pid = pid;
    116 		new->invalidator = invalidator;
    117 		close(pin[0]);
    118 		close(pout[1]);
    119 		close(perr[1]);
    120 		return new;
    121 	}
    122 closeall:
    123 	close(pin[0]);
    124 	close(pin[1]);
    125 closeouterr:
    126 	close(pout[0]);
    127 	close(pout[1]);
    128 closeerr:
    129 	close(perr[0]);
    130 	close(perr[1]);
    131 	if (pid == 0) { /* start command in child process */
    132 		execlp(vis->shell, vis->shell, "-c", command, (char*)NULL);
    133 		fprintf(stderr, "exec failed: %s(%d)\n", strerror(errno), errno);
    134 		exit(1);
    135 	} else {
    136 		vis_info_show(vis, "process creation failed: %s", strerror(errno));
    137 	}
    138 	return NULL;
    139 }
    140 
    141 /**
    142  * Adds file descriptors of currently running subprocesses to the `readfds`
    143  * to track their readiness and returns maximum file descriptor value
    144  * to pass it to the `pselect` call
    145  * @param readfds the structure for `pselect` call to fill
    146  * @return maximum file descriptor number in the readfds structure
    147  */
    148 int vis_process_before_tick(fd_set *readfds) {
    149 	int maxfd = 0;
    150 	for (Process **pointer = &process_pool; *pointer; pointer = &((*pointer)->next)) {
    151 		Process *current = *pointer;
    152 		if (current->outfd != -1) {
    153 			FD_SET(current->outfd, readfds);
    154 			maxfd = maxfd < current->outfd ? current->outfd : maxfd;
    155 		}
    156 		if (current->errfd != -1) {
    157 			FD_SET(current->errfd, readfds);
    158 			maxfd = maxfd < current->errfd ? current->errfd : maxfd;
    159 		}
    160 	}
    161 	return maxfd;
    162 }
    163 
    164 /**
    165  * Reads data from the given subprocess file descriptor `fd` and fires
    166  * the PROCESS_RESPONSE event in Lua with given subprocess `name`,
    167  * `rtype` and the read data as arguments.
    168  * @param vis the editor instance
    169  * @param fd the file descriptor to read data from
    170  * @param name a name of the subprocess
    171  * @param rtype a type of file descriptor where the new data is found
    172  */
    173 static void read_and_fire(Vis* vis, int fd, const char *name, ResponseType rtype) {
    174 	static char buffer[PIPE_BUF];
    175 	size_t obtained = read(fd, &buffer, PIPE_BUF-1);
    176 	if (obtained > 0) {
    177 		vis_lua_process_response(vis, name, buffer, obtained, rtype);
    178 	}
    179 }
    180 
    181 /**
    182  * Checks if a subprocess is dead or needs to be killed then raises an event
    183  * or kills it if necessary.
    184  * @param vis the editor instance
    185  * @param current the process to wait for or kill
    186  * @return true if the process is dead
    187  */
    188 static bool wait_or_kill_process(Vis *vis, Process *current) {
    189 	int status;
    190 	pid_t wpid = waitpid(current->pid, &status, WNOHANG);
    191 	if (wpid == -1) {
    192 		vis_message_show(vis, strerror(errno));
    193 	} else if (wpid == current->pid) {
    194 		goto just_destroy;
    195 	} else if (!*(current->invalidator)) {
    196 		goto kill_and_destroy;
    197 	}
    198 	return false;
    199 
    200 kill_and_destroy:
    201 	kill(current->pid, SIGTERM);
    202 	waitpid(current->pid, &status, 0);
    203 just_destroy:
    204 	if (WIFSIGNALED(status)) {
    205 		vis_lua_process_response(vis, current->name, NULL, WTERMSIG(status), SIGNAL);
    206 	} else {
    207 		vis_lua_process_response(vis, current->name, NULL, WEXITSTATUS(status), EXIT);
    208 	}
    209 	return true;
    210 }
    211 
    212 /**
    213  * Checks if `readfds` contains file descriptors of subprocesses from
    214  * the pool. If so, it reads their data and fires corresponding events.
    215  * Also checks if each subprocess from the pool is dead or needs to be
    216  * killed then raises an event or kills it if necessary.
    217  * @param vis the editor instance
    218  * @param readfds the structure for `pselect` call with file descriptors
    219  */
    220 void vis_process_tick(Vis *vis, fd_set *readfds) {
    221 	for (Process **pointer = &process_pool; *pointer; ) {
    222 		Process *current = *pointer;
    223 		if (current->outfd != -1 && FD_ISSET(current->outfd, readfds)) {
    224 			read_and_fire(vis, current->outfd, current->name, STDOUT);
    225 		}
    226 		if (current->errfd != -1 && FD_ISSET(current->errfd, readfds)) {
    227 			read_and_fire(vis, current->errfd, current->name, STDERR);
    228 		}
    229 		if (!wait_or_kill_process(vis, current)) {
    230 			pointer = &current->next;
    231 		} else {
    232 			/* update our iteration pointer */
    233 			*pointer = destroy_process(current);
    234 		}
    235 	}
    236 }
    237 
    238 /**
    239  * Checks if each subprocess from the pool is dead or needs to be
    240  * killed then raises an event or kills it if necessary.
    241  */
    242 void vis_process_waitall(Vis *vis) {
    243 	for (Process **pointer = &process_pool; *pointer; ) {
    244 		Process *current = *pointer;
    245 		if (!wait_or_kill_process(vis, current)) {
    246 			pointer = &current->next;
    247 		} else {
    248 			/* update our iteration pointer */
    249 			*pointer = destroy_process(current);
    250 		}
    251 	}
    252 }