timeout command in python

I would like to add a timeout to any shell command such that if it does not complete within a specified number of seconds the command will exit. This would be useful for a any long-running command where I'd like it to die on its own rather than manually killing the long-running process.

There are several solutions to this, GNU coreutils provides a timeout command that does exactly like this, you could also use ulimit, or shell hacks such as

bash -c '(sleep 10; kill -9 $$) & exec command'

However, I'd like to give the program a chance to exit gracefully before forcibly killing it. In other words, in a UNIX environment I would like to first send a SIGTERM signal to the long-running process before issuing a SIGKILL. This way the command has a chance to properly close any network or I/O before exiting.

You could use the following python recipe, also available here

#!/usr/bin/env python
"""Usage: timeout.py [timeout] [command]
    timeout in seconds before killing [command]
    command is any command (and arguments) that you wish to timeout"""
import datetime, os, signal, subprocess, sys, time

# how long (in seconds) between sigterm and sigkill
SIGTERM_TO_SIGKILL = 1 

def timeout_command(cmd, timeout):
    start = datetime.datetime.now()
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    while process.poll() is None:
        now = datetime.datetime.now()
        if (now - start).seconds >= timeout:
            os.kill(process.pid, signal.SIGTERM)
            sys.stderr.write('TIMEOUT %s second%s, sent sigterm to %s %s\n' %(str(timeout), '' if timeout==1 else 's', process.pid, ' '.join(cmd)))
            time.sleep(SIGTERM_TO_SIGKILL)
            if process.poll() is None:
                os.kill(process.pid, signal.SIGKILL)
                sys.stderr.write('process still running, sent sigkill to %s %s\n' %(process.pid, ' '.join(cmd)))
                os.waitpid(-1, os.WNOHANG)
            return 2
        time.sleep(0.1)
    sys.stdout.write(process.stdout.read())
    sys.stderr.write(process.stderr.read())
    return 0

def main(argv=None):
    try:
        if "-h" in argv or "--help" in argv:
            print __doc__
            return 0
        return timeout_command(sys.argv[2:], int(argv[1]))
    except:
        print >>sys.stderr, __doc__
        return 2

if __name__ == '__main__':
    sys.exit(main(sys.argv))

This could be run as follows,

tim@laptop:~/bin :) timeout.py 2 sleep 3
TIMEOUT 2 seconds, sent sigterm to 9036 sleep 3

Or for the case where a SIGTERM was caught and ignored,

tim@laptop:~/bin :) timeout.py 2 sleeper_ignore_sigterm.py 
TIMEOUT 2 seconds, sent sigterm to 9060 sleeper_ignore_sigterm.py
process still running, sent sigkill to 9060 sleeper_ignore_sigterm.py
This entry was posted in python, shell tips. Bookmark the permalink.

Comments are closed.