logo

utils-std

Collection of commonly available Unix tools
commit: 06700995d374e2d60b6264dd2a99597f5b52b54d
parent 5709b374b1237299f9039d77737c0572983359e2
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Sat,  4 May 2024 04:16:24 +0200

cmd/timeout: new

Diffstat:

MMakefile7+++++++
Acmd/timeout.175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/timeout.c213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcoreutils.txt2+-
Alib/sys_signame.h95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/sys_signame.sh26++++++++++++++++++++++++++
Mmakeless.sh1+
Atest-cmd/timeout.t21+++++++++++++++++++++
8 files changed, 439 insertions(+), 1 deletion(-)

diff --git a/Makefile b/Makefile @@ -62,6 +62,9 @@ C_SOURCES = cmd/*.c lib/*.h lib/*.c configure.d/*.c format: $(C_SOURCES) clang-format -style=file -assume-filename=.clang-format -i $(C_SOURCES) +lib/sys_signame.h: lib/sys_signame.sh + lib/sys_signame.sh >|lib/sys_signame.h + cmd/date: cmd/date.c lib/iso_parse.c Makefile rm -f ${<:=.gcov} ${@:=.gcda} ${@:=.gcno} $(CC) -std=c99 $(CFLAGS) -o $@ cmd/date.c lib/iso_parse.c $(LDFLAGS) $(LDSTATIC) @@ -80,6 +83,10 @@ cmd/sleep: cmd/sleep.c lib/strtodur.c Makefile rm -f ${<:=.gcov} ${@:=.gcda} ${@:=.gcno} $(CC) -std=c99 $(CFLAGS) -o $@ cmd/sleep.c lib/strtodur.c $(LDFLAGS) $(LDSTATIC) +cmd/timeout: cmd/timeout.c lib/strtodur.c lib/sys_signame.h Makefile + rm -f ${<:=.gcov} ${@:=.gcda} ${@:=.gcno} + $(CC) -std=c99 $(CFLAGS) -o $@ cmd/timeout.c lib/strtodur.c $(LDFLAGS) $(LDSTATIC) + test-lib/mode: test-lib/mode.c lib/mode.c Makefile $(CC) -std=c99 $(CFLAGS) $(ATF_CFLAGS) -o $@ test-lib/mode.c lib/mode.c $(LDFLAGS) $(ATF_LIBS) diff --git a/cmd/timeout.1 b/cmd/timeout.1 @@ -0,0 +1,75 @@ +.\" utils-std: Collection of commonly available Unix tools +.\" Copyright 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> +.\" SPDX-License-Identifier: MPL-2.0 +.Dd 2024-05-04 +.Dt TIMEOUT 1 +.Os +.Sh NAME +.Nm timeout +.Nd run a command with a time limit +.Sh SYNOPSIS +.Nm +.Op Fl k Ar duration +.Op Fl s Ar SIGNAL +.Ar duration +.Ar command +.Op Ar arguments... +.Sh DESCRIPTION +The +.Nm +utility executes +.Ar command +and terminates it, if still running after +.Ar duration . +.Pp +.Ar duration +is a non-negative decimal number including floats, +optionally followed by a suffix: s for seconds (default), m for minutes, h for hours. +.Sh OPTIONS +.Bl -tag -width __ +.It Fl k Ar duration +Enables sending a +.Dv SIGKILL +after +.Ar duration . +.It Fl s Ar SIGNAL +Signal to be sent on timeout, by default +.Dv SIGTERM +is sent. +Signal may be a name like 'HUP' or 'SIGHUP', or a number like '9'. +.Pp +A list of signals may be obtained with +.Cm kill +.Fl l . +.El +.Sh EXIT STATUS +The +.Nm +utility may return one of the following statuses: +.Pp +.Bl -tag -width 111 -compact +.It 124 +Timeout reached +.It 125 +Error within +.Nm +.It 126 +Failed to execute +.Ar command +.El +.Pp +Otherwise, the exit status of +.Ar command +is returned. +.Sh SEE ALSO +.Xr kill 1 , +.Xr sleep 1 +.Sh HISTORY +A +.Nm +utility appeared in SATAN, Netatalk, GNU Coreutils 7.0, +.Ox 7.0 , +.Nx 7.0 , +.Fx 10.3 . +.Sh AUTHORS +.An Haelwenn (lanodan) Monnier Aq Mt contact+utils@hacktivis.me diff --git a/cmd/timeout.c b/cmd/timeout.c @@ -0,0 +1,213 @@ +// utils-std: Collection of commonly available Unix tools +// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> +// SPDX-License-Identifier: MPL-2.0 + +#define _POSIX_C_SOURCE 200809L +#include "../lib/strtodur.h" +#include "../lib/sys_signame.h" + +#include <assert.h> +#include <ctype.h> +#include <errno.h> +#include <signal.h> // kill +#include <spawn.h> // posix_spawn +#include <stdio.h> // fprintf +#include <stdlib.h> // exit +#include <string.h> // strerror +#include <sys/wait.h> +#include <time.h> // nanosleep +#include <unistd.h> // getopt + +#define CMD_EXIT_TIMEOUT 124 +#define CMD_EXIT_FAILURE 125 +#define CMD_EXIT_E_EXEC 126 +#define CMD_EXIT_ENOENT 127 +#define CMD_EXIT_KILL 137 + +extern char **environ; +pid_t child = 0; + +static void +handle_sigchld(int sig) +{ + (void)sig; + int stat_loc = 0; + waitpid(child, &stat_loc, WNOHANG); + exit(WEXITSTATUS(stat_loc)); +} + +static void +usage() +{ + fprintf(stderr, "Usage: timeout [-k duration] [-s SIGNAL] command arguments...\n"); +} + +int +main(int argc, char *argv[]) +{ + struct timespec time_kill = {.tv_sec = 0, .tv_nsec = 0}; + int term_sig = SIGTERM; + char *term_signame = "SIGTERM"; + + char *arg = NULL; + + int c = -1; + while((c = getopt(argc, argv, ":k:s:")) != -1) + { + switch(c) + { + case 'k': + if(strtodur(argv[0], &time_kill) < 0) return 1; + + if(time_kill.tv_sec == 0 && time_kill.tv_nsec == 0) + { + fprintf(stderr, "timeout: Got a duration of 0 for SIGKILL timeout.\n"); + return CMD_EXIT_FAILURE; + } + break; + case 's': + init_util_sys_signame(); + + if(isdigit(optarg[0])) + { + assert(errno == 0); + char *endptr = NULL; + + unsigned long optoul = strtoul(optarg, &endptr, 10); + if(errno != 0 && endptr != NULL) + { + fprintf(stderr, + "timeout: Mix of digit and non-digit characters passed to -s option '%s'\n", + endptr); + return 1; + } + if(errno == 0 && optoul == 0) errno = EINVAL; + if(errno == 0 && optoul >= util_sys_signame_len) errno = ERANGE; + if(errno != 0) + { + fprintf(stderr, + "timeout: Invalid number passed to -s '%s' (got: %lu): %s\n", + optarg, + optoul, + strerror(errno)); + return 1; + } + + if(util_sys_signame[optoul] == NULL) + { + fprintf(stderr, "timeout: Unknown signal number %lu\n", optoul); + return 1; + } + + term_sig = optoul; + } + else + { + arg = optarg; + if(arg[0] == 'S' && arg[1] == 'I' && arg[2] == 'G') arg += 3; + + size_t i = 0; + for(; i < (util_sys_signame_len); i++) + { + if(util_sys_signame[i] == NULL) continue; + if(strcmp(arg, util_sys_signame[i]) == 0) + { + term_sig = i; + term_signame = optarg; + break; + } + } + if(i >= util_sys_signame_len) + { + fprintf(stderr, "timeout: Unknown signal name '%s'\n", optarg); + return CMD_EXIT_FAILURE; + } + } + break; + case ':': + fprintf(stderr, "timeout: Error: Missing operand for option: '-%c'\n", optopt); + usage(); + return 1; + case '?': + fprintf(stderr, "timeout: Error: Unrecognised option: '-%c'\n", optopt); + usage(); + return 1; + } + } + + assert(errno == 0); + + argv += optind; + argc -= optind; + + struct timespec term = {.tv_sec = 0, .tv_nsec = 0}; + if(strtodur(argv[0], &term) < 0) return 1; + + if(term.tv_sec == 0 && term.tv_nsec == 0) + { + fprintf(stderr, "timeout: Got a duration of 0 for (%s) timeout.\n", term_signame); + return CMD_EXIT_FAILURE; + } + + argv++; + argc--; + + assert(errno == 0); + if(signal(SIGCHLD, handle_sigchld) == SIG_ERR) + { + fprintf(stderr, "timeout: Failed registering handler for SIGCHLD: %s\n", strerror(errno)); + return CMD_EXIT_FAILURE; + } + + if(posix_spawnp(&child, argv[0], NULL, NULL, argv, environ) != 0) + { + fprintf(stderr, "timeout: Failed launching command '%s': %s\n", argv[0], strerror(errno)); + return CMD_EXIT_E_EXEC; + } + + // Because PATH-finding typically relies on trial-and-error + errno = 0; + + if(nanosleep(&term, &term) < 0) + { + if(errno != EINTR) + { + fprintf(stderr, "timeout: Error sleeping: %s\n", strerror(errno)); + return CMD_EXIT_FAILURE; + } + } + + assert(errno == 0); + if(kill(child, term_sig) != 0) + { + fprintf(stderr, + "timeout: Failed sending %s to child %d after timeout: %s\n", + term_signame, + child, + strerror(errno)); + return CMD_EXIT_FAILURE; + } + + if(time_kill.tv_sec == 0 && time_kill.tv_nsec == 0) return CMD_EXIT_TIMEOUT; + + if(nanosleep(&time_kill, &time_kill) < 0) + { + if(errno != EINTR) + { + fprintf(stderr, "timeout: Error sleeping: %s\n", strerror(errno)); + return CMD_EXIT_FAILURE; + } + } + + assert(errno == 0); + if(kill(child, SIGKILL) != 0) + { + fprintf(stderr, + "timeout: Failed sending SIGKILL to child %d after timeout: %s\n", + child, + strerror(errno)); + return CMD_EXIT_FAILURE; + } + + return CMD_EXIT_TIMEOUT; +} diff --git a/coreutils.txt b/coreutils.txt @@ -87,7 +87,7 @@ tac: Todo tail: ? tee: Done test: Done -timeout: ? +timeout: Done touch: Done tr: Done true: Done diff --git a/lib/sys_signame.h b/lib/sys_signame.h @@ -0,0 +1,95 @@ +// utils-std: Collection of commonly available Unix tools +// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> +// SPDX-License-Identifier: CC0-1.0 OR WTFPL +#define _BSD_SOURCE // NSIG +#define _GNU_SOURCE // NSIG + +#include <signal.h> // NSIG +static char *util_sys_signame[NSIG]; +static size_t util_sys_signame_len = NSIG; + +static void +init_util_sys_signame() +{ +#ifdef SIGABRT + util_sys_signame[SIGABRT] = "ABRT"; +#endif +#ifdef SIGALRM + util_sys_signame[SIGALRM] = "ALRM"; +#endif +#ifdef SIGBUS + util_sys_signame[SIGBUS] = "BUS"; +#endif +#ifdef SIGCHLD + util_sys_signame[SIGCHLD] = "CHLD"; +#endif +#ifdef SIGCONT + util_sys_signame[SIGCONT] = "CONT"; +#endif +#ifdef SIGFPE + util_sys_signame[SIGFPE] = "FPE"; +#endif +#ifdef SIGHUP + util_sys_signame[SIGHUP] = "HUP"; +#endif +#ifdef SIGILL + util_sys_signame[SIGILL] = "ILL"; +#endif +#ifdef SIGINT + util_sys_signame[SIGINT] = "INT"; +#endif +#ifdef SIGKILL + util_sys_signame[SIGKILL] = "KILL"; +#endif +#ifdef SIGPIPE + util_sys_signame[SIGPIPE] = "PIPE"; +#endif +#ifdef SIGQUIT + util_sys_signame[SIGQUIT] = "QUIT"; +#endif +#ifdef SIGSEGV + util_sys_signame[SIGSEGV] = "SEGV"; +#endif +#ifdef SIGSTOP + util_sys_signame[SIGSTOP] = "STOP"; +#endif +#ifdef SIGTERM + util_sys_signame[SIGTERM] = "TERM"; +#endif +#ifdef SIGTSTP + util_sys_signame[SIGTSTP] = "TSTP"; +#endif +#ifdef SIGTTIN + util_sys_signame[SIGTTIN] = "TTIN"; +#endif +#ifdef SIGTTOU + util_sys_signame[SIGTTOU] = "TTOU"; +#endif +#ifdef SIGUSR1 + util_sys_signame[SIGUSR1] = "USR1"; +#endif +#ifdef SIGUSR2 + util_sys_signame[SIGUSR2] = "USR2"; +#endif +#ifdef SIGWINCH + util_sys_signame[SIGWINCH] = "WINCH"; +#endif +#ifdef SIGSYS + util_sys_signame[SIGSYS] = "SYS"; +#endif +#ifdef SIGTRAP + util_sys_signame[SIGTRAP] = "TRAP"; +#endif +#ifdef SIGURG + util_sys_signame[SIGURG] = "URG"; +#endif +#ifdef SIGVTALRM + util_sys_signame[SIGVTALRM] = "VTALRM"; +#endif +#ifdef SIGXCPU + util_sys_signame[SIGXCPU] = "XCPU"; +#endif +#ifdef SIGXFSZ + util_sys_signame[SIGXFSZ] = "XFSZ"; +#endif +} diff --git a/lib/sys_signame.sh b/lib/sys_signame.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# utils-std: Collection of commonly available Unix tools +# SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> +# SPDX-License-Identifier: CC0-1.0 OR WTFPL +cat <<-EOF +// utils-std: Collection of commonly available Unix tools +// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> +// SPDX-License-Identifier: CC0-1.0 OR WTFPL +#define _BSD_SOURCE // NSIG +#define _GNU_SOURCE // NSIG + +#include <signal.h> // NSIG +static char *util_sys_signame[NSIG]; + +static void +init_util_sys_signame() +{ +EOF + +# From signal.h definition in POSIX +for i in SIGABRT SIGALRM SIGBUS SIGCHLD SIGCONT SIGFPE SIGHUP SIGILL SIGINT SIGKILL SIGPIPE SIGQUIT SIGSEGV SIGSTOP SIGTERM SIGTSTP SIGTTIN SIGTTOU SIGUSR1 SIGUSR2 SIGWINCH SIGSYS SIGTRAP SIGURG SIGVTALRM SIGXCPU SIGXFSZ +do + printf '#ifdef %s\n\tutil_sys_signame[%s] = "%s";\n#endif\n' "$i" "$i" "${i#SIG}" +done + +printf '}\n' diff --git a/makeless.sh b/makeless.sh @@ -48,6 +48,7 @@ $CC -std=c99 $CFLAGS -o cmd/sync cmd/sync.c $LDFLAGS $LDSTATIC $CC -std=c99 $CFLAGS -o cmd/tee cmd/tee.c $LDFLAGS $LDSTATIC $CC -std=c99 $CFLAGS -o cmd/test cmd/test.c $LDFLAGS $LDSTATIC $CC -std=c99 $CFLAGS -o cmd/time cmd/time.c $LDFLAGS $LDSTATIC +$CC -std=c99 $CFLAGS -o cmd/timeout cmd/timeout.c lib/strtodur.c $LDFLAGS $LDSTATIC $CC -std=c99 $CFLAGS -o cmd/touch cmd/touch.c lib/iso_parse.c $LDFLAGS $LDSTATIC $CC -std=c99 $CFLAGS -o cmd/tr cmd/tr.c lib/tr_str.c $LDFLAGS $LDSTATIC $CC -std=c99 $CFLAGS -o cmd/true cmd/true.c $LDFLAGS $LDSTATIC diff --git a/test-cmd/timeout.t b/test-cmd/timeout.t @@ -0,0 +1,21 @@ +#!/usr/bin/env cram +# SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> +# SPDX-License-Identifier: MPL-2.0 + + $ export PATH="$TESTDIR/../cmd:$PATH" + + $ test "$(command -v timeout)" = "$TESTDIR/../cmd/timeout" + + $ timeout 0.5 sleep 10 + [124] + $ timeout 0.5 sleep 0.2 + $ timeout 0.5 true + $ timeout 0.5 false + [1] + + $ timeout -s KILL 0.5 sleep 10 + [124] + $ timeout -s SIGKILL 0.5 sleep 10 + [124] + $ timeout -s 9 0.5 sleep 10 + [124]