diff options
Diffstat (limited to 'bin/timeout')
| -rw-r--r-- | bin/timeout/Makefile | 10 | ||||
| -rw-r--r-- | bin/timeout/Makefile.depend | 16 | ||||
| -rw-r--r-- | bin/timeout/tests/Makefile | 3 | ||||
| -rw-r--r-- | bin/timeout/tests/Makefile.depend | 10 | ||||
| -rw-r--r-- | bin/timeout/tests/timeout_test.sh | 216 | ||||
| -rw-r--r-- | bin/timeout/timeout.1 | 292 | ||||
| -rw-r--r-- | bin/timeout/timeout.c | 511 |
7 files changed, 1058 insertions, 0 deletions
diff --git a/bin/timeout/Makefile b/bin/timeout/Makefile new file mode 100644 index 000000000000..0b06ca57c847 --- /dev/null +++ b/bin/timeout/Makefile @@ -0,0 +1,10 @@ +.include <src.opts.mk> + +PROG= timeout + +SYMLINKS= ../..${BINDIR}/timeout /usr/bin/timeout + +HAS_TESTS= +SUBDIR.${MK_TESTS}+= tests + +.include <bsd.prog.mk> diff --git a/bin/timeout/Makefile.depend b/bin/timeout/Makefile.depend new file mode 100644 index 000000000000..84b8ddd67e34 --- /dev/null +++ b/bin/timeout/Makefile.depend @@ -0,0 +1,16 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + gnu/lib/csu \ + include \ + include/xlocale \ + lib/${CSU_DIR} \ + lib/libc \ + lib/libcompiler_rt \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/bin/timeout/tests/Makefile b/bin/timeout/tests/Makefile new file mode 100644 index 000000000000..bf5dddd77baa --- /dev/null +++ b/bin/timeout/tests/Makefile @@ -0,0 +1,3 @@ +ATF_TESTS_SH= timeout_test + +.include <bsd.test.mk> diff --git a/bin/timeout/tests/Makefile.depend b/bin/timeout/tests/Makefile.depend new file mode 100644 index 000000000000..11aba52f82cf --- /dev/null +++ b/bin/timeout/tests/Makefile.depend @@ -0,0 +1,10 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/bin/timeout/tests/timeout_test.sh b/bin/timeout/tests/timeout_test.sh new file mode 100644 index 000000000000..88dbaa808043 --- /dev/null +++ b/bin/timeout/tests/timeout_test.sh @@ -0,0 +1,216 @@ +# SPDX-License-Identifier: BSD-2-Clause +# Copyright (c) 2014 Baptiste Daroussin <bapt@FreeBSD.org> + +atf_test_case nominal +nominal_head() +{ + atf_set "descr" "Basic tests on timeout(1) utility" +} + +nominal_body() +{ + atf_check \ + -o empty \ + -e empty \ + -s exit:0 \ + timeout 5 true +} + +atf_test_case time_unit +time_unit_head() +{ + atf_set "descr" "Test parsing the default time unit" +} + +time_unit_body() +{ + atf_check \ + -o empty \ + -e empty \ + -s exit:0 \ + timeout 1d true + + atf_check \ + -o empty \ + -e empty \ + -s exit:0 \ + timeout 1h true + + atf_check \ + -o empty \ + -e empty \ + -s exit:0 \ + timeout 1m true + + atf_check \ + -o empty \ + -e empty \ + -s exit:0 \ + timeout 1s true +} + +atf_test_case no_timeout +no_timeout_head() +{ + atf_set "descr" "Test disabled timeout" +} + +no_timeout_body() +{ + atf_check \ + -o empty \ + -e empty \ + -s exit:0 \ + timeout 0 true +} + +atf_test_case exit_numbers +exit_numbers_head() +{ + atf_set "descr" "Test exit numbers" +} + +exit_numbers_body() +{ + atf_check \ + -o empty \ + -e empty \ + -s exit:2 \ + -x timeout 5 sh -c \'exit 2\' + + atf_check \ + -o empty \ + -e empty \ + -s exit:124 \ + timeout .1 sleep 1 + + # With preserve status exit should be 128 + TERM aka 143 + atf_check \ + -o empty \ + -e empty \ + -s signal:15 \ + timeout --preserve-status .1 sleep 10 + + atf_check \ + -o empty \ + -e empty \ + -s exit:124 \ + timeout -s1 -k1 .1 sleep 10 + + atf_check \ + -o empty \ + -e empty \ + -s exit:0 \ + -x sh -c 'trap "" CHLD; exec timeout 10 true' +} + +atf_test_case with_a_child +with_a_child_head() +{ + atf_set "descr" "When starting with a child (coreutils bug#9098)" +} + +with_a_child_body() +{ + out=$(sleep .1 & exec timeout .5 sh -c 'sleep 2; echo foo') + status=$? + test "$out" = "" && test $status = 124 || atf_fail "wrong status $status" + +} + +atf_test_case invalid_timeout +invalid_timeout_head() +{ + atf_set "descr" "Invalid timeout" +} + +invalid_timeout_body() +{ + atf_check \ + -o empty \ + -e inline:"timeout: duration is not a number\n" \ + -s exit:125 \ + timeout invalid sleep 0 + + atf_check \ + -o empty \ + -e inline:"timeout: duration is not a number\n" \ + -s exit:125 \ + timeout --kill-after=invalid 1 sleep 0 + + atf_check \ + -o empty \ + -e inline:"timeout: duration unit suffix invalid\n" \ + -s exit:125 \ + timeout 42D sleep 0 + + atf_check \ + -o empty \ + -e inline:"timeout: duration out of range\n" \ + -s exit:125 \ + timeout 999999999999999999999999999999999999999999999999999999999999d sleep 0 + + atf_check \ + -o empty \ + -e inline:"timeout: duration out of range\n" \ + -s exit:125 \ + timeout 2.34e+5d sleep 0 +} + +atf_test_case invalid_signal +invalid_signal_head() +{ + atf_set "descr" "Invalid signal" +} + +invalid_signal_body() +{ + atf_check \ + -o empty \ + -e inline:"timeout: invalid signal\n" \ + -s exit:125 \ + timeout --signal=invalid 1 sleep 0 +} + +atf_test_case invalid_command +invalid_command_head() +{ + atf_set "descr" "Invalid command" +} + +invalid_command_body() +{ + atf_check \ + -o empty \ + -e inline:"timeout: exec(.): Permission denied\n" \ + -s exit:126 \ + timeout 10 . +} + +atf_test_case no_such_command +no_such_command_head() +{ + atf_set "descr" "No such command" +} + +no_such_command_body() +{ + atf_check \ + -o empty \ + -e inline:"timeout: exec(enoexists): No such file or directory\n" \ + -s exit:127 \ + timeout 10 enoexists +} + +atf_init_test_cases() +{ + atf_add_test_case nominal + atf_add_test_case time_unit + atf_add_test_case no_timeout + atf_add_test_case exit_numbers + atf_add_test_case with_a_child + atf_add_test_case invalid_timeout + atf_add_test_case invalid_signal + atf_add_test_case invalid_command + atf_add_test_case no_such_command +} diff --git a/bin/timeout/timeout.1 b/bin/timeout/timeout.1 new file mode 100644 index 000000000000..6486ccf99a36 --- /dev/null +++ b/bin/timeout/timeout.1 @@ -0,0 +1,292 @@ +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.\" Copyright (c) 2014 Baptiste Daroussin <bapt@FreeBSD.org> +.\" Copyright (c) 2025 Aaron LI <aly@aaronly.me> +.\" 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. +.\" 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 AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. +.\" +.Dd April 3, 2025 +.Dt TIMEOUT 1 +.Os +.Sh NAME +.Nm timeout +.Nd run a command with a time limit +.Sh SYNOPSIS +.Nm +.Op Fl f | Fl -foreground +.Op Fl k Ar time | Fl -kill-after Ar time +.Op Fl p | Fl -preserve-status +.Op Fl s Ar signal | Fl -signal Ar signal +.Op Fl v | Fl -verbose +.Ar duration +.Ar command +.Op Ar arg ... +.Sh DESCRIPTION +.Nm Timeout +starts the +.Ar command +with its +.Ar arg +list. +If the +.Ar command +is still running after +.Ar duration , +it is killed by sending the +.Ar signal , +or +.Dv SIGTERM +if the +.Fl s +option is unspecified. +The special +.Ar duration , +zero, signifies no limit. +Therefore, a signal is never sent if +.Ar duration +is 0. +.Pp +The signal dispositions inherited by the +.Ar command +are the same as the dispositions that +.Nm +inherited, except for the signal that will be sent upon timeout, +which is reset to take the default action and should terminate +the process. +.Pp +If +.Nm +receives the +.Dv SIGALRM +signal, it will behave as if the time limit has been reached +and send the specified signal to +.Ar command . +For any other signals delivered to +.Nm , +it will propagate them to +.Ar command , +with the exception of +.Dv SIGKILL +and +.Dv SIGSTOP . +If you want to prevent the +.Ar command +from being timed out, send +.Dv SIGKILL +to +.Nm . +.Pp +The options are as follows: +.Bl -tag -width indent +.It Fl f , Fl -foreground +Only time out the +.Ar command +itself, but do not propagate signals to its descendants. +See the +.Sx IMPLEMENTATION NOTES +section for more details. +.It Fl k Ar time , Fl -kill-after Ar time +Send a +.Dv SIGKILL +signal if +.Ar command +is still running after +.Ar time +since the first signal was sent. +.It Fl p , Fl -preserve-status +Always exit with the same status as +.Ar command , +even if the timeout was reached. +.It Fl s Ar signal , Fl -signal Ar signal +Specify the signal to send on timeout. +By default, +.Dv SIGTERM +is sent. +.It Fl v , Fl -verbose +Show information to +.Xr stderr 4 +about timeouts, signals to be sent, and the +.Ar command +exits. +.El +.Ss Duration Format +The +.Ar duration +and +.Ar time +are non-negative integer or real (decimal) numbers, with an optional +suffix specifying the unit. +Values without an explicit unit are interpreted as seconds. +.Pp +Supported unit suffixes are: +.Bl -tag -offset indent -width indent -compact +.It Cm s +seconds +.It Cm m +minutes +.It Cm h +hours +.It Cm d +days +.El +.Sh IMPLEMENTATION NOTES +If the +.Fl -foreground +option is not specified, +.Nm +runs as the reaper (see also +.Xr procctl 2 ) +of the +.Ar command +and its descendants, and will wait for all the descendants to terminate. +This behavior might cause surprises if there are descendants running +in the background, because they will ignore +.Dv SIGINT +and +.Dv SIGQUIT +signals. +For example, the following command that sends a +.Dv SIGTERM +signal will complete in 2 seconds: +.Dl $ timeout -s TERM 2 sh -c 'sleep 4 & sleep 5' +However, this command that sends a +.Dv SIGINT +signal will complete in 4 seconds: +.Dl $ timeout -s INT 2 sh -c 'sleep 4 & sleep 5' +.Sh EXIT STATUS +If the time limit was reached and the +.Fl -preserve-status +option is not specified, the exit status is 124. +Otherwise, +.Nm +exits with the same exit status as the +.Ar command . +For example, +.Nm +will terminate itself with the same signal if the +.Ar command +is terminated by a signal. +.Pp +If an error occurred, the following exit values are returned: +.Bl -tag -offset indent with indent -compact +.It 125 +An error other than the two described below occurred. +For example, an invalid duration or signal was specified. +.It 126 +The +.Ar command +was found but could not be executed. +.It 127 +The +.Ar command +could not be found. +.El +.Sh EXAMPLES +Run +.Xr sleep 1 +with a time limit of 4 seconds. +Since the command completes in 2 seconds, the exit status is 0: +.Bd -literal -offset indent +$ timeout 4 sleep 2 +$ echo $? +0 +.Ed +.Pp +Run +.Xr sleep 1 +for 4 seconds and terminate process after 2 seconds. +The exit status is 124 since +.Fl -preserve-status +is not used: +.Bd -literal -offset indent +$ timeout 2 sleep 4 +$ echo $? +124 +.Ed +.Pp +Same as above but preserving status. +The exit status is 128 + signal number (15 for +.Dv SIGTERM ) +for most shells: +.Bd -literal -offset indent +$ timeout --preserve-status 2 sleep 4 +$ echo $? +143 +.Ed +.Pp +Same as above but sending +.Dv SIGALRM +(signal number 14) instead of +.Dv SIGTERM : +.Bd -literal -offset indent +$ timeout --preserve-status -s SIGALRM 2 sleep 4 +$ echo $? +142 +.Ed +.Pp +Try to +.Xr fetch 1 +the PDF version of the +.Fx +Handbook. +Send a +.Dv SIGTERM +signal after 1 minute and send a +.Dv SIGKILL +signal 5 seconds later if the process refuses to stop: +.Bd -literal -offset indent +$ timeout -k 5s 1m fetch \\ +> https://download.freebsd.org/ftp/doc/en/books/handbook/book.pdf +.Ed +.Sh SEE ALSO +.Xr kill 1 , +.Xr nohup 1 , +.Xr signal 3 , +.Xr daemon 8 +.Sh STANDARDS +The +.Nm +utility is expected to conform to the +.St -p1003.1-2024 +specification. +.Sh HISTORY +The +.Nm +command first appeared in +.Fx 10.3 . +.Pp +The initial +.Fx +work was compatible with GNU +.Nm +by +.An Padraig Brady , +from GNU Coreutils 8.21. +The +.Nm +utility first appeared in GNU Coreutils 7.0. +.Sh AUTHORS +.An Baptiste Daroussin Aq Mt bapt@FreeBSD.org , +.An Vsevolod Stakhov Aq Mt vsevolod@FreeBSD.org +and +.An Aaron LI Aq Mt aly@aaronly.me diff --git a/bin/timeout/timeout.c b/bin/timeout/timeout.c new file mode 100644 index 000000000000..58a5797f3eaf --- /dev/null +++ b/bin/timeout/timeout.c @@ -0,0 +1,511 @@ +/*- + * Copyright (c) 2014 Baptiste Daroussin <bapt@FreeBSD.org> + * Copyright (c) 2014 Vsevolod Stakhov <vsevolod@FreeBSD.org> + * Copyright (c) 2025 Aaron LI <aly@aaronly.me> + * 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. + */ + +#include <sys/fcntl.h> +#include <sys/procctl.h> +#include <sys/resource.h> +#include <sys/time.h> +#include <sys/wait.h> + +#include <err.h> +#include <errno.h> +#include <getopt.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define EXIT_TIMEOUT 124 +#define EXIT_INVALID 125 +#define EXIT_CMD_ERROR 126 +#define EXIT_CMD_NOENT 127 + +static volatile sig_atomic_t sig_chld = 0; +static volatile sig_atomic_t sig_alrm = 0; +static volatile sig_atomic_t sig_term = 0; /* signal to terminate children */ +static volatile sig_atomic_t sig_other = 0; /* signal to propagate */ +static int killsig = SIGTERM; /* signal to kill children */ +static const char *command = NULL; +static bool verbose = false; + +static void __dead2 +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", + getprogname()); + exit(EXIT_FAILURE); +} + +static void +logv(const char *fmt, ...) +{ + va_list ap; + + if (!verbose) + return; + + va_start(ap, fmt); + vwarnx(fmt, ap); + va_end(ap); +} + +static double +parse_duration(const char *duration) +{ + double ret; + char *suffix; + + ret = strtod(duration, &suffix); + if (suffix == duration) + errx(EXIT_INVALID, "duration is not a number"); + + if (*suffix == '\0') + return (ret); + + if (suffix[1] != '\0') + errx(EXIT_INVALID, "duration unit suffix too long"); + + switch (*suffix) { + case 's': + break; + case 'm': + ret *= 60; + break; + case 'h': + ret *= 60 * 60; + break; + case 'd': + ret *= 60 * 60 * 24; + break; + default: + errx(EXIT_INVALID, "duration unit suffix invalid"); + } + + if (ret < 0 || ret >= 100000000UL) + errx(EXIT_INVALID, "duration out of range"); + + return (ret); +} + +static int +parse_signal(const char *str) +{ + int sig, i; + const char *errstr; + + sig = strtonum(str, 1, sys_nsig - 1, &errstr); + if (errstr == NULL) + return (sig); + + if (strncasecmp(str, "SIG", 3) == 0) + str += 3; + for (i = 1; i < sys_nsig; i++) { + if (strcasecmp(str, sys_signame[i]) == 0) + return (i); + } + + errx(EXIT_INVALID, "invalid signal"); +} + +static void +sig_handler(int signo) +{ + if (signo == killsig) { + sig_term = signo; + return; + } + + switch (signo) { + case SIGCHLD: + sig_chld = 1; + break; + case SIGALRM: + sig_alrm = 1; + break; + case SIGHUP: + case SIGINT: + case SIGQUIT: + case SIGILL: + case SIGTRAP: + case SIGABRT: + case SIGEMT: + case SIGFPE: + case SIGBUS: + case SIGSEGV: + case SIGSYS: + case SIGPIPE: + case SIGTERM: + case SIGXCPU: + case SIGXFSZ: + case SIGVTALRM: + case SIGPROF: + case SIGUSR1: + case SIGUSR2: + /* + * Signals with default action to terminate the process. + * See the sigaction(2) man page. + */ + sig_term = signo; + break; + default: + sig_other = signo; + break; + } +} + +static void +send_sig(pid_t pid, int signo, bool foreground) +{ + struct procctl_reaper_kill rk; + int error; + + logv("sending signal %s(%d) to command '%s'", + sys_signame[signo], signo, command); + if (foreground) { + if (kill(pid, signo) == -1) { + if (errno != ESRCH) + warn("kill(%d, %s)", (int)pid, + sys_signame[signo]); + } + } else { + memset(&rk, 0, sizeof(rk)); + rk.rk_sig = signo; + error = procctl(P_PID, getpid(), PROC_REAP_KILL, &rk); + if (error == 0 || (error == -1 && errno == ESRCH)) + ; + else if (error == -1) { + warn("procctl(PROC_REAP_KILL)"); + if (rk.rk_fpid > 0) + warnx( + "failed to signal some processes: first pid=%d", + (int)rk.rk_fpid); + } + logv("signaled %u processes", rk.rk_killed); + } + + /* + * If the child process was stopped by a signal, POSIX.1-2024 + * requires to send a SIGCONT signal. However, the standard also + * allows to send a SIGCONT regardless of the stop state, as we + * are doing here. + */ + if (signo != SIGKILL && signo != SIGSTOP && signo != SIGCONT) { + logv("sending signal %s(%d) to command '%s'", + sys_signame[SIGCONT], SIGCONT, command); + if (foreground) { + kill(pid, SIGCONT); + } else { + memset(&rk, 0, sizeof(rk)); + rk.rk_sig = SIGCONT; + procctl(P_PID, getpid(), PROC_REAP_KILL, &rk); + } + } +} + +static void +set_interval(double iv) +{ + struct itimerval tim; + + memset(&tim, 0, sizeof(tim)); + if (iv > 0) { + tim.it_value.tv_sec = (time_t)iv; + iv -= (double)(time_t)iv; + tim.it_value.tv_usec = (suseconds_t)(iv * 1000000UL); + } + + if (setitimer(ITIMER_REAL, &tim, NULL) == -1) + err(EXIT_FAILURE, "setitimer()"); +} + +/* + * In order to avoid any possible ambiguity that a shell may not set '$?' to + * '128+signal_number', POSIX.1-2024 requires that timeout mimic the wait + * status of the child process by terminating itself with the same signal, + * while disabling core generation. + */ +static void __dead2 +kill_self(int signo) +{ + sigset_t mask; + struct rlimit rl; + + /* Reset the signal disposition and make sure it's unblocked. */ + signal(signo, SIG_DFL); + sigfillset(&mask); + sigdelset(&mask, signo); + sigprocmask(SIG_SETMASK, &mask, NULL); + + /* Disable core generation. */ + memset(&rl, 0, sizeof(rl)); + setrlimit(RLIMIT_CORE, &rl); + + logv("killing self with signal %s(%d)", sys_signame[signo], signo); + kill(getpid(), signo); + err(128 + signo, "signal %s(%d) failed to kill self", + sys_signame[signo], signo); +} + +static void +log_termination(const char *name, const siginfo_t *si) +{ + if (si->si_code == CLD_EXITED) { + logv("%s: pid=%d, exit=%d", name, si->si_pid, si->si_status); + } else if (si->si_code == CLD_DUMPED || si->si_code == CLD_KILLED) { + logv("%s: pid=%d, sig=%d", name, si->si_pid, si->si_status); + } else { + logv("%s: pid=%d, reason=%d, status=%d", si->si_pid, + si->si_code, si->si_status); + } +} + +int +main(int argc, char **argv) +{ + int ch, sig; + int pstat = 0; + pid_t pid; + int pp[2], error; + char c; + double first_kill; + double second_kill = 0; + bool foreground = false; + bool preserve = false; + bool timedout = false; + bool do_second_kill = false; + bool child_done = false; + sigset_t zeromask, allmask, oldmask; + struct sigaction sa; + struct procctl_reaper_status info; + siginfo_t si, child_si; + + const char optstr[] = "+fhk:ps:v"; + const struct option longopts[] = { + { "foreground", no_argument, NULL, 'f' }, + { "help", no_argument, NULL, 'h' }, + { "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 }, + }; + + while ((ch = getopt_long(argc, argv, optstr, longopts, NULL)) != -1) { + switch (ch) { + case 'f': + foreground = true; + break; + case 'k': + do_second_kill = true; + second_kill = parse_duration(optarg); + break; + case 'p': + preserve = true; + break; + case 's': + killsig = parse_signal(optarg); + break; + case 'v': + verbose = true; + break; + case 0: + break; + default: + usage(); + } + } + + argc -= optind; + argv += optind; + if (argc < 2) + usage(); + + first_kill = parse_duration(argv[0]); + argc--; + argv++; + command = argv[0]; + + if (!foreground) { + /* Acquire a reaper */ + if (procctl(P_PID, getpid(), PROC_REAP_ACQUIRE, NULL) == -1) + err(EXIT_FAILURE, "procctl(PROC_REAP_ACQUIRE)"); + } + + /* Block all signals to avoid racing against the child. */ + sigfillset(&allmask); + if (sigprocmask(SIG_BLOCK, &allmask, &oldmask) == -1) + err(EXIT_FAILURE, "sigprocmask()"); + + if (pipe2(pp, O_CLOEXEC) == -1) + err(EXIT_FAILURE, "pipe2"); + + pid = fork(); + if (pid == -1) { + err(EXIT_FAILURE, "fork()"); + } else if (pid == 0) { + /* + * child process + * + * POSIX.1-2024 requires that the child process inherit the + * same signal dispositions as the timeout(1) utility + * inherited, except for the signal to be sent upon timeout. + */ + signal(killsig, SIG_DFL); + if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1) + err(EXIT_FAILURE, "sigprocmask(oldmask)"); + + error = read(pp[0], &c, 1); + if (error == -1) + err(EXIT_FAILURE, "read from control pipe"); + if (error == 0) + errx(EXIT_FAILURE, "eof from control pipe"); + execvp(argv[0], argv); + warn("exec(%s)", argv[0]); + _exit(errno == ENOENT ? EXIT_CMD_NOENT : EXIT_CMD_ERROR); + } + + /* parent continues here */ + + /* Catch all signals in order to propagate them. */ + memset(&sa, 0, sizeof(sa)); + sigfillset(&sa.sa_mask); + sa.sa_handler = sig_handler; + sa.sa_flags = SA_RESTART; + for (sig = 1; sig < sys_nsig; sig++) { + if (sig == SIGKILL || sig == SIGSTOP || sig == SIGCONT || + sig == SIGTTIN || sig == SIGTTOU) + continue; + if (sigaction(sig, &sa, NULL) == -1) + err(EXIT_FAILURE, "sigaction(%d)", sig); + } + + /* Don't stop if background child needs TTY */ + signal(SIGTTIN, SIG_IGN); + signal(SIGTTOU, SIG_IGN); + + set_interval(first_kill); + error = write(pp[1], "a", 1); + if (error == -1) + err(EXIT_FAILURE, "write to control pipe"); + if (error == 0) + errx(EXIT_FAILURE, "short write to control pipe"); + sigemptyset(&zeromask); + + for (;;) { + sigsuspend(&zeromask); + + if (sig_chld) { + sig_chld = 0; + + for (;;) { + memset(&si, 0, sizeof(si)); + error = waitid(P_ALL, -1, &si, WEXITED | + WNOHANG); + if (error == -1) { + if (errno != EINTR) + break; + } else if (si.si_pid == pid) { + child_si = si; + child_done = true; + log_termination("child terminated", + &child_si); + } else if (si.si_pid != 0) { + /* + * Collect grandchildren zombies. + * Only effective if we're a reaper. + */ + log_termination("collected zombie", + &si); + } else /* si.si_pid == 0 */ { + break; + } + } + if (child_done) { + if (foreground) { + break; + } else { + procctl(P_PID, getpid(), + PROC_REAP_STATUS, &info); + if (info.rs_children == 0) + break; + } + } + } else if (sig_alrm || sig_term) { + if (sig_alrm) { + sig = killsig; + sig_alrm = 0; + timedout = true; + logv("time limit reached or received SIGALRM"); + } else { + sig = sig_term; + sig_term = 0; + logv("received terminating signal %s(%d)", + sys_signame[sig], sig); + } + + send_sig(pid, sig, foreground); + + if (do_second_kill) { + set_interval(second_kill); + do_second_kill = false; + killsig = SIGKILL; + } + + } else if (sig_other) { + /* Propagate any other signals. */ + sig = sig_other; + sig_other = 0; + logv("received signal %s(%d)", sys_signame[sig], sig); + + send_sig(pid, sig, foreground); + } + } + + if (!foreground) + procctl(P_PID, getpid(), PROC_REAP_RELEASE, NULL); + + if (timedout && !preserve) { + pstat = EXIT_TIMEOUT; + } else if (child_si.si_code == CLD_DUMPED || + child_si.si_code == CLD_KILLED) { + kill_self(child_si.si_status); + /* NOTREACHED */ + } else if (child_si.si_code == CLD_EXITED) { + pstat = child_si.si_status; + } else { + pstat = EXIT_FAILURE; + } + + return (pstat); +} |
