Command line command to auto-kill a command after a certain amount of time

Viewed 31641

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.]

15 Answers

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.

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).

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.

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.

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.

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
}

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 "$@"
Related