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