Skip to content

Instantly share code, notes, and snippets.

@devietti
Created December 28, 2011 07:29
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save devietti/1526975 to your computer and use it in GitHub Desktop.
Save devietti/1526975 to your computer and use it in GitHub Desktop.
Python script+module to run a command with a timeout. A signal (SIGKILL by default) is sent to the command if the timeout expires.
#!/usr/bin/env python
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
## You should have received a copy of the GNU General Public License
## along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Usage: ./timeout.py TIMEOUT COMMAND...
This is a shell script that runs a command for up to TIMEOUT seconds, sending
the command a SIGKILL once the timeout expires. If the command exits sooner,
then this script will exit as well.
This functionality is also exposed as a Python module. The TimerTask class
handles running a command in a child process. There are many modules in the
Python standard library that do this, however. Here's what's different about
TimerTask:
You can set a timeout such that TimerTask.wait() returns when either 1) the
child process exits naturally or 2) the timeout expires and the child process
receives a specified signal causing it to terminate. TimerTask.wait() returns
whenever the *sooner* of 1) or 2) happens.
TimerTask uses the SIGALRM signal for its timeout, and so will interfere with
programs that need SIGALRM for other purposes. If you do not specify the use of
a timeout, however, no signal handling will be used.
Finally, the child process is, by default, run in its own process group, to make
it easier to clean up the child process along with any other processes it may
have spawned.
"""
import errno,os,signal,subprocess,sys
class TimerTask:
def __init__( self, command, timeout=None,
timeoutSignal=signal.SIGKILL, raisePrevSigalrmError=True,
childProcessGroup=0 ):
"""Create a new TimerTask
command : string : the command to run
timeout : int : timeout (in seconds), or None for no timeout (default: None)
signal : int : the signal to send to the child process at timeout (default: SIGKILL)
raisePrevSigalrmError : bool : if True, throw an exception if there's
a previous non-nop SIGALRM handler installed (default: True)
childProcessGroup : int : if 0 (default), put child process into its own process group
if non-zero, put child process into the specified process group
if None, inherit the parent's process group
"""
assert isinstance( command, str ) or isinstance( command, list )
self.command = command
if timeout is not None:
assert isinstance( timeout, int )
assert timeout > 0
self.timeout = timeout
assert isinstance( timeoutSignal, int )
self.timeoutSignal = timeoutSignal
self.prevAlarmHandler = None
self.raisePrevSigalrmError = raisePrevSigalrmError
if None == childProcessGroup:
self.preExecFun = None
else:
assert isinstance( childProcessGroup, int )
self.preExecFun = (lambda : os.setpgid(0,childProcessGroup))
def run( self,
stdin=None, stdout=None, stderr=None,
shell=True, cwd=None, env=None ):
"""Takes the same arguments as Python's subprocess.Popen(), with the
following exceptions:
1. this version runs the command in a shell by default (shell=True)
2. this function needs the preexec_fn hook, so that is not available
This function returns the result from subprocess.Popen() (a subprocess
object), so you can read from pipes, poll, etc.
"""
self.subprocess = subprocess.Popen( self.command,
stdin=stdin,
stdout=stdout,
stderr=stderr,
# runs in the child
# process before
# the exec(), putting
# the child process into
# its own process group
preexec_fn=self.preExecFun,
shell=shell,
cwd=cwd,
env=env )
self.pgid = os.getpgid( self.subprocess.pid )
# Setup the SIGALRM handler: we use a lambda as a "curried"
# function to bind some values
if self.timeout is not None:
if signal.getsignal( signal.SIGALRM ) not in (None, signal.SIG_IGN, signal.SIG_DFL):
# someone is using a SIGALRM handler!
if self.raisePrevSigalrmError:
ValueError( "SIGALRM handler already in use!" )
self.prevAlarmHandler = signal.getsignal( signal.SIGALRM )
signal.signal( signal.SIGALRM,
lambda sig,frame : os.killpg(self.pgid,self.timeoutSignal) )
# setup handler before scheduling signal, to eliminate a race
signal.alarm( self.timeout )
return self.subprocess
def cancelTimeout(self):
"""If we're using a timeout, cancel the SIGALRM timeout when
the child is finished, and restore the previous SIGALRM handler"""
if self.timeout is not None:
signal.alarm( 0 )
signal.signal( signal.SIGALRM, self.prevAlarmHandler )
pass
return
def wait(self):
"""Wait for the child process to exit, or the timeout to expire,
whichever comes first.
This function returns the same thing as Python's
subprocess.wait(). That is, this function returns the exit status of
the child process; if the return value is -N, it indicates that the
child was killed by signal N. """
try:
self.subprocess.wait()
except OSError, e:
# If the child times out, the wait() syscall can get
# interrupted by the SIGALRM. We should then only need to
# wait() once more for the child to actually exit.
if e.errno == errno.EINTR:
self.subprocess.wait()
else:
raise e
pass
self.cancelTimeout()
assert self.subprocess.poll() is not None
return self.subprocess.poll()
def kill(self, deathsig=signal.SIGKILL):
"""Kill the child process. Optionally specify the signal to be used
(default: SIGKILL)"""
try:
os.killpg( self.pgid, deathsig )
except OSError, e:
if e.errno == errno.ESRCH:
# We end up here if the process group has already exited, so it's safe to
# ignore the error
pass
else:
raise e
pass
self.cancelTimeout()
if __name__ == "__main__":
if len(sys.argv) <= 2:
print "Usage:", sys.argv[0], "TIMEOUT COMMAND..."
print
descrip = """Runs COMMAND for up to TIMEOUT seconds, sending COMMAND a SIGKILL if it
attempts to run longer. Exits sooner if COMMAND does so, passing along COMMAND's
exit code.
All of COMMAND's children run in a new process group, and the entire group is
SIGKILL'ed when the timeout expires. """
print descrip
sys.exit( 0 )
pass
t = TimerTask( " ".join(sys.argv[2:]),
timeout=int(sys.argv[1]) )
t.run()
e = t.wait()
sys.exit( e )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment