1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
|
#!/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=subprocess.PIPE, stderr=subprocess.PIPE,
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 )
|