From 265c4b83a81c643f3359be926ef8034f28f321ed Mon Sep 17 00:00:00 2001 From: Pádraig Brady Date: Fri, 28 Mar 2008 11:05:55 +0000 Subject: new program: timeout * AUTHORS: Register as the author. * NEWS: Mention this change. * README: Add timeout command to list. * src/timeout.c: New file. * src/kill.c (operand2sig): Move function to its own file, now that timeout.c will also use it. * src/operand2sig.c (operand2sig): New file, extracted from kill.c. * src/operand2sig.h (operand2sig): Declare. * src/Makefile.am (EXTRA_PROGRAMS): Add timeout. * src/.gitignore: Add timeout binary to list to ignore. * doc/coreutils.texi (timeout invocation): Add timeout info. (Signal specifications): New section, also referenced by kill. * man/Makefile.am (timeout.1): Add dependency. * man/timeout.x: New file. * po/POTFILES.in: Add timeout.c and operand2sig.c to list to translate. * tests/Makefile.am (TESTS): Add the two new tests. * tests/misc/help-version: Add support for new timeout command. * tests/misc/invalid-opt: Add support for new timeout command. * tests/misc/timeout: New file: check basic timeout operation. * tests/misc/timeout-parameters: New file: check invalid parameter combinations. --- src/.gitignore | 1 + src/Makefile.am | 5 +- src/kill.c | 47 +------- src/operand2sig.c | 84 ++++++++++++++ src/operand2sig.h | 18 +++ src/timeout.c | 335 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 443 insertions(+), 47 deletions(-) create mode 100644 src/operand2sig.c create mode 100644 src/operand2sig.h create mode 100644 src/timeout.c (limited to 'src') diff --git a/src/.gitignore b/src/.gitignore index 437dd30d4..cee550bc5 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -87,6 +87,7 @@ tac tail tee test +timeout touch tr true diff --git a/src/Makefile.am b/src/Makefile.am index 14906bc09..97dd33b6d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -39,7 +39,7 @@ EXTRA_PROGRAMS = \ basename date dirname echo env expr factor false \ id kill logname pathchk printenv printf pwd \ runcon seq sleep tee \ - test true tty whoami yes \ + test timeout true tty whoami yes \ base64 bin_PROGRAMS = $(OPTIONAL_BIN_PROGS) @@ -54,6 +54,7 @@ noinst_HEADERS = \ fs.h \ group-list.h \ ls.h \ + operand2sig.h \ prog-fprintf.h \ remove.h \ system.h \ @@ -233,6 +234,8 @@ ln_SOURCES = ln.c ls_SOURCES = ls.c ls-ls.c chown_SOURCES = chown.c chown-core.c chgrp_SOURCES = chgrp.c chown-core.c +kill_SOURCES = kill.c operand2sig.c +timeout_SOURCES = timeout.c operand2sig.c mv_SOURCES = mv.c remove.c $(copy_sources) rm_SOURCES = rm.c remove.c diff --git a/src/kill.c b/src/kill.c index d87c7d0e6..bd5d9b291 100644 --- a/src/kill.c +++ b/src/kill.c @@ -35,6 +35,7 @@ #include "system.h" #include "error.h" #include "sig2str.h" +#include "operand2sig.h" /* The official name of this program (e.g., no `g' prefix). */ #define PROGRAM_NAME "kill" @@ -118,52 +119,6 @@ PID is an integer; if negative it identifies a process group.\n\ exit (status); } -/* Convert OPERAND to a signal number with printable representation SIGNAME. - Return the signal number, or -1 if unsuccessful. */ - -static int -operand2sig (char const *operand, char *signame) -{ - int signum; - - if (ISDIGIT (*operand)) - { - char *endp; - long int l = (errno = 0, strtol (operand, &endp, 10)); - int i = l; - signum = (operand == endp || *endp || errno || i != l ? -1 - : WIFSIGNALED (i) ? WTERMSIG (i) - : i); - } - else - { - /* Convert signal to upper case in the C locale, not in the - current locale. Don't assume ASCII; it might be EBCDIC. */ - char *upcased = xstrdup (operand); - char *p; - for (p = upcased; *p; p++) - if (strchr ("abcdefghijklmnopqrstuvwxyz", *p)) - *p += 'A' - 'a'; - - /* Look for the signal name, possibly prefixed by "SIG", - and possibly lowercased. */ - if (! (str2sig (upcased, &signum) == 0 - || (upcased[0] == 'S' && upcased[1] == 'I' && upcased[2] == 'G' - && str2sig (upcased + 3, &signum) == 0))) - signum = -1; - - free (upcased); - } - - if (signum < 0 || sig2str (signum, signame) != 0) - { - error (0, 0, _("%s: invalid signal"), operand); - return -1; - } - - return signum; -} - /* Print a row of `kill -t' output. NUM_WIDTH is the maximum signal number width, and SIGNUM is the signal number to print. The maximum name width is NAME_WIDTH, and SIGNAME is the name to print. */ diff --git a/src/operand2sig.c b/src/operand2sig.c new file mode 100644 index 000000000..228db401c --- /dev/null +++ b/src/operand2sig.c @@ -0,0 +1,84 @@ +/* operand2sig.c -- common function for parsing signal specifications + Copyright (C) 2008 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Extracted from kill.c/timeout.c by Pádraig Brady. + FIXME: Move this to gnulib/str2sig.c */ + + +/* Convert OPERAND to a signal number with printable representation SIGNAME. + Return the signal number, or -1 if unsuccessful. */ + +#include +#include +#include +#include + +#if HAVE_SYS_WAIT_H +# include +#endif +#ifndef WIFSIGNALED +# define WIFSIGNALED(s) (((s) & 0xFFFF) - 1 < (unsigned int) 0xFF) +#endif +#ifndef WTERMSIG +# define WTERMSIG(s) ((s) & 0x7F) +#endif + +#include "system.h" +#include "error.h" +#include "sig2str.h" +#include "operand2sig.h" + +extern int +operand2sig (char const *operand, char *signame) +{ + int signum; + + if (ISDIGIT (*operand)) + { + char *endp; + long int l = (errno = 0, strtol (operand, &endp, 10)); + int i = l; + signum = (operand == endp || *endp || errno || i != l ? -1 + : WIFSIGNALED (i) ? WTERMSIG (i) : i); + } + else + { + /* Convert signal to upper case in the C locale, not in the + current locale. Don't assume ASCII; it might be EBCDIC. */ + char *upcased = xstrdup (operand); + char *p; + for (p = upcased; *p; p++) + if (strchr ("abcdefghijklmnopqrstuvwxyz", *p)) + *p += 'A' - 'a'; + + /* Look for the signal name, possibly prefixed by "SIG", + and possibly lowercased. */ + if (!(str2sig (upcased, &signum) == 0 + || (upcased[0] == 'S' && upcased[1] == 'I' && upcased[2] == 'G' + && str2sig (upcased + 3, &signum) == 0))) + signum = -1; + + free (upcased); + } + + if (signum < 0 || sig2str (signum, signame) != 0) + { + error (0, 0, _("%s: invalid signal"), operand); + return -1; + } + + return signum; +} diff --git a/src/operand2sig.h b/src/operand2sig.h new file mode 100644 index 000000000..165e6cbb8 --- /dev/null +++ b/src/operand2sig.h @@ -0,0 +1,18 @@ +/* operand2sig.h -- prototype for signal specification function + + Copyright (C) 2008 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +extern int operand2sig (char const *operand, char *signame); diff --git a/src/timeout.c b/src/timeout.c new file mode 100644 index 000000000..19ec8499a --- /dev/null +++ b/src/timeout.c @@ -0,0 +1,335 @@ +/* timeout -- run a command with bounded time + Copyright (C) 2008 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + + +/* timeout - Start a command, and kill it if the specified timeout expires + + We try to behave like a shell starting a single (foreground) job, + and will kill the job if we receive the alarm signal we setup. + The exit status of the job is returned, or one of these errors: + ETIMEDOUT 110 job timed out + ECANCELED 125 internal error + EXIT_CANNOT_INVOKE 126 error executing job + EXIT_ENOENT 127 couldn't find job to exec + + Caveats: + If user specifies the KILL (9) signal is to be sent on timeout, + the monitor is killed and so exits with 128+9 rather than ETIMEDOUT. + + If you start a command in the background, which reads from the tty + and so is immediately sent SIGTTIN to stop, then the timeout + process will ignore this so it can timeout the command as expected. + This can be seen with `timeout 10 dd&` for example. + However if one brings this group to the foreground with the `fg` + command before the timer expires, the command will remain + in the sTop state as the shell doesn't send a SIGCONT + because the timeout process (group leader) is already running. + To get the command running again one can Ctrl-Z, and do fg again. + Note one can Ctrl-C the whole job when in this state. + I think this could be fixed but I'm not sure the extra + complication is justified for this scenario. + + Written by Pádraig Brady. */ + +#include +#include +#include +#include +#include + +#if HAVE_SYS_WAIT_H +# include +#endif +#ifndef WIFSIGNALED +# define WIFSIGNALED(s) (((s) & 0xFFFF) - 1 < (unsigned int) 0xFF) +#endif +#ifndef WTERMSIG +# define WTERMSIG(s) ((s) & 0x7F) +#endif + +#include "system.h" +#include "xstrtol.h" +#include "sig2str.h" +#include "operand2sig.h" +#include "cloexec.h" +#include "error.h" +#include "long-options.h" +#include "quote.h" + +#define PROGRAM_NAME "timeout" + +#define AUTHORS proper_name_utf8 ("Padraig Brady", "P\303\241draig Brady") + +/* Internal failure. */ +#ifndef ECANCELED +#define ECANCELED 125 +#endif + +static int timed_out; +static int term_signal = SIGTERM; /* same default as kill command. */ +static int monitored_pid; +static int sigs_to_ignore[NSIG]; /* so monitor can ignore sigs it resends. */ +static char *program_name; + +static struct option const long_options[] = { + {"signal", required_argument, NULL, 's'}, + {NULL, 0, NULL, 0} +}; + +/* send sig to group but not ourselves. + * FIXME: Is there a better way to achieve this? */ +static int +send_sig (int where, int sig) +{ + sigs_to_ignore[sig] = 1; + return kill (where, sig); +} + +static void +cleanup (int sig) +{ + if (sig == SIGALRM) + { + timed_out = 1; + sig = term_signal; + } + if (monitored_pid) + { + if (sigs_to_ignore[sig]) + { + sigs_to_ignore[sig] = 0; + return; + } + send_sig (0, sig); + if (sig != SIGKILL && sig != SIGCONT) + send_sig (0, SIGCONT); + } + else /* we're the child or the child is not exec'd yet. */ + _exit (128 + sig); +} + +static void +usage (int status) +{ + if (status != EXIT_SUCCESS) + fprintf (stderr, _("Try `%s --help' for more information.\n"), + program_name); + else + { + printf (_("\ +Usage: %s [OPTION] NUMBER[SUFFIX] COMMAND [ARG]...\n\ + or: %s [OPTION]\n"), program_name, program_name); + + fputs (_("\ +Start COMMAND, and kill it if still running after NUMBER seconds.\n\ +SUFFIX may be `s' for seconds (the default), `m' for minutes,\n\ +`h' for hours or `d' for days.\n\ +\n\ +"), stdout); + + fputs (_("\ +Mandatory arguments to long options are mandatory for short options too.\n\ +"), stdout); + fputs (_("\ + -s, --signal=SIGNAL\n\ + specify the signal to be sent on timeout.\n\ + SIGNAL may be a name like `HUP' or a number.\n\ + See `kill -l` for a list of signals\n"), stdout); + + fputs (HELP_OPTION_DESCRIPTION, stdout); + fputs (VERSION_OPTION_DESCRIPTION, stdout); + fputs (_("\n\ +If the command times out, then we exit with status ETIMEDOUT,\n\ +otherwise the normal exit status of the command is returned.\n\ +If no signal is specified, the TERM signal is sent. The TERM signal\n\ +will kill processes which do not catch this signal. For other processes,\n\ +it may be necessary to use the KILL (9) signal, since this signal cannot\n\ +be caught.\n"), stdout); + emit_bug_reporting_address (); + } + exit (status); +} + +/* Given an integer value *X, and a suffix character, SUFFIX_CHAR, + scale *X by the multiplier implied by SUFFIX_CHAR. SUFFIX_CHAR may + be the NUL byte or `s' to denote seconds, `m' for minutes, `h' for + hours, or `d' for days. If SUFFIX_CHAR is invalid, don't modify *X + and return false. If *X would overflow, don't modify *X and return false. + Otherwise return true. */ + +static bool +apply_time_suffix (unsigned int *x, char suffix_char) +{ + int multiplier=1; + + switch (suffix_char) + { + case 0: + case 's': + return true; + case 'd': + multiplier *= 24; + case 'h': + multiplier *= 60; + case 'm': + multiplier *= 60; + break; + default: + return false; + } + + if (*x > UINT_MAX / multiplier) + return false; + + *x *= multiplier; + + return true; +} + +static void +install_signal_handlers (void) +{ + struct sigaction sa; + sigemptyset(&sa.sa_mask); /* Allow concurrent calls to handler */ + sa.sa_handler = cleanup; + sa.sa_flags = SA_RESTART; /* restart syscalls (like wait() below) */ + + sigaction (SIGALRM, &sa, NULL); /* our timeout. */ + sigaction (SIGINT, &sa, NULL); /* Ctrl-C at terminal for example. */ + sigaction (SIGQUIT, &sa, NULL); /* Ctrl-\ at terminal for example. */ + sigaction (SIGTERM, &sa, NULL); /* if we're killed, stop monitored proc. */ + sigaction (SIGHUP, &sa, NULL); /* terminal closed for example. */ +} + +int +main (int argc, char **argv) +{ + unsigned long timeout; + char signame[SIG2STR_MAX]; + int c; + char *ep; + + initialize_main (&argc, &argv); + program_name = argv[0]; + setlocale (LC_ALL, ""); + bindtextdomain (PACKAGE, LOCALEDIR); + textdomain (PACKAGE); + + initialize_exit_failure (ECANCELED); + atexit (close_stdout); + + parse_long_options (argc, argv, PROGRAM_NAME, PACKAGE_NAME, VERSION, + usage, AUTHORS, (char const *) NULL); + + while ((c = getopt_long (argc, argv, "+s:", long_options, NULL)) != -1) + { + switch (c) + { + case 's': + term_signal = operand2sig (optarg, signame); + if (term_signal == -1) + usage (ECANCELED); + break; + default: + usage (ECANCELED); + break; + } + } + + if (argc - optind < 2) + usage (ECANCELED); + + if (xstrtoul (argv[optind], &ep, 10, &timeout, NULL) + /* Invalid interval. Note 0 disables timeout */ + || (timeout > UINT_MAX) + /* Extra chars after the number and an optional s,m,h,d char. */ + || (*ep && *(ep + 1)) + /* Check any suffix char and update timeout based on the suffix. */ + || !apply_time_suffix ((unsigned int *) &timeout, *ep)) + { + error (0, 0, _("invalid time interval %s"), quote (argv[optind])); + usage (ECANCELED); + } + optind++; + + argc -= optind; + argv += optind; + + /* Ensure we're in our own group so all subprocesses can be killed. + * Note we don't put the just child in a separate group as + * then we would need to worry about foreground and background groups + * and propagating signals between them. */ + setpgid (0, 0); + + /* Setup handlers before fork() so that we + * handle any signals caused by child, without races. */ + install_signal_handlers (); + signal (SIGTTIN, SIG_IGN); /* don't sTop if background child needs tty. */ + signal (SIGTTOU, SIG_IGN); /* don't sTop if background child needs tty. */ + + monitored_pid = fork (); + if (monitored_pid == -1) + { + perror ("fork"); + return errno; + } + else if (monitored_pid == 0) + { /* child */ + int exit_status; + + /* exec doesn't reset SIG_IGN -> SIG_DFL. */ + signal (SIGTTIN, SIG_DFL); + signal (SIGTTOU, SIG_DFL); + + execvp (argv[0], argv); /* FIXME: should we use "sh -c" ... here? */ + + /* exit like sh, env, nohup, ... */ + exit_status = (errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE); + perror (argv[0]); + return exit_status; + } + else + { + int status; + + alarm ((unsigned int) timeout); + + /* We're just waiting for a single process here, so wait() suffices. + * Note the signal() calls above on linux and BSD at least, essentially + * call the lower level sigaction() with the SA_RESTART flag set, which + * ensures the following wait call will only return if the child exits, + * not on this process receiving a signal. Also we're not passing + * WUNTRACED | WCONTINUED to a waitpid() call and so will not get + * indication that the child has stopped or continued. */ + wait (&status); + + if (WIFEXITED (status)) + status = WEXITSTATUS (status); + else if (WIFSIGNALED (status)) + status = WTERMSIG (status) + 128; /* what sh does at least. */ + + if (timed_out) + return ETIMEDOUT; + else + return status; + } +} + +/* + * Local variables: + * indent-tabs-mode: nil + * End: + */ -- cgit v1.2.3-54-g00ecf