60

I'd like to automatically kill a command after a certain amount of time. I have in mind an interface like this:

% constrain 300 ./foo args

Which would run "./foo" with "args" but automatically kill it if it's still running after 5 minutes.

It might be useful to generalize the idea to other constraints, such as autokilling a process if it uses too much memory.

Are there any existing tools that do that, or has anyone written such a thing?

ADDED: Jonathan's solution is precisely what I had in mind and it works like a charm on linux, but I can't get it to work on Mac OSX. I got rid of the SIGRTMIN which lets it compile fine, but the signal just doesn't get sent to the child process. Anyone know how to make this work on Mac?

[Added: Note that an update is available from Jonathan that works on Mac and elsewhere.]

5

15 Answers 15

53

GNU Coreutils includes the timeout command, installed by default on many systems.

https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html

To watch free -m for one minute, then kill it by sending a TERM signal:

timeout 1m watch free -m
2
41

Maybe I'm not understanding the question, but this sounds doable directly, at least in bash:

( /path/to/slow command with options ) & sleep 5 ; kill $!

This runs the first command, inside the parenthesis, for five seconds, and then kills it. The entire operation runs synchronously, i.e. you won't be able to use your shell while it is busy waiting for the slow command. If that is not what you wanted, it should be possible to add another &.

The $! variable is a Bash builtin that contains the process ID of the most recently started subshell. It is important to not have the & inside the parenthesis, doing it that way loses the process ID.

3
  • Very smart! I didn't know bash could do that. Anyone know of a similar trick for tcsh?
    – dreeves
    Commented Mar 12, 2009 at 20:52
  • I've asked a follow-up to your nifty answer at stackoverflow.com/questions/687948/… Commented Mar 26, 2009 at 23:34
  • 6
    Very interesting, but there is a race condition if the process terminates early and the PID is reused for another, unrelated process of the same user. Now I have to read the answers to system PAUSE's follow-up and see if they have the same problem. Commented May 31, 2011 at 17:49
30

I've arrived rather late to this party, but I don't see my favorite trick listed in the answers.

Under *NIX, an alarm(2) is inherited across an execve(2) and SIGALRM is fatal by default. So, you can often simply:

$ doalarm () { perl -e 'alarm shift; exec @ARGV' "$@"; } # define a helper function

$ doalarm 300 ./foo.sh args

or install a trivial C wrapper to do that for you.

Advantages Only one PID is involved, and the mechanism is simple. You won't kill the wrong process if, for example, ./foo.sh exited "too quickly" and its PID was re-used. You don't need several shell subprocesses working in concert, which can be done correctly but is rather race-prone.

Disadvantages The time-constrained process cannot manipulate its alarm clock (e.g., alarm(2), ualarm(2), setitimer(2)), since this would likely clear the inherited alarm. Obviously, neither can it block or ignore SIGALRM, though the same can be said of SIGINT, SIGTERM, etc. for some other approaches.

Some (very old, I think) systems implement sleep(2) in terms of alarm(2), and, even today, some programmers use alarm(2) as a crude internal timeout mechanism for I/O and other operations. In my experience, however, this technique is applicable to the vast majority of processes you want to time limit.

7
  • I tried this and it works beautifully. This may be my new favorite.
    – dreeves
    Commented Jul 11, 2010 at 17:59
  • I have been using the perl script one liner. I have encountered an odd difficulty: it does not seem to work when the command is a bash function. doalarm 7200 echo "Cool" works perfectly, testfun () { echo $1 } ; testfun "Cool" works perfectly, but doalarm 7200 testfun "Cool" does not work. Thanks for any suggestions. Commented May 17, 2013 at 19:25
  • 1
    @EtienneLow-Décarie, yes, the this style of wrapper will only work on commands that are exec()d as separate processes. So, shell functions and builtins won't be doalarm-able.
    – pilcrow
    Commented May 17, 2013 at 20:54
  • Thanks @pilcrow! Does this produce a variable that holds a value depending on wether the process was killed after timeout or wether the process was complete? Commented May 21, 2013 at 19:14
  • 1
    @EtienneLow-Décarie, the short answer is that the $? in shell will encode exit status. Signal death is $? & 127, which, for example, is 14 on my system for death by SIGALRM. Ordinary exit, success or failure, is $? >> 8 presuming no death by signal.
    – pilcrow
    Commented May 21, 2013 at 19:20
