From 2b403d74007ad51fb882def0ac3c3a02b85c7e94 Mon Sep 17 00:00:00 2001 From: job Date: Wed, 1 Sep 2021 15:50:33 +0000 Subject: [PATCH] Import timeout(1) from NetBSD The timeout(1) utility can be used to run commands with a time limit. OK deraadt@ beck@ --- usr.bin/timeout/Makefile | 5 + usr.bin/timeout/timeout.1 | 133 +++++++++++++++ usr.bin/timeout/timeout.c | 349 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 487 insertions(+) create mode 100644 usr.bin/timeout/Makefile create mode 100644 usr.bin/timeout/timeout.1 create mode 100644 usr.bin/timeout/timeout.c diff --git a/usr.bin/timeout/Makefile b/usr.bin/timeout/Makefile new file mode 100644 index 00000000000..e4fae7a15c8 --- /dev/null +++ b/usr.bin/timeout/Makefile @@ -0,0 +1,5 @@ +# $OpenBSD: Makefile,v 1.1 2021/09/01 15:50:33 job Exp $ + +PROG= timeout + +.include diff --git a/usr.bin/timeout/timeout.1 b/usr.bin/timeout/timeout.1 new file mode 100644 index 00000000000..4bd780ea33a --- /dev/null +++ b/usr.bin/timeout/timeout.1 @@ -0,0 +1,133 @@ +.\" $NetBSD: timeout.1,v 1.4 2016/10/13 06:22:26 dholland Exp $ +.\" +.\" Copyright (c) 2014 Baptiste Daroussin +.\" 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. +.\" +.\" $FreeBSD: head/usr.bin/timeout/timeout.1 268861 2014-07-18 22:56:59Z bapt $ +.\" +.Dd July 19, 2014 +.Dt TIMEOUT 1 +.Os +.Sh NAME +.Nm timeout +.Nd run a command with a time limit +.Sh SYNOPSIS +.Nm +.Op Fl Fl signal Ar sig | Fl s Ar sig +.Op Fl Fl preserve-status +.Op Fl Fl kill-after Ar time | Fl k Ar time +.Op Fl Fl foreground +.Ao Ar duration Ac +.Ao Ar command Ac +.Ao Ar args ... Ac +.Sh DESCRIPTION +.Nm +starts the +.Ar command +with its +.Ar args . +If +.Ar command +is still running after +.Ar duration , +it is killed. +By default, +.Dv SIGTERM +is sent. +.Bl -tag -width "-k time, --kill-after time" +.It Fl Fl preserve-status +Always exits with the same status as +.Ar command +even if it times out. +.It Fl Fl foreground +Do not propagate timeout to the +.Ar command +children. +.It Fl s Ar sig , Fl Fl signal Ar sig +Specify the signal to send on timeout. +By default, +.Dv SIGTERM +is sent. +.It Fl k Ar time , Fl Fl kill-after Ar time +Send a second kill signal if +.Ar command +is still running after +.Ar time +after the first signal was sent. +.El +.Sh DURATION FORMAT +.Ar duration +and +.Ar time +can be integer or decimal numbers. +Values without unit symbols are interpreted as seconds. +.Pp +Supported unit symbols are: +.Bl -tag -width indent -compact +.It s +seconds +.It m +minutes +.It h +hours +.It d +days +.El +.Sh EXIT STATUS +If the timeout was not reached, the exit status of +.Ar command +is returned. +.Pp +If the timeout was reached and +.Fl Fl preserve-status +is set, the exit status of +.Ar command +is returned. +If +.Fl Fl preserve-status +is not set, an exit status of 124 is returned. +.Pp +If +.Ar command +exits after receiving a signal, the exit status returned is the signal number plus 128. +.Sh SEE ALSO +.Xr kill 1 , +.Xr signal 3 +.Sh HISTORY +A +.Nm +utility appeared in a development branch of +.Fx 11 +and was imported into +.Nx 7 . +The +.Fx +work is compatible with GNU +.Nm +by +.An Padraig Brady , +from GNU Coreutils 8.21. +The +.Nm +utility first appeared in GNU Coreutils 7.0. diff --git a/usr.bin/timeout/timeout.c b/usr.bin/timeout/timeout.c new file mode 100644 index 00000000000..71eb77a1228 --- /dev/null +++ b/usr.bin/timeout/timeout.c @@ -0,0 +1,349 @@ +/* $NetBSD: timeout.c,v 1.4 2014/08/05 08:20:02 christos Exp $ */ + +/*- + * Copyright (c) 2014 Baptiste Daroussin + * Copyright (c) 2014 Vsevolod Stakhov + * 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EXIT_TIMEOUT 124 + +static sig_atomic_t sig_chld = 0; +static sig_atomic_t sig_term = 0; +static sig_atomic_t sig_alrm = 0; +static sig_atomic_t sig_ign = 0; + +static void __dead +usage(void) +{ + fprintf(stderr, "Usage: %s [--signal sig | -s sig] [--preserve-status]" + " [--kill-after time | -k time] [--foreground] " + " \n", getprogname()); + + exit(EX_USAGE); +} + +static double +parse_duration(const char *duration) +{ + double ret; + char *end; + + ret = strtod(duration, &end); + if (ret == 0 && end == duration) + errx(EXIT_FAILURE, "invalid duration"); + + if (end == NULL || *end == '\0') + return (ret); + + if (end != NULL && *(end + 1) != '\0') + errx(EX_USAGE, "invalid duration"); + + switch (*end) { + case 's': + break; + case 'm': + ret *= 60; + break; + case 'h': + ret *= 60 * 60; + break; + case 'd': + ret *= 60 * 60 * 24; + break; + default: + errx(EX_USAGE, "invalid duration"); + } + + if (ret < 0 || ret >= 100000000UL) + errx(EX_USAGE, "invalid duration"); + + return (ret); +} + +static int +parse_signal(const char *str) +{ + char *ep; + int i; + long sig; + + if (strncasecmp(str, "SIG", 3) == 0) { + str += 3; + + for (i = 1; i < NSIG; i++) { + if (strcasecmp(str, sys_signame[i]) == 0) + return (i); + } + + goto err; + } + + errno = 0; + sig = strtol(str, &ep, 10); + + if (str[0] == '\0' || *ep != '\0') + goto err; + if (errno == ERANGE && (sig == LONG_MAX || sig == LONG_MIN)) + goto err; + if (sig >= NSIG || sig < 0) + goto err; + + return (int)sig; + +err: + errx(EX_USAGE, "invalid signal"); +} + +static void +sig_handler(int signo) +{ + if (sig_ign != 0 && signo == sig_ign) { + sig_ign = 0; + return; + } + + switch(signo) { + case 0: + case SIGINT: + case SIGHUP: + case SIGQUIT: + case SIGTERM: + sig_term = signo; + break; + case SIGCHLD: + sig_chld = 1; + break; + case SIGALRM: + sig_alrm = 1; + break; + } +} + +static void +set_interval(double iv) +{ + struct itimerval tim; + + memset(&tim, 0, sizeof(tim)); + tim.it_value.tv_sec = (time_t)iv; + iv -= (double)tim.it_value.tv_sec; + tim.it_value.tv_usec = (suseconds_t)(iv * 1000000UL); + + if (setitimer(ITIMER_REAL, &tim, NULL) == -1) + err(EX_OSERR, "setitimer()"); +} + +int +main(int argc, char **argv) +{ + int ch; + unsigned long i; + int foreground, preserve; + int error, pstat, status; + int killsig = SIGTERM; + pid_t pgid, pid, cpid; + double first_kill; + double second_kill; + bool timedout = false; + bool do_second_kill = false; + struct sigaction signals; + int signums[] = { + -1, + SIGTERM, + SIGINT, + SIGHUP, + SIGCHLD, + SIGALRM, + SIGQUIT, + }; + + setprogname(argv[0]); + + foreground = preserve = 0; + second_kill = 0; + cpid = -1; + pgid = -1; + + const struct option longopts[] = { + { "preserve-status", no_argument, &preserve, 1 }, + { "foreground", no_argument, &foreground, 1 }, + { "kill-after", required_argument, NULL, 'k'}, + { "signal", required_argument, NULL, 's'}, + { "help", no_argument, NULL, 'h'}, + { NULL, 0, NULL, 0 } + }; + + while ((ch = getopt_long(argc, argv, "+k:s:h", longopts, NULL)) != -1) { + switch (ch) { + case 'k': + do_second_kill = true; + second_kill = parse_duration(optarg); + break; + case 's': + killsig = parse_signal(optarg); + break; + case 0: + break; + case 'h': + default: + usage(); + break; + } + } + + argc -= optind; + argv += optind; + + if (argc < 2) + usage(); + + first_kill = parse_duration(argv[0]); + argc--; + argv++; + + if (!foreground) { + pgid = setpgid(0,0); + + if (pgid == -1) + err(EX_OSERR, "setpgid()"); + } + + memset(&signals, 0, sizeof(signals)); + sigemptyset(&signals.sa_mask); + + if (killsig != SIGKILL && killsig != SIGSTOP) + signums[0] = killsig; + + for (i = 0; i < sizeof(signums) / sizeof(signums[0]); i ++) + sigaddset(&signals.sa_mask, signums[i]); + + signals.sa_handler = sig_handler; + signals.sa_flags = SA_RESTART; + + for (i = 0; i < sizeof(signums) / sizeof(signums[0]); i ++) { + if (signums[i] != -1 && signums[i] != 0 && + sigaction(signums[i], &signals, NULL) == -1) + err(EX_OSERR, "sigaction()"); + } + + signal(SIGTTIN, SIG_IGN); + signal(SIGTTOU, SIG_IGN); + + pid = fork(); + if (pid == -1) + err(EX_OSERR, "fork()"); + else if (pid == 0) { + /* child process */ + signal(SIGTTIN, SIG_DFL); + signal(SIGTTOU, SIG_DFL); + + error = execvp(argv[0], argv); + if (error == -1) + err(EX_UNAVAILABLE, "exec()"); + } + + if (sigprocmask(SIG_BLOCK, &signals.sa_mask, NULL) == -1) + err(EX_OSERR, "sigprocmask()"); + + /* parent continues here */ + set_interval(first_kill); + + for (;;) { + sigemptyset(&signals.sa_mask); + sigsuspend(&signals.sa_mask); + + if (sig_chld) { + sig_chld = 0; + while (((cpid = wait(&status)) < 0) && errno == EINTR) + continue; + + if (cpid == pid) { + pstat = status; + break; + } + } else if (sig_alrm) { + sig_alrm = 0; + + timedout = true; + if (!foreground) + killpg(pgid, killsig); + else + kill(pid, killsig); + + if (do_second_kill) { + set_interval(second_kill); + second_kill = 0; + sig_ign = killsig; + killsig = SIGKILL; + } else + break; + + } else if (sig_term) { + if (!foreground) + killpg(pgid, killsig); + else + kill(pid, (int)sig_term); + + if (do_second_kill) { + set_interval(second_kill); + second_kill = 0; + sig_ign = killsig; + killsig = SIGKILL; + } else + break; + } + } + + while (cpid != pid && wait(&pstat) == -1) { + if (errno != EINTR) + err(EX_OSERR, "waitpid()"); + } + + if (WEXITSTATUS(pstat)) + pstat = WEXITSTATUS(pstat); + else if(WIFSIGNALED(pstat)) + pstat = 128 + WTERMSIG(pstat); + + if (timedout && !preserve) + pstat = EXIT_TIMEOUT; + + return (pstat); +} -- 2.20.1