diff options
Diffstat (limited to 'timeout.c')
| -rw-r--r-- | timeout.c | 1016 |
1 files changed, 1016 insertions, 0 deletions
diff --git a/timeout.c b/timeout.c new file mode 100644 index 0000000000..bcb56f978c --- /dev/null +++ b/timeout.c @@ -0,0 +1,1016 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2014 Baptiste Daroussin <bapt@FreeBSD.org> + * Copyright (c) 2014 Vsevolod Stakhov <vsevolod@FreeBSD.org> + * Copyright (c) 2025 Aaron LI <aly@aaronly.me> + * Copyright (c) 2026 Project Tick + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer + * in this position and unchanged. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#define _POSIX_C_SOURCE 200809L + +#include <ctype.h> +#include <errno.h> +#include <getopt.h> +#include <inttypes.h> +#include <limits.h> +#include <math.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/prctl.h> +#include <sys/resource.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> + +#define EXIT_TIMEOUT 124 +#define EXIT_INVALID 125 +#define EXIT_CMD_ERROR 126 +#define EXIT_CMD_NOENT 127 + +#ifndef NSIG +#define NSIG 128 +#endif + +struct signal_entry { + const char *name; + int number; +}; + +#define SIGNAL_ENTRY(name) { #name, SIG##name } +#define SIGNAL_ALIAS(name, signal) { name, signal } + +static const struct signal_entry canonical_signals[] = { +#ifdef SIGHUP + SIGNAL_ENTRY(HUP), +#endif +#ifdef SIGINT + SIGNAL_ENTRY(INT), +#endif +#ifdef SIGQUIT + SIGNAL_ENTRY(QUIT), +#endif +#ifdef SIGILL + SIGNAL_ENTRY(ILL), +#endif +#ifdef SIGTRAP + SIGNAL_ENTRY(TRAP), +#endif +#ifdef SIGABRT + SIGNAL_ENTRY(ABRT), +#endif +#ifdef SIGBUS + SIGNAL_ENTRY(BUS), +#endif +#ifdef SIGFPE + SIGNAL_ENTRY(FPE), +#endif +#ifdef SIGKILL + SIGNAL_ENTRY(KILL), +#endif +#ifdef SIGUSR1 + SIGNAL_ENTRY(USR1), +#endif +#ifdef SIGSEGV + SIGNAL_ENTRY(SEGV), +#endif +#ifdef SIGUSR2 + SIGNAL_ENTRY(USR2), +#endif +#ifdef SIGPIPE + SIGNAL_ENTRY(PIPE), +#endif +#ifdef SIGALRM + SIGNAL_ENTRY(ALRM), +#endif +#ifdef SIGTERM + SIGNAL_ENTRY(TERM), +#endif +#ifdef SIGSTKFLT + SIGNAL_ENTRY(STKFLT), +#endif +#ifdef SIGCHLD + SIGNAL_ENTRY(CHLD), +#endif +#ifdef SIGCONT + SIGNAL_ENTRY(CONT), +#endif +#ifdef SIGSTOP + SIGNAL_ENTRY(STOP), +#endif +#ifdef SIGTSTP + SIGNAL_ENTRY(TSTP), +#endif +#ifdef SIGTTIN + SIGNAL_ENTRY(TTIN), +#endif +#ifdef SIGTTOU + SIGNAL_ENTRY(TTOU), +#endif +#ifdef SIGURG + SIGNAL_ENTRY(URG), +#endif +#ifdef SIGXCPU + SIGNAL_ENTRY(XCPU), +#endif +#ifdef SIGXFSZ + SIGNAL_ENTRY(XFSZ), +#endif +#ifdef SIGVTALRM + SIGNAL_ENTRY(VTALRM), +#endif +#ifdef SIGPROF + SIGNAL_ENTRY(PROF), +#endif +#ifdef SIGWINCH + SIGNAL_ENTRY(WINCH), +#endif +#ifdef SIGIO + SIGNAL_ENTRY(IO), +#endif +#ifdef SIGPWR + SIGNAL_ENTRY(PWR), +#endif +#ifdef SIGSYS + SIGNAL_ENTRY(SYS), +#endif +}; + +static const struct signal_entry signal_aliases[] = { +#ifdef SIGIOT + SIGNAL_ALIAS("IOT", SIGIOT), +#endif +#ifdef SIGCLD + SIGNAL_ALIAS("CLD", SIGCLD), +#endif +#ifdef SIGPOLL + SIGNAL_ALIAS("POLL", SIGPOLL), +#endif +#ifdef SIGUNUSED + SIGNAL_ALIAS("UNUSED", SIGUNUSED), +#endif +}; + +struct options { + bool foreground; + bool preserve; + bool verbose; + bool kill_after_set; + int timeout_signal; + long double duration; + long double kill_after; + const char *command_name; + char **command_argv; +}; + +struct child_state { + bool reaped; + int status; +}; + +enum deadline_kind { + DEADLINE_NONE = 0, + DEADLINE_FIRST, + DEADLINE_SECOND, +}; + +struct runtime_state { + bool timed_out; + bool first_timer_active; + bool second_timer_active; + bool second_pending; + long double first_deadline; + long double second_deadline; + int active_signal; +}; + +static const char *program_name = "timeout"; + +static void usage(void) __attribute__((noreturn)); +static void die(int status, const char *fmt, ...) + __attribute__((noreturn, format(printf, 2, 3))); +static void die_errno(int status, const char *context) __attribute__((noreturn)); +static void vlog(const struct options *opt, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +static const char *basename_const(const char *path); +static bool parse_nonnegative_int(const char *text, int *value); +static char *normalize_signal_name(const char *token); +static bool signal_name_for_number(int signum, char *buffer, size_t buffer_size); +static bool parse_signal_name(const char *token, int *signum); +static bool parse_signal_token(const char *token, int *signum); +static long double parse_duration_or_die(const char *text); +static long double monotonic_seconds(void); +static long double max_time_t_seconds(void); +static struct timespec seconds_to_timeout(long double seconds); +static enum deadline_kind next_deadline(const struct runtime_state *state, + struct timespec *timeout); +static void enable_subreaper_or_die(void); +static bool is_terminating_signal(int signo); +static void send_signal_to_command(pid_t pid, int signo, const struct options *opt); +static void arm_second_timer(struct runtime_state *state, const struct options *opt); +static void reap_children(pid_t command_pid, struct child_state *child, + bool *have_children, const struct options *opt); +static void child_exec(const struct options *opt, const sigset_t *oldmask) + __attribute__((noreturn)); +static void kill_self(int signo) __attribute__((noreturn)); +static bool process_group_exists(pid_t pgid); + +static void +usage(void) +{ + fprintf(stderr, + "Usage: %s [-f | --foreground] [-k time | --kill-after time]" + " [-p | --preserve-status] [-s signal | --signal signal] " + "[-v | --verbose] <duration> <command> [arg ...]\n", + program_name); + exit(EXIT_INVALID); +} + +static void +die(int status, const char *fmt, ...) +{ + va_list ap; + + fprintf(stderr, "%s: ", program_name); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + exit(status); +} + +static void +die_errno(int status, const char *context) +{ + die(status, "%s: %s", context, strerror(errno)); +} + +static void +vlog(const struct options *opt, const char *fmt, ...) +{ + va_list ap; + + if (!opt->verbose) + return; + + fprintf(stderr, "%s: ", program_name); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} + +static const char * +basename_const(const char *path) +{ + const char *base; + + base = strrchr(path, '/'); + if (base == NULL || base[1] == '\0') + return (path); + return (base + 1); +} + +static bool +parse_nonnegative_int(const char *text, int *value) +{ + intmax_t parsed; + char *end; + + if (text[0] == '\0') + return (false); + + errno = 0; + parsed = strtoimax(text, &end, 10); + if (errno == ERANGE || *end != '\0' || parsed < 0 || parsed > INT_MAX) + return (false); + *value = (int)parsed; + return (true); +} + +static char * +normalize_signal_name(const char *token) +{ + size_t i, length, offset; + char *name; + + length = strlen(token); + name = malloc(length + 1); + if (name == NULL) + die(EXIT_INVALID, "out of memory"); + + for (i = 0; i < length; i++) + name[i] = (char)toupper((unsigned char)token[i]); + name[length] = '\0'; + + offset = 0; + if (length > 3 && name[0] == 'S' && name[1] == 'I' && name[2] == 'G') + offset = 3; + if (offset != 0) + memmove(name, name + offset, length - offset + 1); + return (name); +} + +static bool +signal_name_for_number(int signum, char *buffer, size_t buffer_size) +{ + size_t i; + + if (signum == 0) { + if (buffer != NULL && buffer_size > 0) { + buffer[0] = '0'; + if (buffer_size > 1) + buffer[1] = '\0'; + } + return (true); + } + + for (i = 0; i < sizeof(canonical_signals) / sizeof(canonical_signals[0]); i++) { + if (canonical_signals[i].number != signum) + continue; + if (buffer != NULL && buffer_size > 0) { + if (snprintf(buffer, buffer_size, "%s", + canonical_signals[i].name) >= (int)buffer_size) + die(EXIT_INVALID, "internal signal name overflow"); + } + return (true); + } + +#ifdef SIGRTMIN +#ifdef SIGRTMAX + if (signum == SIGRTMIN) { + if (buffer != NULL && buffer_size > 0) + snprintf(buffer, buffer_size, "RTMIN"); + return (true); + } + if (signum == SIGRTMAX) { + if (buffer != NULL && buffer_size > 0) + snprintf(buffer, buffer_size, "RTMAX"); + return (true); + } + if (signum > SIGRTMIN && signum < SIGRTMAX) { + if (buffer != NULL && buffer_size > 0) { + if (snprintf(buffer, buffer_size, "RTMIN+%d", + signum - SIGRTMIN) >= (int)buffer_size) + die(EXIT_INVALID, "internal signal name overflow"); + } + return (true); + } +#endif +#endif + + return (false); +} + +static bool +parse_signal_name(const char *token, int *signum) +{ + char *name, *end; + int offset; + size_t i; + + if (strcmp(token, "0") == 0) { + *signum = 0; + return (true); + } + + name = normalize_signal_name(token); + + for (i = 0; i < sizeof(canonical_signals) / sizeof(canonical_signals[0]); i++) { + if (strcmp(name, canonical_signals[i].name) == 0) { + *signum = canonical_signals[i].number; + free(name); + return (true); + } + } + for (i = 0; i < sizeof(signal_aliases) / sizeof(signal_aliases[0]); i++) { + if (strcmp(name, signal_aliases[i].name) == 0) { + *signum = signal_aliases[i].number; + free(name); + return (true); + } + } + +#ifdef SIGRTMIN +#ifdef SIGRTMAX + if (strcmp(name, "RTMIN") == 0) { + *signum = SIGRTMIN; + free(name); + return (true); + } + if (strncmp(name, "RTMIN+", 6) == 0) { + errno = 0; + offset = (int)strtol(name + 6, &end, 10); + if (errno == 0 && *end == '\0' && offset >= 0 && + SIGRTMIN + offset <= SIGRTMAX) { + *signum = SIGRTMIN + offset; + free(name); + return (true); + } + } + if (strcmp(name, "RTMAX") == 0) { + *signum = SIGRTMAX; + free(name); + return (true); + } + if (strncmp(name, "RTMAX-", 6) == 0) { + errno = 0; + offset = (int)strtol(name + 6, &end, 10); + if (errno == 0 && *end == '\0' && offset >= 0 && + SIGRTMAX - offset >= SIGRTMIN) { + *signum = SIGRTMAX - offset; + free(name); + return (true); + } + } +#endif +#endif + + free(name); + return (false); +} + +static bool +parse_signal_token(const char *token, int *signum) +{ + if (parse_nonnegative_int(token, signum)) { + if (*signum >= NSIG) + return (false); + return (true); + } + return (parse_signal_name(token, signum)); +} + +static long double +parse_duration_or_die(const char *text) +{ + long double value; + char *end; + + if (text[0] == '\0' || isspace((unsigned char)text[0])) + die(EXIT_INVALID, "duration is not a number"); + + errno = 0; + value = strtold(text, &end); + if (end == text) + die(EXIT_INVALID, "duration is not a number"); + if (errno == ERANGE || !isfinite(value)) + die(EXIT_INVALID, "duration out of range"); + if (*end != '\0') { + if (end[1] != '\0') + die(EXIT_INVALID, "duration unit suffix too long"); + switch (*end) { + case 's': + break; + case 'm': + value *= 60.0L; + break; + case 'h': + value *= 3600.0L; + break; + case 'd': + value *= 86400.0L; + break; + default: + die(EXIT_INVALID, "duration unit suffix invalid"); + } + } + if (!isfinite(value) || value < 0.0L) + die(EXIT_INVALID, "duration out of range"); + return (value); +} + +static long double +monotonic_seconds(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + die_errno(EXIT_INVALID, "clock_gettime(CLOCK_MONOTONIC)"); + + return ((long double)ts.tv_sec + + (long double)ts.tv_nsec / 1000000000.0L); +} + +static long double +max_time_t_seconds(void) +{ + int bits; + + bits = (int)(sizeof(time_t) * CHAR_BIT); + if ((time_t)-1 < (time_t)0) + return (ldexpl(1.0L, bits - 1) - 1.0L); + return (ldexpl(1.0L, bits) - 1.0L); +} + +static struct timespec +seconds_to_timeout(long double seconds) +{ + struct timespec ts; + long double whole, fractional, max_seconds; + + if (seconds <= 0.0L) { + ts.tv_sec = 0; + ts.tv_nsec = 0; + return (ts); + } + + max_seconds = max_time_t_seconds(); + if (seconds > max_seconds) + seconds = max_seconds; + + fractional = modfl(seconds, &whole); + ts.tv_sec = (time_t)whole; + ts.tv_nsec = (long)(fractional * 1000000000.0L); + if (ts.tv_nsec < 0) + ts.tv_nsec = 0; + if (ts.tv_nsec >= 1000000000L) + ts.tv_nsec = 999999999L; + return (ts); +} + +static enum deadline_kind +next_deadline(const struct runtime_state *state, struct timespec *timeout) +{ + long double now, best, remaining; + enum deadline_kind kind; + + now = monotonic_seconds(); + best = 0.0L; + kind = DEADLINE_NONE; + + if (state->first_timer_active) { + remaining = state->first_deadline - now; + if (remaining < 0.0L) + remaining = 0.0L; + best = remaining; + kind = DEADLINE_FIRST; + } + if (state->second_timer_active) { + remaining = state->second_deadline - now; + if (remaining < 0.0L) + remaining = 0.0L; + if (kind == DEADLINE_NONE || remaining < best) { + best = remaining; + kind = DEADLINE_SECOND; + } + } + + if (kind != DEADLINE_NONE) + *timeout = seconds_to_timeout(best); + return (kind); +} + +static void +enable_subreaper_or_die(void) +{ +#ifdef PR_SET_CHILD_SUBREAPER + if (prctl(PR_SET_CHILD_SUBREAPER, 1L, 0L, 0L, 0L) != 0) + die_errno(EXIT_INVALID, "prctl(PR_SET_CHILD_SUBREAPER)"); +#else + die(EXIT_INVALID, + "Linux child subreaper API is unavailable; use -f/--foreground"); +#endif +} + +static bool +is_terminating_signal(int signo) +{ + switch (signo) { +#ifdef SIGHUP + case SIGHUP: +#endif +#ifdef SIGINT + case SIGINT: +#endif +#ifdef SIGQUIT + case SIGQUIT: +#endif +#ifdef SIGILL + case SIGILL: +#endif +#ifdef SIGTRAP + case SIGTRAP: +#endif +#ifdef SIGABRT + case SIGABRT: +#endif +#ifdef SIGBUS + case SIGBUS: +#endif +#ifdef SIGFPE + case SIGFPE: +#endif +#ifdef SIGSEGV + case SIGSEGV: +#endif +#ifdef SIGPIPE + case SIGPIPE: +#endif +#ifdef SIGTERM + case SIGTERM: +#endif +#ifdef SIGXCPU + case SIGXCPU: +#endif +#ifdef SIGXFSZ + case SIGXFSZ: +#endif +#ifdef SIGVTALRM + case SIGVTALRM: +#endif +#ifdef SIGPROF + case SIGPROF: +#endif +#ifdef SIGUSR1 + case SIGUSR1: +#endif +#ifdef SIGUSR2 + case SIGUSR2: +#endif +#ifdef SIGSYS + case SIGSYS: +#endif + return (true); + default: + return (false); + } +} + +static void +send_signal_to_command(pid_t pid, int signo, const struct options *opt) +{ + char sigbuf[32]; + const char *signame; + pid_t target; + + if (!signal_name_for_number(signo, sigbuf, sizeof(sigbuf))) + snprintf(sigbuf, sizeof(sigbuf), "%d", signo); + signame = sigbuf; + + if (opt->foreground) { + target = pid; + vlog(opt, "sending signal %s(%d) to command '%s'", + signame, signo, opt->command_name); + } else { + target = -pid; + vlog(opt, "sending signal %s(%d) to command group '%s'", + signame, signo, opt->command_name); + } + + if (kill(target, signo) != 0 && errno != ESRCH) { + fprintf(stderr, "%s: kill(%ld, %s): %s\n", program_name, + (long)target, signame, strerror(errno)); + } + + if (signo == SIGKILL || signo == SIGSTOP || signo == SIGCONT) + return; + + if (!signal_name_for_number(SIGCONT, sigbuf, sizeof(sigbuf))) + snprintf(sigbuf, sizeof(sigbuf), "%d", SIGCONT); + vlog(opt, "sending signal %s(%d) to command '%s'", + sigbuf, SIGCONT, opt->command_name); + (void)kill(target, SIGCONT); +} + +static void +arm_second_timer(struct runtime_state *state, const struct options *opt) +{ + long double now; + + if (!state->second_pending) + return; + + now = monotonic_seconds(); + state->second_deadline = now + opt->kill_after; + state->second_timer_active = true; + state->second_pending = false; + state->active_signal = SIGKILL; + state->first_timer_active = false; +} + +static void +reap_children(pid_t command_pid, struct child_state *child, bool *have_children, + const struct options *opt) +{ + pid_t waited; + int status; + + *have_children = false; + for (;;) { + waited = waitpid(-1, &status, WNOHANG); + if (waited > 0) { + if (waited == command_pid) { + child->reaped = true; + child->status = status; + if (WIFEXITED(status)) { + vlog(opt, "child terminated: pid=%d exit=%d", + (int)waited, WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + vlog(opt, "child terminated: pid=%d sig=%d", + (int)waited, WTERMSIG(status)); + } else { + vlog(opt, "child changed state: pid=%d status=0x%x", + (int)waited, status); + } + } else { + vlog(opt, "collected descendant: pid=%d status=0x%x", + (int)waited, status); + } + continue; + } + if (waited == 0) { + *have_children = true; + return; + } + if (errno == EINTR) + continue; + if (errno == ECHILD) + return; + die_errno(EXIT_INVALID, "waitpid"); + } +} + +static void +child_exec(const struct options *opt, const sigset_t *oldmask) +{ + struct sigaction sa; + int saved_errno; + + if (!opt->foreground) { + if (setpgid(0, 0) != 0) { + dprintf(STDERR_FILENO, "%s: setpgid: %s\n", + program_name, strerror(errno)); + _exit(EXIT_INVALID); + } + } + + if (opt->timeout_signal != SIGKILL && opt->timeout_signal != SIGSTOP) { + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + if (sigaction(opt->timeout_signal, &sa, NULL) != 0) { + dprintf(STDERR_FILENO, "%s: sigaction(%d): %s\n", + program_name, opt->timeout_signal, strerror(errno)); + _exit(EXIT_INVALID); + } + } + + if (sigprocmask(SIG_SETMASK, oldmask, NULL) != 0) { + dprintf(STDERR_FILENO, "%s: sigprocmask: %s\n", + program_name, strerror(errno)); + _exit(EXIT_INVALID); + } + + execvp(opt->command_argv[0], opt->command_argv); + saved_errno = errno; + dprintf(STDERR_FILENO, "%s: exec(%s): %s\n", program_name, + opt->command_argv[0], strerror(saved_errno)); + _exit(saved_errno == ENOENT ? EXIT_CMD_NOENT : EXIT_CMD_ERROR); +} + +static void +kill_self(int signo) +{ + struct sigaction sa; + struct rlimit rl; + sigset_t mask; + + memset(&rl, 0, sizeof(rl)); + (void)setrlimit(RLIMIT_CORE, &rl); + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + (void)sigaction(signo, &sa, NULL); + + sigfillset(&mask); + sigdelset(&mask, signo); + (void)sigprocmask(SIG_SETMASK, &mask, NULL); + + raise(signo); + _exit(128 + signo); +} + +static bool +process_group_exists(pid_t pgid) +{ + if (pgid <= 0) + return (false); + if (kill(-pgid, 0) == 0) + return (true); + if (errno == EPERM) + return (true); + if (errno == ESRCH) + return (false); + return (true); +} + +int +main(int argc, char **argv) +{ + const char optstr[] = "+fk:ps:v"; + const struct option longopts[] = { + { "foreground", no_argument, NULL, 'f' }, + { "kill-after", required_argument, NULL, 'k' }, + { "preserve-status", no_argument, NULL, 'p' }, + { "signal", required_argument, NULL, 's' }, + { "verbose", no_argument, NULL, 'v' }, + { NULL, 0, NULL, 0 }, + }; + struct options opt; + struct child_state child; + struct runtime_state state; + struct timespec timeout; + sigset_t allmask, oldmask; + pid_t child_pid; + int ch; + + if (argv[0] != NULL && argv[0][0] != '\0') + program_name = basename_const(argv[0]); + + memset(&opt, 0, sizeof(opt)); + opt.timeout_signal = SIGTERM; + + opterr = 0; + while ((ch = getopt_long(argc, argv, optstr, longopts, NULL)) != -1) { + switch (ch) { + case 'f': + opt.foreground = true; + break; + case 'k': + opt.kill_after = parse_duration_or_die(optarg); + opt.kill_after_set = true; + break; + case 'p': + opt.preserve = true; + break; + case 's': + if (!parse_signal_token(optarg, &opt.timeout_signal)) + die(EXIT_INVALID, "invalid signal: %s", optarg); + break; + case 'v': + opt.verbose = true; + break; + default: + usage(); + } + } + + argc -= optind; + argv += optind; + if (argc < 2) + usage(); + + opt.duration = parse_duration_or_die(argv[0]); + opt.command_argv = &argv[1]; + opt.command_name = argv[1]; + + if (!opt.foreground) + enable_subreaper_or_die(); + + sigfillset(&allmask); + sigdelset(&allmask, SIGKILL); + sigdelset(&allmask, SIGSTOP); + if (sigprocmask(SIG_SETMASK, &allmask, &oldmask) != 0) + die_errno(EXIT_INVALID, "sigprocmask"); + + child_pid = fork(); + if (child_pid < 0) + die_errno(EXIT_INVALID, "fork"); + if (child_pid == 0) + child_exec(&opt, &oldmask); + + if (!opt.foreground) { + if (setpgid(child_pid, child_pid) != 0 && + errno != EACCES && errno != ESRCH) { + die_errno(EXIT_INVALID, "setpgid"); + } + } + + memset(&child, 0, sizeof(child)); + memset(&state, 0, sizeof(state)); + state.active_signal = opt.timeout_signal; + state.second_pending = opt.kill_after_set; + if (opt.duration > 0.0L) { + state.first_timer_active = true; + state.first_deadline = monotonic_seconds() + opt.duration; + } + + for (;;) { + enum deadline_kind deadline_kind; + bool have_children; + siginfo_t si; + int signo; + + reap_children(child_pid, &child, &have_children, &opt); + if (opt.foreground) { + if (child.reaped) + break; + } else { + if (state.timed_out && have_children && + !process_group_exists(child_pid)) { + die(EXIT_INVALID, + "descendant processes escaped command process group; " + "unsupported on Linux without --foreground"); + } + if (child.reaped && !have_children) + break; + } + + deadline_kind = next_deadline(&state, &timeout); + if (deadline_kind == DEADLINE_NONE) + signo = sigwaitinfo(&allmask, &si); + else + signo = sigtimedwait(&allmask, &si, &timeout); + + if (signo >= 0) { + char sigbuf[32]; + + if (signo == SIGCHLD) + continue; + + if (!signal_name_for_number(signo, sigbuf, sizeof(sigbuf))) + snprintf(sigbuf, sizeof(sigbuf), "%d", signo); + + if (signo == SIGALRM) { + vlog(&opt, "received SIGALRM, treating as timeout"); + state.timed_out = true; + state.first_timer_active = false; + send_signal_to_command(child_pid, state.active_signal, &opt); + arm_second_timer(&state, &opt); + continue; + } + + if (signo == state.active_signal || is_terminating_signal(signo)) { + vlog(&opt, "received terminating signal %s(%d)", + sigbuf, signo); + send_signal_to_command(child_pid, signo, &opt); + arm_second_timer(&state, &opt); + } else { + vlog(&opt, "received signal %s(%d)", sigbuf, signo); + send_signal_to_command(child_pid, signo, &opt); + } + continue; + } + + if (errno == EINTR) + continue; + if (errno != EAGAIN) + die_errno(EXIT_INVALID, "sigwait"); + + if (deadline_kind == DEADLINE_FIRST) { + state.timed_out = true; + state.first_timer_active = false; + vlog(&opt, "time limit reached"); + send_signal_to_command(child_pid, state.active_signal, &opt); + arm_second_timer(&state, &opt); + } else if (deadline_kind == DEADLINE_SECOND) { + state.second_timer_active = false; + vlog(&opt, "kill-after limit reached"); + send_signal_to_command(child_pid, SIGKILL, &opt); + } + } + + (void)sigprocmask(SIG_SETMASK, &oldmask, NULL); + + if (!child.reaped) + die(EXIT_INVALID, "failed to retrieve command status"); + + if (state.timed_out && !opt.preserve) + return (EXIT_TIMEOUT); + + if (WIFEXITED(child.status)) + return (WEXITSTATUS(child.status)); + if (WIFSIGNALED(child.status)) + kill_self(WTERMSIG(child.status)); + + return (EXIT_INVALID); +} |
