reap
[gemrepl.git] / main.c
blobea73b237cc22a444f6030cd32c342ddb68930c00
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 /* How long in ms to wait for child to output something */
20 #define READ_TIMEOUT_INITIAL 5000
22 /* How long in ms child can pause between writes before we consider it to have
23 * finished writing */
24 #define READ_TIMEOUT_SUBSEQUENT 500
27 typedef struct Child {
28 bool exists;
29 char owner[32];
30 uint64_t last_active;
31 pid_t pid;
32 int in;
33 int out;
34 } Child;
36 typedef struct State {
37 const char *command;
38 char *const *args;
39 int max_children;
40 int num_children;
41 Child children[MAX_CHILDREN];
42 } State;
44 static bool spawn(const char *command, char *const *args, Child *child, int socket)
46 int infds[2], outfds[2];
47 if (pipe(infds) == -1 || pipe(outfds) == -1) {
48 perror("pipe");
49 return false;
52 const pid_t pid = fork();
53 if (pid == -1) {
54 perror("fork");
55 return false;
58 if (pid == 0) {
59 // child
60 close(socket);
61 close(infds[1]);
62 close(outfds[0]);
63 dup2(infds[0], 0);
64 dup2(outfds[1], 1);
65 dup2(outfds[1], 2);
66 setlinebuf(stdin);
67 setlinebuf(stdout);
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) {
106 char buf[256];
107 struct pollfd pfd = { in, POLLIN | POLLHUP, 0 };
108 int backticks = 0;
110 poll(&pfd, 1, READ_TIMEOUT_INITIAL);
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, READ_TIMEOUT_SUBSEQUENT);
139 return (pfd.revents & POLLHUP ? 0 : 1);
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, 32)) {
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 if (!spawn(state->command, state->args, child, socket)) {
193 put("40 Spawn failure.\r\n");
194 return;
197 set_child_last_active(child);
198 child->exists = true;
199 strncpy(child->owner, request_info->tls_client_hash, 32);
201 spawned = true;
204 const char *q = request_info->query_string_decoded;
205 if (*q == '!') {
206 ++q;
207 if (*q == '?') {
208 put("10\r\n");
209 return;
210 } else if (*q != '!') {
211 put("40 Unknown gemrepl meta-command (use '!!' for a literal '!')\r\n");
215 put("20 text/gemini\r\n");
216 put("=> ?!? Input command\r\n");
218 kill(child->pid, SIGCONT);
220 const int qlen = strlen(q);
221 if (!spawned || qlen > 0) {
222 signal(SIGPIPE, SIG_IGN);
223 bool err = (write(child->in, q, qlen) < 0
224 || write(child->in, "\n", 1) < 0);
225 signal(SIGPIPE, SIG_DFL);
226 if (err) {
227 put("[gemrepl: error when writing to child]\r\n");
231 put("```\r\n");
232 const int succ = stream_text(child->out, socket, true);
233 put("\r\n```\r\n");
234 if (succ < 0) put("[gemrepl: error when reading from child]\r\n");
235 else if (succ == 0) {
236 // got HUP; sleep briefly to give child a chance to exit
237 usleep(100);
240 set_child_last_active(child);
242 if (waitpid(child->pid, NULL, WNOHANG) == child->pid) {
243 put("[gemrepl: child process terminated]");
244 close(child->in);
245 close(child->out);
246 child->exists = false;
247 } else {
248 kill(child->pid, SIGSTOP);
252 static void usage()
254 printf("Usage: gemrepl [OPTION]... COMMAND [ARG]...\n");
255 printf(" -h --help This help\n");
256 printf(" -m NUM --max-children=NUM Max concurrent children to spawn (default %d)\n", MAX_CHILDREN);
259 int main(int argc, char **argv)
261 if (argc < 2) {
262 usage();
263 exit(1);
266 State *state = malloc(sizeof(State));
267 if (state == NULL) {
268 fprintf(stderr, "Failed to allocate memory for state.");
269 exit(1);
272 int max_children = -1;
274 const struct option longoptions[] =
275 { { "help", 0, NULL, 'h' }
276 , { "max-children", 1, NULL, 'm' }
277 , { 0,0,0,0 }
279 int o;
280 while (-1 != (o = getopt_long(argc, argv, "hm:", longoptions, NULL))) {
281 switch (o) {
282 case 'h':
283 case '?':
284 usage();
285 exit(0);
287 case 'm':
288 max_children = atoi(optarg);
289 if (max_children <= 0 || max_children > MAX_CHILDREN) {
290 printf("Bad value for max children.\n");
291 printf("You may need to increase MAX_CHILDREN in the source.\n");
292 exit(1);
294 break;
298 if (argv[optind] == NULL) {
299 usage();
300 exit(1);
303 state->command = argv[optind];
304 state->args = &argv[optind];
305 state->max_children = max_children > 0 ? max_children : MAX_CHILDREN;
307 runSCGI("/tmp/gemrepl_socket", respond, state);