chiark / gitweb /
hush.in: Close extraneous file descriptors when running the command.
[misc] / mtimeout.c
CommitLineData
e825e5a9
MW
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>
753f86de 53#include <math.h>
e825e5a9
MW
54#include <signal.h>
55#include <stdio.h>
56#include <stdlib.h>
57#include <string.h>
58
59#include <sys/types.h>
60#include <sys/time.h>
61#include <fcntl.h>
62#include <unistd.h>
63#include <sys/select.h>
64#include <sys/wait.h>
65
66#include <mLib/fdflags.h>
67#include <mLib/macros.h>
68#include <mLib/mdwopt.h>
69#include <mLib/quis.h>
70#include <mLib/report.h>
71#include <mLib/sel.h>
72#include <mLib/sig.h>
73#include <mLib/tv.h>
74
75/*----- Static variables --------------------------------------------------*/
76
77static sel_state sel;
78static int state;
79
80enum {
81 ST_WAIT,
82 ST_ABORT,
83 ST_DONE
84};
85
86/*----- Argument conversion functions -------------------------------------*/
87
88/* --- @namesig@ --- *
89 *
90 * Arguments: @const char *p@ = pointer to string
91 *
92 * Returns: Signal number, or @-1@ for no match.
93 *
94 * Use: Converts a signal name into its number.
95 */
96
97struct namesig {
98 const char *name;
99 int sig;
100};
101
102int cmp_namesig(const void *k, const void *v)
103 { const struct namesig *ns = v; return (strcmp(k, ns->name)); }
104
105static int namesig(const char *p)
106{
107 const static struct namesig tab[] = {
e33fa755
MW
108 /*
109 ;;; The signal name table is very boring to type. To make life less
110 ;;; awful, put the signal names in this list and evaluate the code to
111 ;;; get Emacs to regenerate it. We use @bsearch@ on it, so it's
112 ;;; important that it be sorted: Emacs does this for us.
e825e5a9 113
e33fa755
MW
114 (let ((signals '(HUP INT QUIT ILL ABRT FPE KILL SEGV PIPE ALRM TERM
115 USR1 USR2 CHLD CONT STOP TSTP TTIN TTOU BUS POLL
116 PROF SYS TRAP URG VTALRM XCPU XFSZ IOT EMT STKFLT
117 IO CLD PWR INFO LOST WINCH)))
118 (save-excursion
119 (goto-char (point-min))
120 (search-forward (concat "***" "BEGIN siglist" "***"))
121 (beginning-of-line 2)
122 (delete-region (point)
123 (progn
124 (search-forward "***END***")
125 (beginning-of-line)
126 (point)))
127 (dolist (sig (sort (copy-list signals) #'string<))
128 (insert (format "#ifdef SIG%s\n { \"%s\", SIG%s },\n#endif\n"
129 sig sig sig)))))
130 */
131
132 /***BEGIN siglist***/
e825e5a9
MW
133#ifdef SIGABRT
134 { "ABRT", SIGABRT },
135#endif
136#ifdef SIGALRM
137 { "ALRM", SIGALRM },
138#endif
139#ifdef SIGBUS
140 { "BUS", SIGBUS },
141#endif
142#ifdef SIGCHLD
143 { "CHLD", SIGCHLD },
144#endif
145#ifdef SIGCLD
146 { "CLD", SIGCLD },
147#endif
148#ifdef SIGCONT
149 { "CONT", SIGCONT },
150#endif
151#ifdef SIGEMT
152 { "EMT", SIGEMT },
153#endif
154#ifdef SIGFPE
155 { "FPE", SIGFPE },
156#endif
157#ifdef SIGHUP
158 { "HUP", SIGHUP },
159#endif
160#ifdef SIGILL
161 { "ILL", SIGILL },
162#endif
163#ifdef SIGINFO
164 { "INFO", SIGINFO },
165#endif
166#ifdef SIGINT
167 { "INT", SIGINT },
168#endif
169#ifdef SIGIO
170 { "IO", SIGIO },
171#endif
172#ifdef SIGIOT
173 { "IOT", SIGIOT },
174#endif
175#ifdef SIGKILL
176 { "KILL", SIGKILL },
177#endif
178#ifdef SIGLOST
179 { "LOST", SIGLOST },
180#endif
181#ifdef SIGPIPE
182 { "PIPE", SIGPIPE },
183#endif
184#ifdef SIGPOLL
185 { "POLL", SIGPOLL },
186#endif
187#ifdef SIGPROF
188 { "PROF", SIGPROF },
189#endif
190#ifdef SIGPWR
191 { "PWR", SIGPWR },
192#endif
193#ifdef SIGQUIT
194 { "QUIT", SIGQUIT },
195#endif
196#ifdef SIGSEGV
197 { "SEGV", SIGSEGV },
198#endif
199#ifdef SIGSTKFLT
200 { "STKFLT", SIGSTKFLT },
201#endif
202#ifdef SIGSTOP
203 { "STOP", SIGSTOP },
204#endif
205#ifdef SIGSYS
206 { "SYS", SIGSYS },
207#endif
208#ifdef SIGTERM
209 { "TERM", SIGTERM },
210#endif
211#ifdef SIGTRAP
212 { "TRAP", SIGTRAP },
213#endif
214#ifdef SIGTSTP
215 { "TSTP", SIGTSTP },
216#endif
217#ifdef SIGTTIN
218 { "TTIN", SIGTTIN },
219#endif
220#ifdef SIGTTOU
221 { "TTOU", SIGTTOU },
222#endif
223#ifdef SIGURG
224 { "URG", SIGURG },
225#endif
226#ifdef SIGUSR1
227 { "USR1", SIGUSR1 },
228#endif
229#ifdef SIGUSR2
230 { "USR2", SIGUSR2 },
231#endif
232#ifdef SIGVTALRM
233 { "VTALRM", SIGVTALRM },
234#endif
235#ifdef SIGWINCH
236 { "WINCH", SIGWINCH },
237#endif
238#ifdef SIGXCPU
239 { "XCPU", SIGXCPU },
240#endif
241#ifdef SIGXFSZ
242 { "XFSZ", SIGXFSZ },
243#endif
e33fa755 244 /***END***/
e825e5a9
MW
245 };
246
247 const struct namesig *ns = bsearch(p, tab, N(tab), sizeof(tab[0]),
248 cmp_namesig);
249 if (ns) return (ns->sig);
250 else if (isdigit((unsigned char)*p)) return (atoi(p));
251 else return (-1);
252}
253
753f86de
MW
254/* --- @strtotime@ --- *
255 *
256 * Arguments: @const char *p@ = pointer to string
257 * @struct timeval *tv@ = where to put the result
258 *
259 * Returns: ---
260 *
261 * Use: Converts a string representation of a duration into an
262 * internal version. Understands various time units.
263 */
264
265static void strtotime(const char *p, struct timeval *tv)
266{
267 char *q = (/*unconst*/ char *)p;
268 double t, i, f;
269
270 while (isspace((unsigned char)*q)) q++;
271 t = strtod(q, &q);
272 while (isspace((unsigned char)*q)) q++;
273 switch (*q) {
274 case 'd': case 'D': t *= 24;
275 case 'h': case 'H': t *= 60;
276 case 'm': case 'M': t *= 60;
277 case 's': case 'S':
278 q++;
279 while (isspace((unsigned char)*q)) q++;
280 }
281 if (*q) die(253, "bad time value `%s'", p);
282 f = modf(t, &i);
283 tv->tv_sec = i;
284 tv->tv_usec = f * 1000000;
285}
286
e825e5a9
MW
287/*----- Help functions ----------------------------------------------------*/
288
289static void usage(FILE *fp)
290 { pquis(fp, "Usage: $ [-s SIG] SECONDS COMMAND [ARGUMENTS ...]\n"); }
291
292static void version(FILE *fp)
293 { pquis(fp, "$ (version " VERSION ")\n"); }
294
295static void help(FILE *fp)
296{
297 version(fp); fputc('\n', stdout);
298 usage(fp);
299 pquis(fp, "\n\
300Run COMMAND, giving it the ARGUMENTS. If it fails to complete within the\n\
301specified number of SECONDS, kill it. Otherwise exit with the status it\n\
302returns.\n \
303\n\
304Options:\n\
305 -h, --help Show this help text.\n\
306 -v, --version Show version string.\n\
307 -u, --usage Show a terse usage summary.\n\
308\n\
309 -s, --signal=SIG Send signal SIG to the command.\n\
310");
311}
312
313/*----- Timeout handling --------------------------------------------------*/
314
315/* --- @timeout@ --- *
316 *
f42ff973
MW
317 * The timeout sequencing stuff is complicated, so here's a simple machine to
318 * make it work.
e825e5a9
MW
319 */
320
f42ff973
MW
321enum {
322 TA_MOAN, /* Report message to user */
323#define TAARG_MOAN s
324
325 TA_KILL, /* Send a signal */
326#define TAARG_KILL i
327
328 TA_GOTO, /* Goto different state */
329#define TAARG_GOTO i
330
331 TA_WAIT, /* Wait for more time */
332#define TAARG_WAIT tv
333
334 TA_STATE, /* Alter the internal state */
335#define TAARG_STATE i
336};
337
338struct tmact {
339 unsigned act;
340 union {
341 const char *s; /* String parameter */
342 int i; /* Integer parameter */
343 struct timeval tv; /* Time parameter */
344 } u;
345};
346
e825e5a9 347struct timeout {
f42ff973
MW
348 sel_timer t; /* Timer intrusion */
349 struct tmact *ta; /* Instruction vector */
350 int ip; /* Instruction pointer */
e825e5a9
MW
351 pid_t kid;
352};
353
354static void timeout(struct timeval *now, void *p)
355{
356 struct timeout *t = p;
f42ff973 357 struct tmact *ta;
e825e5a9
MW
358 struct timeval tv;
359
f42ff973
MW
360 for (;;) {
361 ta = &t->ta[t->ip++];
362 switch (ta->act) {
363 case TA_MOAN:
364 moan(ta->u.s);
365 break;
366 case TA_KILL:
367 kill(-t->kid, ta->u.i);
368 break;
369 case TA_GOTO:
370 t->ip = ta->u.i;
371 break;
372 case TA_STATE:
373 state = ta->u.i;
374 return;
375 case TA_WAIT:
376 TV_ADD(&tv, now, &ta->u.tv);
377 sel_addtimer(&sel, &t->t, &tv, timeout, t);
378 return;
379 default:
380 moan("unexpected tmact %u", ta->act);
381 abort();
382 }
e825e5a9 383 }
e825e5a9
MW
384}
385
386/*----- Signal handling ---------------------------------------------------*/
387
388/* --- @sigchld@ --- *
389 *
390 * If it's our child that died, fetch its exit status and stop.
391 */
392
393struct sigchld {
394 pid_t kid;
395 int rc;
396};
397
398static void sigchld(int sig, void *p)
399{
400 struct sigchld *s = p;
401 int rc;
402 pid_t pid;
403
404 if ((pid = waitpid(s->kid, &rc, WNOHANG)) < 0)
405 die(254, "waitpid failed: %s", strerror(errno));
406 if (pid == s->kid) {
407 s->rc = rc;
408 state = ST_DONE;
409 }
410}
411
412/* --- @sigpropagate@ --- *
413 *
414 * Propagate various signals to the child process group and then maybe act on
415 * them.
416 */
417
418static void sigpropagate(int sig, void *p)
419{
420 struct sigchld *s = p;
421
422 kill(-s->kid, sig);
423 switch (sig) {
424 case SIGTSTP: raise(SIGSTOP); break;
425 }
426}
427
428#define PROPAGATE_SIGNALS(_) \
429 _(TSTP) _(CONT) _(INT) _(HUP) _(QUIT)
430
431/*----- Main program ------------------------------------------------------*/
432
433/* --- @main@ --- */
434
435int main(int argc, char *const argv[])
436{
e825e5a9 437 pid_t kid;
e825e5a9 438 struct timeout to;
f42ff973
MW
439 struct timeval now;
440
441#define PAIR(x, y) { x, y }
442#define TACODE(I) \
443 I(sigwait, WAIT, PAIR(0, 0)) \
444 I(_a, MOAN, "timed out: killing child process") \
445 I(sig, KILL, SIGTERM) \
446 I(killwait, WAIT, PAIR(5, 0)) \
447 I(_b, MOAN, "child hasn't responded: killing harder") \
448 I(_c, KILL, SIGKILL) \
449 I(boredwait, WAIT, PAIR(5, 0)) \
450 I(_d, MOAN, "child still undead: giving up") \
451 I(_e, STATE, ST_ABORT)
452
453 enum {
454#define TALBL(label, op, arg) taoff_##label,
455 TACODE(TALBL)
456#undef TALBL
457 taoff_end
458 };
459
460 static struct tmact ta[] = {
461#define TAASM(label, op, arg) { TA_##op, { .TAARG_##op = arg } },
462 TACODE(TAASM)
463#undef TAASM
464 };
465
e825e5a9
MW
466 struct sigchld sc;
467 sig sig_CHLD;
468#define DEFSIG(tag) sig sig_##tag;
469 PROPAGATE_SIGNALS(DEFSIG)
470#undef DEFSIG
471 unsigned f = 0;
472#define F_BOGUS 1u
473
474 /* --- Option parsing --- */
475
476 ego(argv[0]);
477
478 for (;;) {
479 static const struct option opts[] = {
480 { "help", 0, 0, 'h' },
481 { "version", 0, 0, 'v' },
482 { "usage", 0, 0, 'u' },
0db9a123
MW
483 { "no-kill", 0, 0, 'K' },
484 { "kill-after", OPTF_ARGREQ, 0, 'k' },
485 { "bored-after", OPTF_ARGREQ, 0, 'b' },
e825e5a9
MW
486 { "signal", OPTF_ARGREQ, 0, 's' },
487 { 0, 0, 0, 0 }
488 };
489
0db9a123 490 int i = mdwopt(argc, argv, "+hvuKb:k:s:", opts, 0, 0, 0);
e825e5a9
MW
491 if (i < 0) break;
492 switch (i) {
493 case 'h': help(stdout); exit(0);
494 case 'v': version(stdout); exit(0);
495 case 'u': usage(stdout); exit(0);
0db9a123
MW
496 case 'b':
497 ta[taoff_boredwait].act = TA_WAIT;
498 strtotime(optarg, &ta[taoff_boredwait].u.tv);
499 break;
500 case 'k':
501 ta[taoff_killwait].act = TA_WAIT;
502 strtotime(optarg, &ta[taoff_killwait].u.tv);
503 break;
504 case 'K':
505 ta[taoff_killwait].act = TA_GOTO;
506 ta[taoff_killwait].u.i = taoff_boredwait;
507 break;
e825e5a9 508 case 's':
f42ff973 509 if ((ta[taoff_sig].u.i = namesig(optarg)) < 0)
e825e5a9
MW
510 die(253, "bad signal spec `%s'", optarg);
511 break;
512 default: f |= F_BOGUS; break;
513 }
514 }
515 argc -= optind; argv += optind;
516 if ((f & F_BOGUS) || argc < 2) { usage(stderr); exit(253); }
f42ff973 517 strtotime(argv[0], &ta[taoff_sigwait].u.tv);
e825e5a9
MW
518
519 /* --- Get things set up --- */
520
521 state = ST_WAIT;
522 sel_init(&sel);
523
524 /* --- Set up signal handling --- *
525 *
526 * Doing anything with asynchronous signals is a mug's game, so we bring
527 * them in-band. Do this before starting the child process, because
528 * otherwise we might miss an immediate @SIGCHLD@.
529 */
530
531 sig_init(&sel);
532 sc.rc = 0;
533 sig_add(&sig_CHLD, SIGCHLD, sigchld, &sc);
534#define ADDSIG(tag) sig_add(&sig_##tag, SIG##tag, sigpropagate, &sc);
535 PROPAGATE_SIGNALS(ADDSIG)
536#undef ADDSIG
537
538 /* --- Now start the child process --- */
539
540 if ((kid = fork()) < 0) die(2, "fork failed: %s", strerror(errno));
541 if (!kid) {
542 setpgid(0, 0);
543 execvp(argv[1], argv + 1);
3ece3fc9 544 die(252, "exec(%s) failed: %s", argv[1], strerror(errno));
e825e5a9
MW
545 }
546 sc.kid = kid;
547
548 /* --- Set up the timer --- */
549
753f86de 550 gettimeofday(&now, 0);
f42ff973
MW
551 to.kid = kid;
552 to.ta = ta;
553 to.ip = 0;
554 timeout(&now, &to);
e825e5a9
MW
555
556 /* --- Main @select@ loop */
557
558 while (state == ST_WAIT) {
559 if (sel_select(&sel)) {
560 if (errno == EINTR) continue;
561 die(254, "select failed: %s", strerror(errno));
562 }
563 }
564
565 /* --- Check and translate the exit code --- */
566
567 switch (state) {
568 case ST_ABORT:
569 exit(251);
570 case ST_DONE:
571 if (WIFEXITED(sc.rc))
572 exit(WEXITSTATUS(sc.rc));
573 else if (WIFSIGNALED(sc.rc))
574 exit(128 | WTERMSIG(sc.rc));
575 else
576 exit(128);
577 default:
578 moan("FATAL: unexpected state %d", state);
579 abort();
580 }
581}
582
583/*----- That's all, folks -------------------------------------------------*/