diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:30:06 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:30:06 +0300 |
| commit | b82d29b6d7d6e7f092f705bb3a1387367562aa24 (patch) | |
| tree | eff5fec85e6ee67626d2d8530edf3b30f178d9b4 /corebinutils/timeout | |
| parent | 22343fc2bb8db94066d3e314c5a20a9b9278f4c9 (diff) | |
| parent | 2625a98c83fe53dabba3e3c7743d0fbb77c235a2 (diff) | |
| download | Project-Tick-b82d29b6d7d6e7f092f705bb3a1387367562aa24.tar.gz Project-Tick-b82d29b6d7d6e7f092f705bb3a1387367562aa24.zip | |
Add 'corebinutils/timeout/' from commit '2625a98c83fe53dabba3e3c7743d0fbb77c235a2'
git-subtree-dir: corebinutils/timeout
git-subtree-mainline: 22343fc2bb8db94066d3e314c5a20a9b9278f4c9
git-subtree-split: 2625a98c83fe53dabba3e3c7743d0fbb77c235a2
Diffstat (limited to 'corebinutils/timeout')
| -rw-r--r-- | corebinutils/timeout/.gitignore | 2 | ||||
| -rw-r--r-- | corebinutils/timeout/GNUmakefile | 37 | ||||
| -rw-r--r-- | corebinutils/timeout/LICENSE | 25 | ||||
| -rw-r--r-- | corebinutils/timeout/README.md | 47 | ||||
| -rw-r--r-- | corebinutils/timeout/renovate.json | 3 | ||||
| -rwxr-xr-x | corebinutils/timeout/tests/test.sh | 285 | ||||
| -rw-r--r-- | corebinutils/timeout/timeout.1 | 292 | ||||
| -rw-r--r-- | corebinutils/timeout/timeout.c | 1016 |
8 files changed, 1707 insertions, 0 deletions
diff --git a/corebinutils/timeout/.gitignore b/corebinutils/timeout/.gitignore new file mode 100644 index 0000000000..a07d5ed8b8 --- /dev/null +++ b/corebinutils/timeout/.gitignore @@ -0,0 +1,2 @@ +/build/ +/out/ diff --git a/corebinutils/timeout/GNUmakefile b/corebinutils/timeout/GNUmakefile new file mode 100644 index 0000000000..8b19b9f278 --- /dev/null +++ b/corebinutils/timeout/GNUmakefile @@ -0,0 +1,37 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS ?= +CPPFLAGS += -D_POSIX_C_SOURCE=200809L +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Werror +LDFLAGS ?= +LDLIBS ?= +LDLIBS += -lm + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/timeout +OBJS := $(OBJDIR)/timeout.o + +.PHONY: all clean dirs status test + +all: $(TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(OBJDIR)/timeout.o: $(CURDIR)/timeout.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/timeout.c" -o "$@" + +test: $(TARGET) + CC="$(CC)" TIMEOUT_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(OBJDIR)" "$(OUTDIR)" diff --git a/corebinutils/timeout/LICENSE b/corebinutils/timeout/LICENSE new file mode 100644 index 0000000000..27c8b75e7b --- /dev/null +++ b/corebinutils/timeout/LICENSE @@ -0,0 +1,25 @@ +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 + +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 REGENTS 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 REGENTS 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. diff --git a/corebinutils/timeout/README.md b/corebinutils/timeout/README.md new file mode 100644 index 0000000000..3d8448ef87 --- /dev/null +++ b/corebinutils/timeout/README.md @@ -0,0 +1,47 @@ +# timeout + +Standalone Linux-native port of FreeBSD `timeout` for Project Tick BSD/Linux Distribution. + +## Build + +```sh +gmake -f GNUmakefile +gmake -f GNUmakefile CC=musl-gcc +``` + +## Test + +```sh +gmake -f GNUmakefile test +gmake -f GNUmakefile test CC=musl-gcc +``` + +## Port Strategy + +- Port structure follows sibling standalone ports (`bin/sleep`, `bin/rmdir`, `bin/pwait`): local `GNUmakefile`, short technical `README.md`, shell tests under `tests/`. +- The FreeBSD source was converted to Linux-native process control instead of carrying BSD-only reaper APIs (`procctl(PROC_REAP_*)`, `str2sig`, `err(3)` helpers). +- Signal handling was redesigned around blocked-signal waiting (`sigwaitinfo(2)`/`sigtimedwait(2)`) to avoid asynchronous global-flag races and keep strict, deterministic state transitions. +- Duration and signal parsing are strict and explicit: malformed tokens fail with clear diagnostics and status `125` instead of silent coercion. + +## Linux API Mapping + +- FreeBSD process reaper control (`procctl(PROC_REAP_ACQUIRE/KILL/STATUS/RELEASE)`) maps to: + - `prctl(PR_SET_CHILD_SUBREAPER)` for descendant reparent/wait behavior. + - Process-group signaling (`kill(-pgid, sig)`) for default non-foreground propagation. + - `waitpid(-1, ..., WNOHANG)` loops to reap both command and adopted descendants. +- FreeBSD timer path (`setitimer(ITIMER_REAL)` + `SIGALRM`) maps to monotonic deadlines driven by `clock_gettime(CLOCK_MONOTONIC)` and `sigtimedwait(2)` timeout waits. +- FreeBSD `str2sig(3)` maps to an in-tree, libc-independent signal parser (numbers, `SIG`-prefixed names, aliases, and realtime forms). +- FreeBSD `err(3)`/`warn(3)` maps to direct `fprintf(3)`/`dprintf(2)` diagnostics, preserving musl portability. + +## Supported Semantics On Linux + +- `timeout [-f] [-k time] [-p] [-s signal] [-v] duration command [arg ...]` as documented by `timeout.1`. +- `duration`/`-k time` with `s`, `m`, `h`, `d` suffixes and strict decimal parsing. +- Exit statuses `124`, `125`, `126`, `127`, plus command status passthrough and self-termination with the command's signal status when required. +- External `SIGALRM` handling as "time limit reached" behavior, matching `timeout.1`. + +## Intentionally Unsupported / Limited Semantics + +- Non-finite durations (`inf`, `infinity`, `nan`) are rejected with explicit `125` errors. +- Descendants that intentionally escape the command process group (e.g. explicit `setsid(2)`/`setpgid(2)` re-grouping) are treated as unsupported during timeout enforcement: if timed-out descendants remain but the original command process group no longer exists, the utility fails explicitly with status `125` instead of silently waiting indefinitely. +- If Linux subreaper support is unavailable (`prctl(PR_SET_CHILD_SUBREAPER)` missing/failing), non-foreground mode fails explicitly with an error instead of silently downgrading behavior. diff --git a/corebinutils/timeout/renovate.json b/corebinutils/timeout/renovate.json new file mode 100644 index 0000000000..7190a60b64 --- /dev/null +++ b/corebinutils/timeout/renovate.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json" +} diff --git a/corebinutils/timeout/tests/test.sh b/corebinutils/timeout/tests/test.sh new file mode 100755 index 0000000000..c9018c50f1 --- /dev/null +++ b/corebinutils/timeout/tests/test.sh @@ -0,0 +1,285 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +TIMEOUT_BIN=${TIMEOUT_BIN:-"$ROOT/out/timeout"} +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp -d "$TMPDIR/timeout-test.XXXXXX") +STDOUT_FILE="$WORKDIR/stdout" +STDERR_FILE="$WORKDIR/stderr" +LAST_STATUS=0 +LAST_ELAPSED=0 +LAST_STDOUT= +LAST_STDERR= +trap 'rm -rf "$WORKDIR"' EXIT INT TERM + +export LC_ALL=C + +USAGE_TEXT='Usage: timeout [-f | --foreground] [-k time | --kill-after time] [-p | --preserve-status] [-s signal | --signal signal] [-v | --verbose] <duration> <command> [arg ...]' + +fail() { + printf '%s\n' "FAIL: $1" >&2 + exit 1 +} + +assert_eq() { + name=$1 + expected=$2 + actual=$3 + if [ "$expected" != "$actual" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "--- expected ---" >&2 + printf '%s\n' "$expected" >&2 + printf '%s\n' "--- actual ---" >&2 + printf '%s\n' "$actual" >&2 + exit 1 + fi +} + +assert_contains() { + name=$1 + text=$2 + pattern=$3 + case $text in + *"$pattern"*) ;; + *) fail "$name" ;; + esac +} + +assert_empty() { + name=$1 + text=$2 + if [ -n "$text" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "--- expected empty ---" >&2 + printf '%s\n' "--- actual ---" >&2 + printf '%s\n' "$text" >&2 + exit 1 + fi +} + +assert_status() { + name=$1 + expected=$2 + actual=$3 + if [ "$expected" -ne "$actual" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "expected status: $expected" >&2 + printf '%s\n' "actual status: $actual" >&2 + exit 1 + fi +} + +assert_ge() { + name=$1 + actual=$2 + minimum=$3 + if [ "$actual" -lt "$minimum" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "expected >= $minimum" >&2 + printf '%s\n' "actual: $actual" >&2 + exit 1 + fi +} + +assert_le() { + name=$1 + actual=$2 + maximum=$3 + if [ "$actual" -gt "$maximum" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "expected <= $maximum" >&2 + printf '%s\n' "actual: $actual" >&2 + exit 1 + fi +} + +run_capture() { + if "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + LAST_STATUS=0 + else + LAST_STATUS=$? + fi + LAST_STDOUT=$(cat "$STDOUT_FILE") + LAST_STDERR=$(cat "$STDERR_FILE") +} + +run_timed_capture() { + start=$(date +%s) + if "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + LAST_STATUS=0 + else + LAST_STATUS=$? + fi + end=$(date +%s) + LAST_ELAPSED=$((end - start)) + LAST_STDOUT=$(cat "$STDOUT_FILE") + LAST_STDERR=$(cat "$STDERR_FILE") +} + +[ -x "$TIMEOUT_BIN" ] || fail "missing binary: $TIMEOUT_BIN" + +run_capture "$TIMEOUT_BIN" +assert_status "usage status" 125 "$LAST_STATUS" +assert_empty "usage stdout" "$LAST_STDOUT" +assert_eq "usage stderr" "$USAGE_TEXT" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" -z +assert_status "invalid option status" 125 "$LAST_STATUS" +assert_empty "invalid option stdout" "$LAST_STDOUT" +assert_eq "invalid option stderr" "$USAGE_TEXT" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" -- 1 sh -c 'exit 0' +assert_status "double-dash separator status" 0 "$LAST_STATUS" +assert_empty "double-dash separator stdout" "$LAST_STDOUT" +assert_empty "double-dash separator stderr" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" invalid sleep 0 +assert_status "invalid duration token status" 125 "$LAST_STATUS" +assert_empty "invalid duration token stdout" "$LAST_STDOUT" +assert_eq "invalid duration token stderr" \ + "timeout: duration is not a number" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" -k invalid 1 sleep 0 +assert_status "invalid kill-after status" 125 "$LAST_STATUS" +assert_empty "invalid kill-after stdout" "$LAST_STDOUT" +assert_eq "invalid kill-after stderr" \ + "timeout: duration is not a number" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" 42D sleep 0 +assert_status "invalid duration unit status" 125 "$LAST_STATUS" +assert_empty "invalid duration unit stdout" "$LAST_STDOUT" +assert_eq "invalid duration unit stderr" \ + "timeout: duration unit suffix invalid" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" 1ss sleep 0 +assert_status "duration suffix too long status" 125 "$LAST_STATUS" +assert_empty "duration suffix too long stdout" "$LAST_STDOUT" +assert_eq "duration suffix too long stderr" \ + "timeout: duration unit suffix too long" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" -- -1 sleep 0 +assert_status "negative duration status" 125 "$LAST_STATUS" +assert_empty "negative duration stdout" "$LAST_STDOUT" +assert_eq "negative duration stderr" \ + "timeout: duration out of range" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" inf sleep 0 +assert_status "non-finite duration status" 125 "$LAST_STATUS" +assert_empty "non-finite duration stdout" "$LAST_STDOUT" +assert_eq "non-finite duration stderr" \ + "timeout: duration out of range" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" -s not-a-signal 1 sleep 0 +assert_status "invalid signal status" 125 "$LAST_STATUS" +assert_empty "invalid signal stdout" "$LAST_STDOUT" +assert_eq "invalid signal stderr" \ + "timeout: invalid signal: not-a-signal" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" 1 sh -c 'exit 7' +assert_status "normal exit passthrough status" 7 "$LAST_STATUS" +assert_empty "normal exit passthrough stdout" "$LAST_STDOUT" +assert_empty "normal exit passthrough stderr" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" 0 sh -c 'exit 23' +assert_status "zero duration status passthrough status" 23 "$LAST_STATUS" +assert_empty "zero duration status passthrough stdout" "$LAST_STDOUT" +assert_empty "zero duration status passthrough stderr" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" 1 sleep 5 +assert_status "timeout status" 124 "$LAST_STATUS" +assert_empty "timeout stdout" "$LAST_STDOUT" +assert_empty "timeout stderr" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" -v 1 sleep 5 +assert_status "verbose timeout status" 124 "$LAST_STATUS" +assert_empty "verbose timeout stdout" "$LAST_STDOUT" +assert_contains "verbose timeout stderr marker" "$LAST_STDERR" "time limit reached" + +run_capture "$TIMEOUT_BIN" -p 1 sleep 5 +assert_status "preserve timeout status" 143 "$LAST_STATUS" +assert_empty "preserve timeout stdout" "$LAST_STDOUT" +assert_empty "preserve timeout stderr" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" -p -s KILL 1 sleep 5 +assert_status "preserve timeout SIGKILL status" 137 "$LAST_STATUS" +assert_empty "preserve timeout SIGKILL stdout" "$LAST_STDOUT" +assert_empty "preserve timeout SIGKILL stderr" "$LAST_STDERR" + +run_capture "$TIMEOUT_BIN" -p -k 1 1 sh -c 'trap "" TERM; sleep 10' +assert_status "kill-after escalation status" 137 "$LAST_STATUS" +assert_empty "kill-after escalation stdout" "$LAST_STDOUT" +assert_empty "kill-after escalation stderr" "$LAST_STDERR" + +NOEXEC="$WORKDIR/noexec.sh" +printf '%s\n' '#!/bin/sh' 'exit 0' >"$NOEXEC" +chmod 0644 "$NOEXEC" +run_capture "$TIMEOUT_BIN" 1 "$NOEXEC" +assert_status "non-executable command status" 126 "$LAST_STATUS" +assert_empty "non-executable command stdout" "$LAST_STDOUT" +assert_contains "non-executable command stderr path" "$LAST_STDERR" "exec($NOEXEC)" + +run_capture "$TIMEOUT_BIN" 1 does-not-exist-timeout-test-cmd +assert_status "missing command status" 127 "$LAST_STATUS" +assert_empty "missing command stdout" "$LAST_STDOUT" +assert_contains "missing command stderr cmd" "$LAST_STDERR" "exec(does-not-exist-timeout-test-cmd)" + +"$TIMEOUT_BIN" 30 sleep 10 >"$STDOUT_FILE" 2>"$STDERR_FILE" & +bg_timeout_pid=$! +sleep 1 +kill -ALRM "$bg_timeout_pid" +if wait "$bg_timeout_pid"; then + bg_timeout_status=0 +else + bg_timeout_status=$? +fi +assert_status "SIGALRM external timeout status" 124 "$bg_timeout_status" +assert_empty "SIGALRM external timeout stdout" "$(cat "$STDOUT_FILE")" +assert_empty "SIGALRM external timeout stderr" "$(cat "$STDERR_FILE")" + +"$TIMEOUT_BIN" 30 sh -c 'trap "exit 77" USR1; while :; do sleep 1; done' \ + >"$STDOUT_FILE" 2>"$STDERR_FILE" & +prop_pid=$! +sleep 1 +kill -USR1 "$prop_pid" +if wait "$prop_pid"; then + prop_status=0 +else + prop_status=$? +fi +assert_status "signal propagation status" 77 "$prop_status" +assert_empty "signal propagation stdout" "$(cat "$STDOUT_FILE")" +assert_empty "signal propagation stderr" "$(cat "$STDERR_FILE")" + +PIDFILE="$WORKDIR/desc.pid" +run_timed_capture \ + "$TIMEOUT_BIN" -s INT 1 \ + sh -c '(trap "" INT HUP; sleep 5) & echo $! >"$1"; sleep 10' \ + sh "$PIDFILE" +assert_status "non-foreground descendant wait status" 124 "$LAST_STATUS" +assert_ge "non-foreground descendant wait elapsed seconds" "$LAST_ELAPSED" 4 +assert_empty "non-foreground descendant wait stdout" "$LAST_STDOUT" +assert_empty "non-foreground descendant wait stderr" "$LAST_STDERR" +[ -s "$PIDFILE" ] || fail "missing descendant pid for non-foreground case" +desc_pid=$(cat "$PIDFILE") +if kill -0 "$desc_pid" 2>/dev/null; then + fail "descendant still alive after non-foreground timeout" +fi + +rm -f "$PIDFILE" +run_timed_capture \ + "$TIMEOUT_BIN" -f -s INT 1 \ + sh -c '(trap "" INT HUP; sleep 5) & echo $! >"$1"; sleep 10' \ + sh "$PIDFILE" +assert_status "foreground timeout status" 124 "$LAST_STATUS" +assert_le "foreground timeout elapsed seconds" "$LAST_ELAPSED" 3 +assert_empty "foreground timeout stdout" "$LAST_STDOUT" +assert_empty "foreground timeout stderr" "$LAST_STDERR" +[ -s "$PIDFILE" ] || fail "missing descendant pid for foreground case" +desc_pid=$(cat "$PIDFILE") +if ! kill -0 "$desc_pid" 2>/dev/null; then + fail "descendant not alive after foreground timeout" +fi +kill -KILL "$desc_pid" 2>/dev/null || true + +printf '%s\n' "PASS" diff --git a/corebinutils/timeout/timeout.1 b/corebinutils/timeout/timeout.1 new file mode 100644 index 0000000000..0a9754a2cc --- /dev/null +++ b/corebinutils/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 -width 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/corebinutils/timeout/timeout.c b/corebinutils/timeout/timeout.c new file mode 100644 index 0000000000..bcb56f978c --- /dev/null +++ b/corebinutils/timeout/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); +} |
