don't pass \!restart to child
[gemrepl.git] / main.c
blob7f6900f69afbaf56a34ee9996364a62ca65ca7e0
1 #include <getopt.h>
2 #include <poll.h>
3 #include <signal.h>
4 #include <stdbool.h>
5 #include <stdio.h>
6 #include <stdint.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <time.h>
10 #include <sys/types.h>
11 #include <sys/wait.h>
12 #include <unistd.h>
14 #include "gemscgi.h"
16 /* If you increase this too far, you may run into file descriptor limits */
17 #define MAX_CHILDREN 256
19 typedef struct Child {
20 bool exists;
21 char owner[64];
22 uint64_t last_active;
23 pid_t pid;
24 int in;
25 int out;
26 bool nolink;
27 } Child;
29 typedef struct State {
30 const char *command;
31 char *const *args;
33 int max_children;
34 int read_timeout;
35 int pause_timeout;
37 int num_children;
38 Child children[MAX_CHILDREN];
39 } State;
41 static bool spawn(const char *command, char *const *args, Child *child, int socket)
43 int infds[2], outfds[2];
44 if (pipe(infds) == -1 || pipe(outfds) == -1) {
45 perror("pipe");
46 return false;
49 const pid_t pid = fork();
50 if (pid == -1) {
51 perror("fork");
52 return false;
55 if (pid == 0) {
56 // child
57 close(socket);
58 close(infds[1]);
59 close(outfds[0]);
60 dup2(infds[0], 0);
61 dup2(outfds[1], 1);
62 dup2(outfds[1], 2);
63 setlinebuf(stdin);
64 setlinebuf(stdout);
65 char tlsenv[81];
66 snprintf(tlsenv, 81, "TLS_CLIENT_HASH=%s", child->owner);
67 putenv(tlsenv);
68 execvp(command, args);
69 exit(1);
70 } else {
71 // parent
72 close(infds[0]);
73 close(outfds[1]);
74 child->pid = pid;
75 child->in = infds[1];
76 child->out = outfds[0];
77 setlinebuf(fdopen(infds[1], "w"));
78 setlinebuf(fdopen(outfds[0], "r"));
81 return true;
84 static bool write_all(int fd, const char* buf, int n)
86 while (n > 0) {
87 int w = write(fd, buf, n);
88 if (w < 0) return false;
89 buf += w;
90 n -= w;
92 return true;
95 static void set_child_last_active(Child *child)
97 struct timespec clock_mono;
98 clock_gettime(CLOCK_MONOTONIC, &clock_mono);
99 child->last_active = clock_mono.tv_sec;
102 /* Write anything written timelily on `in` to `out`, converting \n to \r\n.
103 * If `gemtext` is true, also space-stuff lines beginning with "```".
104 * Return -1 on read error, 0 on HUP, else 1. */
105 static int stream_text(int in, int out, bool gemtext, int read_timeout, int pause_timeout) {
106 char buf[256];
107 struct pollfd pfd = { in, POLLIN | POLLHUP, 0 };
108 int backticks = 0;
110 poll(&pfd, 1, read_timeout);
111 while (pfd.revents & POLLIN) {
112 const int r = read(in, buf, 256 - 1);
113 if (r < 0) return false;
114 buf[r] = 0;
116 const char *b = buf;
117 while (*b) {
118 if (gemtext && backticks >= 0) {
119 if (*b == '`') {
120 ++backticks;
121 if (backticks == 3) {
122 write(out, " ```", 4);
123 backticks = 0;
125 ++b;
126 continue;
127 } else while (--backticks >= 0) write(out, "`", 1);
130 if (*b == '\n') {
131 write(out, "\r\n", 2);
132 backticks = 0;
133 } else write(out, b, 1);
134 ++b;
137 poll(&pfd, 1, pause_timeout);
139 return (!(pfd.revents & POLLHUP));
142 void respond(void *object, const Request_Info *request_info, int socket)
144 State *state = (State *)object;
146 #define put(s) write_all(socket, s, strlen(s))
148 if (!request_info->tls_client_hash) {
149 put("60 Client certificate required\r\n");
150 return;
153 Child *child = NULL, *slot = NULL;
154 bool spawned = false;
156 /* Find child with this cert hash, or spawn new.
157 * For simplicity, we use a static array of children rather than
158 * allocating dynamically. This wastes a few KB of memory; you may want to
159 * rewrite this if memory is tight. We also don't bother to keep the list
160 * sorted, but just strcmp for each child. Terribly wasteful. */
161 for (int i = 0; i < state->num_children; ++i) {
162 Child *const c = &state->children[i];
163 if (c->exists) {
164 if (0 == strncmp(c->owner,
165 request_info->tls_client_hash, 64)) {
166 child = c;
167 break;
170 if (slot == NULL || (slot->exists
171 && slot->last_active > c->last_active)) {
172 slot = c;
174 } else if (slot == NULL || slot->exists) slot = c;
177 if (child == NULL) {
178 if (slot == NULL || (slot->exists && state->num_children < state->max_children)) {
179 slot = &state->children[state->num_children++];
181 child = slot;
183 if (child->exists) {
184 // TODO: would be nice to queue a regretful message for the owner
185 // of the child we're killing...
186 close(child->in);
187 close(child->out);
188 kill(child->pid, 9);
189 child->exists = false;
192 memset(child, 0, sizeof(Child));
194 if (request_info->tls_client_hash == NULL) child->owner[0] = 0;
195 else strncpy(child->owner, request_info->tls_client_hash, 64);
197 if (!spawn(state->command, state->args, child, socket)) {
198 put("40 Spawn failure.\r\n");
199 return;
202 set_child_last_active(child);
203 child->exists = true;
205 spawned = true;
208 const char *q = request_info->query_string_decoded;
209 if (*q == '!') {
210 ++q;
211 if (*q == '?') {
212 put("10\r\n");
213 return;
214 } else if (0 == strncmp(q, "help", strlen(q))) {
215 put("20 text/gemini\r\n");
216 put("An input line not beginning with '!' will be passed to the process.\r\n");
217 put("A newline will be appended unless the line ends with a trailing backslash.\r\n");
218 put("\r\n");
219 put("# gemrepl meta commands\r\n");
220 put("=> ?!help !help: This help\r\n");
221 put("=> ?!restart !restart: kill process and start again\r\n");
222 put("=> ?!nolink !nolink: suppress input link\r\n");
223 put("=> ?!showlink !showlink: show input link\r\n");
224 put("=> ?!? !?: Prompt for input\r\n");
225 put("=> ?!! !!: Literal '!'\r\n");
226 return;
227 } else if (0 == strncmp(q, "restart", strlen(q))) {
228 kill(child->pid, SIGKILL);
229 q += strlen(q);
230 } else if (0 == strncmp(q, "nolink", strlen(q))) {
231 // TODO: might be better to have this be a permanent option
232 // attached to the cert rather than the child.
233 child->nolink = true;
234 put("20 text/gemini\r\n");
235 put("Input links disabled.\r\n");
236 put("=> ?!showlink re-enable\r\n");
237 return;
238 } else if (0 == strncmp(q, "showlink", strlen(q))) {
239 child->nolink = false;
240 put("20 text/gemini\r\n");
241 put("Input links enabled.\r\n");
242 put("=> ?!? Input command\r\n");
243 return;
244 } else if (*q != '!') {
245 put("40 Unknown gemrepl meta-command (use '!!' for a literal '!')\r\n");
246 return;
250 put("20 text/gemini\r\n");
251 if (spawned) {
252 put("[gemrepl child spawned. Input \"!help\" for meta-commands]\r\n");
255 if (!child->nolink) put("=> ?!? Input command\r\n");
257 if (!spawned) kill(child->pid, SIGCONT);
259 int qlen = strlen(q);
260 if (!spawned || qlen > 0) {
261 bool newline = true;
262 if (q[qlen-1] == '\\') {
263 --qlen;
264 newline = false;
266 signal(SIGPIPE, SIG_IGN);
267 bool succ = (write(child->in, q, qlen) == qlen
268 && (!newline || write(child->in, "\n", 1) == 1));
269 signal(SIGPIPE, SIG_DFL);
270 if (!succ) {
271 put("[gemrepl: error when writing to child]\r\n");
275 put("```\r\n");
276 const int succ = stream_text(child->out, socket, true, state->read_timeout, state->pause_timeout);
277 put("\r\n```\r\n");
278 if (succ < 0) put("[gemrepl: error when reading from child]\r\n");
279 else if (succ == 0) {
280 // got HUP; sleep briefly to give child a chance to exit
281 usleep(100);
284 set_child_last_active(child);
286 if (waitpid(child->pid, NULL, WNOHANG) == child->pid) {
287 put("[gemrepl: child process terminated]");
288 close(child->in);
289 close(child->out);
290 child->exists = false;
291 } else {
292 kill(child->pid, SIGSTOP);
296 /* How long in ms to wait for child to output something */
297 #define DEF_READ_TIMEOUT 5000
299 /* How long in ms child can pause between writes before we consider it to have
300 * finished writing */
301 #define DEF_PAUSE_TIMEOUT 500
303 static void usage()
305 printf("Usage: gemrepl [OPTION]... -s PATH COMMAND [ARG]...\n");
306 printf(" -h --help This help\n");
307 printf(" -s PATH --socket=PATH Path for socket file, which will be created\n");
308 printf(" -m NUM --max-children=NUM Max concurrent children to spawn (%d)\n", MAX_CHILDREN);
309 printf(" -t MS --read-timeout=MS Time to wait for child to start writing (%d)\n", DEF_READ_TIMEOUT);
310 printf(" -T MS --pause-timeout=MS Silence period after which child is paused (%d)\n", DEF_PAUSE_TIMEOUT);
314 int main(int argc, char **argv)
316 if (argc < 2) {
317 usage();
318 exit(1);
321 State *state = malloc(sizeof(State));
322 if (state == NULL) {
323 fprintf(stderr, "Failed to allocate memory for state.");
324 exit(1);
327 state->max_children = MAX_CHILDREN;
328 state->read_timeout = 5000;
329 state->pause_timeout = 500;
331 const struct option longoptions[] =
332 { { "help", 0, NULL, 'h' }
333 , { "socket", 1, NULL, 's' }
334 , { "max-children", 1, NULL, 'm' }
335 , { "read-timeout", 1, NULL, 't' }
336 , { "pause-timeout", 1, NULL, 'T' }
337 , { 0,0,0,0 }
339 int o;
340 const char *socketname = NULL;
341 while (-1 != (o = getopt_long(argc, argv, "+hs:m:t:T:", longoptions, NULL))) {
342 switch (o) {
343 case 'h':
344 case '?':
345 usage();
346 exit((o=='?'));
347 case 's':
348 socketname = optarg;
349 break;
350 case 'm':
351 state->max_children = atoi(optarg);
352 if (state->max_children <= 0 || state->max_children > MAX_CHILDREN) {
353 printf("Bad value for max children.\n");
354 printf("You may need to increase MAX_CHILDREN in the source.\n");
355 exit(1);
357 break;
358 case 't':
359 state->read_timeout = atoi(optarg);
360 if (state->read_timeout < 0) {
361 printf("Bad value for read timeout.\n");
362 exit(1);
364 break;
365 case 'T':
366 state->pause_timeout = atoi(optarg);
367 if (state->pause_timeout < 0) {
368 printf("Bad value for pause timeout.\n");
369 exit(1);
371 break;
375 if (argv[optind] == NULL || socketname == NULL) {
376 usage();
377 exit(1);
380 state->command = argv[optind];
381 state->args = &argv[optind];
383 runSCGI(socketname, respond, state);