13

There is also ulimit, which can be used to limit the execution time available to sub-processes.

ulimit -t 10

Limits the process to 10 seconds of CPU time.

To actually use it to limit a new process, rather than the current process, you may wish to use a wrapper script:

#! /usr/bin/env python

import os
os.system("ulimit -t 10; other-command-here")

other-command can be any tool. I was running a Java, Python, C and Scheme versions of different sorting algorithms, and logging how long they took, whilst limiting execution time to 30 seconds. A Cocoa-Python application generated the various command lines - including the arguments - and collated the times into a CSV file, but it was really just fluff on top of the command provided above.

3
  • Aha, I had forgotten about ulimit. But is there a way to apply that to a specific process? For example, can what Jonathan did be done more simply using ulimit?
    – dreeves
    Commented Mar 2, 2009 at 16:06
  • I haven't used ulimit for ages. What I did was have a python script that ran another tool: os.system("ulimit -t 600; gtime -f <command> java -Xms1024m -Xmx1024m Main") Commented Mar 3, 2009 at 1:20
  • Thanks Matthew! Want to incorporate that into your answer? Looks like a nice quick-and-dirty alternative to Jonathan's C program. Could even wrap that in a perl/python/shell script and have the same command line interface. What would be the advantages/disadvantages of that over Jonathon's solution?
    – dreeves
    Commented Mar 3, 2009 at 23:00
13

I have a program called timeout that does that - written in C, originally in 1989 but updated periodically since then.


Update: this code fails to compile on MacOS X because SIGRTMIN is not defined, and fails to timeout when run on MacOS X because the `signal()` function there resumes the `wait()` after the alarm times out - which is not the required behaviour. I have a new version of `timeout.c` which deals with both these problems (using `sigaction()` instead of `signal()`). As before, contact me for a 10K gzipped tar file with the source code and a manual page (see my profile).
/*
@(#)File:           $RCSfile: timeout.c,v $
@(#)Version:        $Revision: 4.6 $
@(#)Last changed:   $Date: 2007/03/01 22:23:02 $
@(#)Purpose:        Run command with timeout monitor
@(#)Author:         J Leffler
@(#)Copyright:      (C) JLSS 1989,1997,2003,2005-07
*/

#define _POSIX_SOURCE       /* Enable kill() in <unistd.h> on Solaris 7 */
#define _XOPEN_SOURCE 500

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "stderr.h"

#define CHILD       0
#define FORKFAIL    -1

static const char usestr[] = "[-vV] -t time [-s signal] cmd [arg ...]";

#ifndef lint
/* Prevent over-aggressive optimizers from eliminating ID string */
const char jlss_id_timeout_c[] = "@(#)$Id: timeout.c,v 4.6 2007/03/01 22:23:02 jleffler Exp $";
#endif /* lint */

static void catcher(int signum)
{
    return;
}

