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