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 = ¤t->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 = ¤t->next;
247 } else {
248 /* update our iteration pointer */
249 *pointer = destroy_process(current);
250 }
251 }
252 }