commit: 06700995d374e2d60b6264dd2a99597f5b52b54d
parent 5709b374b1237299f9039d77737c0572983359e2
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Sat, 4 May 2024 04:16:24 +0200
cmd/timeout: new
Diffstat:
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]