int main(int argc, char **argv)
{
    pid_t   pid;
    int     tm_out;
    int     kill_signal;
    pid_t   corpse;
    int     status;
    int     opt;
    int     vflag = 0;

    err_setarg0(argv[0]);

    opterr = 0;
    tm_out = 0;
    kill_signal = SIGTERM;
    while ((opt = getopt(argc, argv, "vVt:s:")) != -1)
    {
        switch(opt)
        {
        case 'V':
            err_version("TIMEOUT", &"@(#)$Revision: 4.6 $ ($Date: 2007/03/01 22:23:02 $)"[4]);
            break;
        case 's':
            kill_signal = atoi(optarg);
            if (kill_signal <= 0 || kill_signal >= SIGRTMIN)
                err_error("signal number must be between 1 and %d\n", SIGRTMIN - 1);
            break;
        case 't':
            tm_out = atoi(optarg);
            if (tm_out <= 0)
                err_error("time must be greater than zero (%s)\n", optarg);
            break;
        case 'v':
            vflag = 1;
            break;
        default:
            err_usage(usestr);
            break;
        }
    }

    if (optind >= argc || tm_out == 0)
        err_usage(usestr);

    if ((pid = fork()) == FORKFAIL)
        err_syserr("failed to fork\n");
    else if (pid == CHILD)
    {
        execvp(argv[optind], &argv[optind]);
        err_syserr("failed to exec command %s\n", argv[optind]);
    }

    /* Must be parent -- wait for child to die */
    if (vflag)
        err_remark("time %d, signal %d, child PID %u\n", tm_out, kill_signal, (unsigned)pid);
    signal(SIGALRM, catcher);
    alarm((unsigned int)tm_out);
    while ((corpse = wait(&status)) != pid && errno != ECHILD)
    {
        if (errno == EINTR)
        {
            /* Timed out -- kill child */
            if (vflag)
                err_remark("timed out - send signal %d to process %d\n", (int)kill_signal, (int)pid);
            if (kill(pid, kill_signal) != 0)
                err_syserr("sending signal %d to PID %d - ", kill_signal, pid);
            corpse = wait(&status);
            break;
        }
    }

    alarm(0);
    if (vflag)
    {
        if (corpse == (pid_t) -1)
            err_syserr("no valid PID from waiting - ");
        else
            err_remark("child PID %u status 0x%04X\n", (unsigned)corpse, (unsigned)status);
    }

    if (corpse != pid)
        status = 2; /* I don't know what happened! */
    else if (WIFEXITED(status))
        status = WEXITSTATUS(status);
    else if (WIFSIGNALED(status))
        status = WTERMSIG(status);
    else
        status = 2; /* I don't know what happened! */

    return(status);
}

If you want the 'official' code for 'stderr.h' and 'stderr.c', contact me (see my profile).

2
  • Jonathan, thanks so much! This works beautifully on linux but doesn't work on Mac OSX for me. It compiles (after removing the SIGRTMIN) and seems to be working but then doesn't actually send the signal.
    – dreeves
    Commented Mar 2, 2009 at 8:50
  • 2
    For those too shy to contact Jonathan, he said I was welcome to host the code, so here it is: yootles.com/outbox/timeout-4.09.tgz Thanks again, Jonathan, for making this available! (Also from him: "Incidentally, I think the sigaction() fix was also needed for Cygwin.")
    – dreeves
    Commented Mar 3, 2009 at 17:40
4

Perl one liner, just for kicks:

perl -e '$s = shift; $SIG{ALRM} = sub { print STDERR "Timeout!\n"; kill INT => $p }; exec(@ARGV) unless $p = fork; alarm $s; waitpid $p, 0' 10 yes foo

This prints 'foo' for ten seconds, then times out. Replace '10' with any number of seconds, and 'yes foo' with any command.

