Li Zefan wrote: >> +/* TODO: merge with kill.c */ >> +int arg_to_signum (char *arg) >> +{ >> + int numsig; >> + char *ep; >> + >> + if (isdigit(*arg)) { > > This check is unnecessary. removed. thanks. > [..snip..] > >> + } else { >> + alarm(timeout); >> + >> + int status; > > Move this declaration above 'alarm(timeout)' ? > >> + wait(&status); OK, made source fully ansi compliant. Now compiles with: gcc -std=c89 -pedantic -Werror -D_GNU_SOURCE timeout.c -o timeout Previous patch was also missing update to Makefile.am. Ammended patch is attached. thanks, Pádraig.
>From 7631c1188818ffb03e8f65821fe95a6acc0708af Mon Sep 17 00:00:00 2001 From: =?utf-8?q?P=C3=A1draig=20Brady?= <P@xxxxxxxxxxxxxx> Date: Fri, 7 Mar 2008 10:48:34 +0000 Subject: [PATCH] Add the timeout utility Signed-off-by: Pádraig Brady <P@xxxxxxxxxxxxxx> --- misc-utils/Makefile.am | 4 +- misc-utils/timeout.1 | 31 ++++++++ misc-utils/timeout.c | 186 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 misc-utils/timeout.1 create mode 100644 misc-utils/timeout.c diff --git a/misc-utils/Makefile.am b/misc-utils/Makefile.am index b799462..f0e20de 100644 --- a/misc-utils/Makefile.am +++ b/misc-utils/Makefile.am @@ -5,7 +5,7 @@ EXTRA_DIST = README.flushb bin_PROGRAMS = usrbinexec_PROGRAMS = cal ddate logger look mcookie \ - namei script whereis + namei script timeout whereis EXTRA_DIST += README.cal README.ddate README.namei README.namei2 mcookie_SOURCES = mcookie.c ../lib/md5.c @@ -15,7 +15,7 @@ usrbinexec_SCRIPTS = chkdupexe scriptreplay CLEANFILES = chkdupexe scriptreplay dist_man_MANS = cal.1 chkdupexe.1 ddate.1 logger.1 look.1 mcookie.1 \ - namei.1 script.1 whereis.1 scriptreplay.1 + namei.1 script.1 scriptreplay.1 timeout.1 whereis.1 if HAVE_TINFO cal_LDADD = -ltinfo -lncurses diff --git a/misc-utils/timeout.1 b/misc-utils/timeout.1 new file mode 100644 index 0000000..1a60791 --- /dev/null +++ b/misc-utils/timeout.1 @@ -0,0 +1,31 @@ +.\" Copyright 2008 Padraig Brady (P@xxxxxxxxxxxxxx) +.\" May be distributed under the GNU General Public License +.TH TIMEOUT 1 "7 March 2008" "Linux Utilities" "Linux Programmer's Manual" +.SH NAME +timeout \- Start a command, and kill it if the timeout expires +.SH SYNOPSIS +.BI "timeout [ \-s " signal " ] " timeout " " command " [ " args... " ] +.SH DESCRIPTION +The command +.B timeout +starts the command and will kill it after timeout seconds have passed. +If the command times out, then we exit with status ETIMEDOUT, +otherwise the normal exit status of the command is returned. +If no signal is specified, the TERM signal is sent. The TERM signal +will kill processes which do not catch this signal. For other processes, +it may be necessary to use the KILL (9) signal, since this signal cannot +be caught. +.SH OPTIONS +.TP +.IR timeout ... +Specify the timeout in seconds +.TP +.BI \-s " signal " +Specify the signal number to send. +.SH "SEE ALSO" +.BR kill (1), +.SH AUTHOR +Padraig Brady <P@xxxxxxxxxxxxxx>. +.SH AVAILABILITY +The timeout command is part of the util-linux-ng package and is available from +ftp://ftp.kernel.org/pub/linux/utils/util-linux-ng/. diff --git a/misc-utils/timeout.c b/misc-utils/timeout.c new file mode 100644 index 0000000..577f067 --- /dev/null +++ b/misc-utils/timeout.c @@ -0,0 +1,186 @@ +/* + Copyright © 2008 Pádraig Brady <P@xxxxxxxxxxxxxx> + + 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 2 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, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +/* + 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. + If the jobs times out, then we exit with status ETIMEDOUT, + otherwise the normal exit status of the job is returned. +*/ + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <signal.h> +#include <errno.h> +#include <ctype.h> +#include <limits.h> + +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 */ + +/* TODO: merge with kill.c */ +int arg_to_signum (char *arg) +{ + int numsig; + char *ep; + + numsig = strtol(arg, &ep, 10); + if (*ep!='\0' || numsig<0 || numsig>=NSIG) + return -1; + return numsig; +} + +/* send sig to group but not ourselves. + * ??? Is there a better way to achieve this. */ +int send_sig(int where, int sig) +{ + sigs_to_ignore[sig]=1; + return kill(where, sig); +} + +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); + } +} + +void usage(char** argv) +{ + fprintf(stderr, "Usage: %s [-s signal number] seconds command\n", argv[0]); + fprintf(stderr, "Start a command, and kill it if the timeout expires\n\n"); + + fprintf(stderr, " -s send signal number to command (default=TERM)\n"); + fprintf(stderr, " See `kill -l` for a list of signal numbers\n"); + exit(EINVAL); +} + +int main(int argc, char** argv) +{ + unsigned int timeout; + int c; + char* ep; + + while ((c = getopt(argc, argv, "+s:")) != -1) { + switch (c) { + case 's': + term_signal = arg_to_signum(optarg); + if (term_signal == -1) { + fprintf(stderr, "Invalid signal number: %s\n", optarg); + usage(argv); + } + break; + default: + usage(argv); + break; + } + } + + if (argc-optind < 2) { + usage(argv); + } + + timeout = strtoul(argv[optind++], &ep, 10); + if (*ep!='\0' || !timeout || timeout==ULONG_MAX) { + usage(argv); + } + + 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 */ + signal(SIGALRM, cleanup); /* our timeout */ + signal(SIGINT, cleanup); /* Ctrl-C at terminal for example */ + signal(SIGQUIT, cleanup); /* Ctrl-\ at terminal for example */ + signal(SIGTERM, cleanup); /* if we're killed, stop monitored process */ + signal(SIGHUP, cleanup); /* terminal closed for example */ + 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*/ + enum { EXIT_CANNOT_INVOKE=126, EXIT_ENOENT=127 }; + int exit_status; + + /* exec doesn't reset SIG_IGN -> SIG_DFL */ + signal(SIGTTIN, SIG_DFL); + signal(SIGTTOU, SIG_DFL); + + execvp(argv[0], argv); /* ??? should we just 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(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; + } + } +} -- 1.5.3.6