chiark / gitweb /
0493efafd1be7f2f41075177b273d987dd40e8bc
[misc] / timeout.c
1 /* -*-c-*-
2  *
3  * Run a command with a timeout.
4  *
5  * (c) 2011 Mark Wooding
6  */
7
8 /*----- Licensing notice --------------------------------------------------*
9  *
10  * This file is part of the Toys utilties collection.
11  *
12  * Toys is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  * Toys is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with Toys; if not, write to the Free Software Foundation,
24  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25  */
26
27 /*----- Basic strategy ----------------------------------------------------*
28  *
29  * There's an obvious and simple way to make a process stop after a certain
30  * amount of time: set an alarm.  Alarms are inherited, so this will take
31  * down the whole subprocess tree.  Unfortunately, processes use alarms for
32  * all sorts of things.  And this relies on the process not fiddling with its
33  * @SIGALRM@ handler.
34  *
35  * The other possibility is to have a separate process which kills our target
36  * program when the time's up.  In order to do this effectively, we need to
37  * trap the whole thing in a process group.  Then we need to wait until the
38  * time is up or the process finished.  We'll assume that the top-level
39  * process finishing is sufficient indication that we don't need to hang on
40  * any longer.
41  *
42  * This isn't perfect.  It won't catch stragglers if the target process
43  * creates its own sub-process groups -- so, in particular, nested timeouts
44  * won't behave in an obvious way.  And, possibly most annoyingly, it
45  * interferes with the way terminal I/O works.  I'm sorry: you can't have
46  * everything.
47  */
48
49 /*----- Header files ------------------------------------------------------*/
50
51 #include <ctype.h>
52 #include <errno.h>
53 #include <signal.h>
54 #include <stdio.h>
55 #include <stdlib.h>
56 #include <string.h>
57
58 #include <sys/types.h>
59 #include <sys/time.h>
60 #include <fcntl.h>
61 #include <unistd.h>
62 #include <sys/select.h>
63 #include <sys/wait.h>
64
65 #include <mLib/fdflags.h>
66 #include <mLib/macros.h>
67 #include <mLib/mdwopt.h>
68 #include <mLib/quis.h>
69 #include <mLib/report.h>
70 #include <mLib/sel.h>
71 #include <mLib/sig.h>
72 #include <mLib/tv.h>
73
74 /*----- Static variables --------------------------------------------------*/
75
76 static sel_state sel;
77 static int state;
78
79 enum {
80   ST_WAIT,
81   ST_ABORT,
82   ST_DONE
83 };
84
85 /*----- Argument conversion functions -------------------------------------*/
86
87 /* --- @namesig@ --- *
88  *
89  * Arguments:   @const char *p@ = pointer to string
90  *
91  * Returns:     Signal number, or @-1@ for no match.
92  *
93  * Use:         Converts a signal name into its number.
94  */
95
96 struct namesig {
97   const char *name;
98   int sig;
99 };
100
101 int cmp_namesig(const void *k, const void *v)
102   { const struct namesig *ns = v; return (strcmp(k, ns->name)); }
103
104 static int namesig(const char *p)
105 {
106   const static struct namesig tab[] = {
107 /*
108   ;;; The signal name table is very boring to type.  To make life less awful,
109   ;;; put the signal names in this list and evaluate the code to get Emacs to
110   ;;; regenerate it.  We use @bsearch@ on it, so it's important that it be
111   ;;; sorted: Emacs does this for us.
112   (let ((signals '(HUP INT QUIT ILL ABRT FPE KILL SEGV PIPE ALRM TERM
113                    USR1 USR2 CHLD CONT STOP TSTP TTIN TTOU BUS POLL
114                    PROF SYS TRAP URG VTALRM XCPU XFSZ IOT EMT STKFLT
115                    IO CLD PWR INFO LOST WINCH)))
116     (save-excursion
117       (goto-char (point-min))
118       (let ((start (search-forward (concat "/" "* SIGLIST *" "/\n")))
119             (end (search-forward (concat "/" "* END *" "/\n"))))
120         (delete-region start end))
121       (dolist (sig (sort (copy-list signals) #'string<))
122         (insert (format "#ifdef SIG%s\n    { \"%s\", SIG%s },\n#endif\n"
123                         sig sig sig)))
124       (insert (concat "/" "* END *" "/\n"))))
125 */
126
127 /* SIGLIST */
128 #ifdef SIGABRT
129     { "ABRT", SIGABRT },
130 #endif
131 #ifdef SIGALRM
132     { "ALRM", SIGALRM },
133 #endif
134 #ifdef SIGBUS
135     { "BUS", SIGBUS },
136 #endif
137 #ifdef SIGCHLD
138     { "CHLD", SIGCHLD },
139 #endif
140 #ifdef SIGCLD
141     { "CLD", SIGCLD },
142 #endif
143 #ifdef SIGCONT
144     { "CONT", SIGCONT },
145 #endif
146 #ifdef SIGEMT
147     { "EMT", SIGEMT },
148 #endif
149 #ifdef SIGFPE
150     { "FPE", SIGFPE },
151 #endif
152 #ifdef SIGHUP
153     { "HUP", SIGHUP },
154 #endif
155 #ifdef SIGILL
156     { "ILL", SIGILL },
157 #endif
158 #ifdef SIGINFO
159     { "INFO", SIGINFO },
160 #endif
161 #ifdef SIGINT
162     { "INT", SIGINT },
163 #endif
164 #ifdef SIGIO
165     { "IO", SIGIO },
166 #endif
167 #ifdef SIGIOT
168     { "IOT", SIGIOT },
169 #endif
170 #ifdef SIGKILL
171     { "KILL", SIGKILL },
172 #endif
173 #ifdef SIGLOST
174     { "LOST", SIGLOST },
175 #endif
176 #ifdef SIGPIPE
177     { "PIPE", SIGPIPE },
178 #endif
179 #ifdef SIGPOLL
180     { "POLL", SIGPOLL },
181 #endif
182 #ifdef SIGPROF
183     { "PROF", SIGPROF },
184 #endif
185 #ifdef SIGPWR
186     { "PWR", SIGPWR },
187 #endif
188 #ifdef SIGQUIT
189     { "QUIT", SIGQUIT },
190 #endif
191 #ifdef SIGSEGV
192     { "SEGV", SIGSEGV },
193 #endif
194 #ifdef SIGSTKFLT
195     { "STKFLT", SIGSTKFLT },
196 #endif
197 #ifdef SIGSTOP
198     { "STOP", SIGSTOP },
199 #endif
200 #ifdef SIGSYS
201     { "SYS", SIGSYS },
202 #endif
203 #ifdef SIGTERM
204     { "TERM", SIGTERM },
205 #endif
206 #ifdef SIGTRAP
207     { "TRAP", SIGTRAP },
208 #endif
209 #ifdef SIGTSTP
210     { "TSTP", SIGTSTP },
211 #endif
212 #ifdef SIGTTIN
213     { "TTIN", SIGTTIN },
214 #endif
215 #ifdef SIGTTOU
216     { "TTOU", SIGTTOU },
217 #endif
218 #ifdef SIGURG
219     { "URG", SIGURG },
220 #endif
221 #ifdef SIGUSR1
222     { "USR1", SIGUSR1 },
223 #endif
224 #ifdef SIGUSR2
225     { "USR2", SIGUSR2 },
226 #endif
227 #ifdef SIGVTALRM
228     { "VTALRM", SIGVTALRM },
229 #endif
230 #ifdef SIGWINCH
231     { "WINCH", SIGWINCH },
232 #endif
233 #ifdef SIGXCPU
234     { "XCPU", SIGXCPU },
235 #endif
236 #ifdef SIGXFSZ
237     { "XFSZ", SIGXFSZ },
238 #endif
239 /* END */
240   };
241
242   const struct namesig *ns = bsearch(p, tab, N(tab), sizeof(tab[0]),
243                                      cmp_namesig);
244   if (ns) return (ns->sig);
245   else if (isdigit((unsigned char)*p)) return (atoi(p));
246   else return (-1);
247 }
248
249 /*----- Help functions ----------------------------------------------------*/
250
251 static void usage(FILE *fp)
252   { pquis(fp, "Usage: $ [-s SIG] SECONDS COMMAND [ARGUMENTS ...]\n"); }
253
254 static void version(FILE *fp)
255   { pquis(fp, "$ (version " VERSION ")\n"); }
256
257 static void help(FILE *fp)
258 {
259   version(fp); fputc('\n', stdout);
260   usage(fp);
261   pquis(fp, "\n\
262 Run COMMAND, giving it the ARGUMENTS.  If it fails to complete within the\n\
263 specified number of SECONDS, kill it.  Otherwise exit with the status it\n\
264 returns.\n                                                              \
265 \n\
266 Options:\n\
267   -h, --help            Show this help text.\n\
268   -v, --version         Show version string.\n\
269   -u, --usage           Show a terse usage summary.\n\
270 \n\
271   -s, --signal=SIG      Send signal SIG to the command.\n\
272 ");
273 }
274
275 /*----- Timeout handling --------------------------------------------------*/
276
277 /* --- @timeout@ --- *
278  *
279  * The first time, we send the signal requested by the caller.  Then we wait
280  * five seconds for the child to die, and send @SIGKILL@.  If that still
281  * doesn't help, then we just give up.  It's not like there's anything else
282  * we can do which is likely to help.  And it's not like the process is going
283  * to be doing anything else in user mode ever again.
284  */
285
286 struct timeout {
287   sel_timer t;
288   int panic;
289   int sig;
290   pid_t kid;
291 };
292
293 static void timeout(struct timeval *now, void *p)
294 {
295   struct timeout *t = p;
296   struct timeval tv;
297
298   switch (t->panic) {
299     case 0:
300       moan("timed out: killing child process");
301       kill(-t->kid, t->sig);
302       break;
303     case 1:
304       moan("child hasn't responded: killing harder");
305       kill(-t->kid, SIGKILL);
306       break;
307     case 2:
308       moan("child still undead: giving up");
309       state = ST_ABORT;
310       break;
311   }
312   TV_ADDL(&tv, now, 5, 0);
313   sel_addtimer(&sel, &t->t, &tv, timeout, t);
314   t->panic++;
315 }
316
317 /*----- Signal handling ---------------------------------------------------*/
318
319 /* --- @sigchld@ --- *
320  *
321  * If it's our child that died, fetch its exit status and stop.
322  */
323
324 struct sigchld {
325   pid_t kid;
326   int rc;
327 };
328
329 static void sigchld(int sig, void *p)
330 {
331   struct sigchld *s = p;
332   int rc;
333   pid_t pid;
334
335   if ((pid = waitpid(s->kid, &rc, WNOHANG)) < 0)
336     die(254, "waitpid failed: %s", strerror(errno));
337   if (pid == s->kid) {
338     s->rc = rc;
339     state = ST_DONE;
340   }
341 }
342
343 /* --- @sigpropagate@ --- *
344  *
345  * Propagate various signals to the child process group and then maybe act on
346  * them.
347  */
348
349 static void sigpropagate(int sig, void *p)
350 {
351   struct sigchld *s = p;
352
353   kill(-s->kid, sig);
354   switch (sig) {
355     case SIGTSTP: raise(SIGSTOP); break;
356   }
357 }
358
359 #define PROPAGATE_SIGNALS(_)                                            \
360   _(TSTP) _(CONT) _(INT) _(HUP) _(QUIT)
361
362 /*----- Main program ------------------------------------------------------*/
363
364 /* --- @main@ --- */
365
366 int main(int argc, char *const argv[])
367 {
368   char *p;
369   double t;
370   int signo = SIGTERM;
371   pid_t kid;
372   struct timeval tv;
373   struct timeout to;
374   struct sigchld sc;
375   sig sig_CHLD;
376 #define DEFSIG(tag) sig sig_##tag;
377   PROPAGATE_SIGNALS(DEFSIG)
378 #undef DEFSIG
379   unsigned f = 0;
380 #define F_BOGUS 1u
381
382   /* --- Option parsing --- */
383
384   ego(argv[0]);
385
386   for (;;) {
387     static const struct option opts[] = {
388       { "help",                 0,              0,      'h' },
389       { "version",              0,              0,      'v' },
390       { "usage",                0,              0,      'u' },
391       { "signal",               OPTF_ARGREQ,    0,      's' },
392       { 0,                      0,              0,      0 }
393     };
394
395     int i = mdwopt(argc, argv, "+hvus:", opts, 0, 0, 0);
396     if (i < 0) break;
397     switch (i) {
398       case 'h': help(stdout); exit(0);
399       case 'v': version(stdout); exit(0);
400       case 'u': usage(stdout); exit(0);
401       case 's':
402         if ((signo = namesig(optarg)) < 0)
403           die(253, "bad signal spec `%s'", optarg);
404         break;
405       default: f |= F_BOGUS; break;
406     }
407   }
408   argc -= optind; argv += optind;
409   if ((f & F_BOGUS) || argc < 2) { usage(stderr); exit(253); }
410
411   p = argv[0];
412   while (isspace((unsigned char)*p)) p++;
413   t = strtod(argv[0], &p);
414   while (isspace((unsigned char)*p)) p++;
415   if (*p) die(254, "bad time value `%s'", argv[0]);
416
417   /* --- Get things set up --- */
418
419   state = ST_WAIT;
420   sel_init(&sel);
421
422   /* --- Set up signal handling --- *
423    *
424    * Doing anything with asynchronous signals is a mug's game, so we bring
425    * them in-band.  Do this before starting the child process, because
426    * otherwise we might miss an immediate @SIGCHLD@.
427    */
428
429   sig_init(&sel);
430   sc.rc = 0;
431   sig_add(&sig_CHLD, SIGCHLD, sigchld, &sc);
432 #define ADDSIG(tag) sig_add(&sig_##tag, SIG##tag, sigpropagate, &sc);
433   PROPAGATE_SIGNALS(ADDSIG)
434 #undef ADDSIG
435
436   /* --- Now start the child process --- */
437
438   if ((kid = fork()) < 0) die(2, "fork failed: %s", strerror(errno));
439   if (!kid) {
440     setpgid(0, 0);
441     execvp(argv[1], argv + 1);
442     _exit(252);
443   }
444   sc.kid = kid;
445
446   /* --- Set up the timer --- */
447
448   to.kid = kid;
449   to.sig = signo;
450   to.panic = 0;
451   gettimeofday(&tv, 0);
452   TV_ADDL(&tv, &tv, (time_t)t, ((long)(t * 1000000))%1000000);
453   sel_addtimer(&sel, &to.t, &tv, timeout, &to);
454
455   /* --- Main @select@ loop */
456
457   while (state == ST_WAIT) {
458     if (sel_select(&sel)) {
459       if (errno == EINTR) continue;
460       die(254, "select failed: %s", strerror(errno));
461     }
462   }
463
464   /* --- Check and translate the exit code --- */
465
466   switch (state) {
467     case ST_ABORT:
468       exit(251);
469     case ST_DONE:
470       if (WIFEXITED(sc.rc))
471         exit(WEXITSTATUS(sc.rc));
472       else if (WIFSIGNALED(sc.rc))
473         exit(128 | WTERMSIG(sc.rc));
474       else
475         exit(128);
476     default:
477       moan("FATAL: unexpected state %d", state);
478       abort();
479   }
480 }
481
482 /*----- That's all, folks -------------------------------------------------*/