| 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(253, "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 | die(252, "exec(%s) failed: %s", argv[1], strerror(errno)); |
| 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 -------------------------------------------------*/ |