bug-coreutils
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH] Add misc-utils/timeout utility


From: Pádraig Brady
Subject: [PATCH] Add misc-utils/timeout utility
Date: Fri, 7 Mar 2008 11:08:41 +0000
User-agent: Thunderbird 2.0.0.6 (X11/20071008)

Hi Karel,

I've attached a patch to add the "timeout" utility
which was previously discussed on coreutils and util-linux-ng lists.

I've tested this in a lot of situations and have
noted the following caveats:

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.

If one does Ctrl-Z then the timeout (and command) will be stopped
but the alarm timer keeps running, so that once the timeout process is
resumed it will exit if the timer has expired at this time.

If specify -s9 to kill timeout jobs with, then
exit code will not be ETIMEDOUT, just standard killed exit code.

cheers,
Pádraig.

p.s. I'll send on the misc-utils/truncate utility next
Should be a _lot_ easier to test than this one.
>From 95f14d6a09894bfcba034a281bccd46b9da05f1d Mon Sep 17 00:00:00 2001
From: =?utf-8?q?P=C3=A1draig=20Brady?= <address@hidden>
Date: Fri, 7 Mar 2008 10:48:34 +0000
Subject: [PATCH] Add the timeout utility


Signed-off-by: Pádraig Brady <address@hidden>
---
 misc-utils/timeout.1 |   31 ++++++++
 misc-utils/timeout.c |  188 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 219 insertions(+), 0 deletions(-)
 create mode 100644 misc-utils/timeout.1
 create mode 100644 misc-utils/timeout.c

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 (address@hidden)
+.\" 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 <address@hidden>.
+.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..f4bbd4b
--- /dev/null
+++ b/misc-utils/timeout.c
@@ -0,0 +1,188 @@
+/*
+ Copyright © 2008 Pádraig Brady <address@hidden>
+
+ 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 <signal.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <stdlib.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;
+
+    if (isdigit(*arg)) {
+        numsig = strtol(arg, &ep, 10);
+        if (*ep!='\0' || numsig<0 || numsig>=NSIG)
+            return -1;
+        return numsig;
+    }
+    return -1;
+}
+
+/* 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;
+
+    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);
+    }
+
+    char* ep;
+    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*/
+        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, ... */
+        enum { EXIT_CANNOT_INVOKE=126, EXIT_ENOENT=127 };
+        exit_status = (errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE);
+        perror(argv[0]);
+        return exit_status;
+    } else {
+        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. */
+        int status;
+        wait(&status);
+
+        if (WIFEXITED(status)) {
+            status = WEXITSTATUS(status);
+        } else if (WIFSIGNALED(status)) {
+            status = WTERMSIG(status)+128; /* what bash does at least */
+        }
+
+        if (timed_out) {
+            return ETIMEDOUT;
+        } else {
+            return status;
+        }
+    }
+}
-- 
1.5.3.6


reply via email to

[Prev in Thread] Current Thread [Next in Thread]