2
  • Nice! We now have three ways to do this: Jonathan's C program, Matthew's ulimit trick, and this perl script (similar conceptually to Jonathan's program). Any thoughts on advantages and disadvantages of the different solutions?
    – dreeves
    Commented Mar 11, 2009 at 20:58
  • Well, ulimit different. I think it limits CPU-time rather than wall-clock-time, which could be weird: what happens if you peg both processors of a two-core box? or if you sleep and yield CPU time? Jonathan's program and mine are similar. His does exit status right, mine doesn't need a compiler.
    – vasi
    Commented Mar 12, 2009 at 8:35
4

The timeout command from Ubuntu/Debian when compiled from source to work on the Mac. Darwin

10.4.*

http://packages.ubuntu.com/lucid/timeout

4

My variation on the perl one-liner gives you the exit status without mucking with fork() and wait() and without the risk of killing the wrong process:

#!/bin/sh
# Usage: timelimit.sh secs cmd [ arg ... ]
exec perl -MPOSIX -e '$SIG{ALRM} = sub { print "timeout: @ARGV\n"; kill(SIGTERM, -$$); }; alarm shift; $exit = system @ARGV; exit(WIFEXITED($exit) ? WEXITSTATUS($exit) : WTERMSIG($exit));' "$@"

Basically the fork() and wait() are hidden inside system(). The SIGALRM is delivered to the parent process which then kills itself and its child by sending SIGTERM to the whole process group (-$$). In the unlikely event that the child exits and the child's pid gets reused before the kill() occurs, this will NOT kill the wrong process because the new process with the old child's pid will not be in the same process group of the parent perl process.

As an added benefit, the script also exits with what is probably the correct exit status.

1
  • I have a question for you. When I call timelimit.sh secs cmd [args] via bash on the mac os it works as intended and kills the processes launched by cmd [args] if they still exist after 5 seconds. However when I call timelimit.sh via Python using the commands os.system or the subprocess module, it does not kill the processes launched by cmd [args]. Any idea why? Commented Jul 16, 2018 at 15:50
4
#!/bin/sh
( some_slow_task ) & pid=$!
( sleep $TIMEOUT && kill -HUP $pid ) 2>/dev/null & watcher=$!
wait $pid 2>/dev/null && pkill -HUP -P $watcher

The watcher kills the slow task after given timeout; the script waits for the slow task and terminates the watcher.

Examples:

  • The slow task run more than 2 sec and was terminated

Slow task interrupted

( sleep 20 ) & pid=$!
( sleep 2 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
    echo "Slow task finished"
    pkill -HUP -P $watcher
    wait $watcher
else
    echo "Slow task interrupted"
fi
  • This slow task finished before the given timeout

Slow task finished

( sleep 2 ) & pid=$!
( sleep 20 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
    echo "Slow task finished"
    pkill -HUP -P $watcher
    wait $watcher
else
    echo "Slow task interrupted"
fi
1
  • This is better than the perl version as it's a hit or miss whether SIGALARM works or not.
    – user239558
    Commented Oct 30, 2018 at 9:24
2

Try something like:

# This function is called with a timeout (in seconds) and a pid.
# After the timeout expires, if the process still exists, it attempts
# to kill it.
function timeout() {
    sleep $1
    # kill -0 tests whether the process exists
    if kill -0 $2 > /dev/null 2>&1 ; then
        echo "killing process $2"
        kill $2 > /dev/null 2>&1
    else
        echo "process $2 already completed"
    fi
}

<your command> &
cpid=$!
timeout 3 $cpid
wait $cpid > /dev/null 2>&
exit $?

It has the downside that if your process' pid is reused within the timeout, it may kill the wrong process. This is highly unlikely, but you may be starting 20000+ processes per second. This could be fixed.

2

How about using the expect tool?

## run a command, aborting if timeout exceeded, e.g. timed-run 20 CMD ARGS ...
timed-run() {
  # timeout in seconds
  local tmout="$1"
  shift
  env CMD_TIMEOUT="$tmout" expect -f - "$@" <<"EOF"
# expect script follows
eval spawn -noecho $argv
set timeout $env(CMD_TIMEOUT)
expect {
   timeout {
      send_error "error: operation timed out\n"
      exit 1
   }
   eof
}
EOF
}
1
  • This feels like overkill but is still a very useful answer, especially as a template for something more elaborate where Expect is really needed. Thanks!
    – dreeves
    Commented Sep 23, 2010 at 11:02
2

pure bash:


#!/bin/bash

if [[ $# < 2 ]]; then
  echo "Usage: $0 timeout cmd [options]"
  exit 1
fi

TIMEOUT="$1"
shift

BOSSPID=$$

(
  sleep $TIMEOUT
  kill -9 -$BOSSPID
)&
TIMERPID=$!

trap "kill -9 $TIMERPID" EXIT

eval "$@"
2

I use "timelimit", which is a package available in the debian repository.

http://devel.ringlet.net/sysutils/timelimit/

1

A slight modification of the perl one-liner will get the exit status right.

perl -e '$s = shift; $SIG{ALRM} = sub { print STDERR "Timeout!\n"; kill INT => $p; exit 77 }; exec(@ARGV) unless $p = fork; alarm $s; waitpid $p, 0; exit ($? >> 8)' 10 yes foo

Basically, exit ($? >> 8) will forward the exit status of the subprocess. I just chose 77 at the exit status for timeout.

1

Isn't there a way to set a specific time with "at" to do this?

$ at 05:00 PM kill -9 $pid

Seems a lot simpler.

If you don't know what the pid number is going to be, I assume there's a way to script reading it with ps aux and grep, but not sure how to implement that.

$   | grep someprogram
tony     11585  0.0  0.0   3116   720 pts/1    S+   11:39   0:00 grep someprogram
tony     22532  0.0  0.9  27344 14136 ?        S    Aug25   1:23 someprogram

Your script would have to read the pid and assign it a variable. I'm not overly skilled, but assume this is doable.

Not the answer you're looking for? Browse other questions tagged or ask your own question.