26
26
import subprocess
27
27
import string
28
28
import threading
29
+ import time
30
+
31
+
32
+ class TimeoutException (Exception ):
33
+ """
34
+ Exception returned when command exceeded its timeout.
35
+ """
36
+ pass
29
37
30
38
31
39
class Command :
@@ -38,14 +46,17 @@ class Command:
38
46
FINISHED = "finished"
39
47
INTERRUPTED = "interrupted"
40
48
ERRORED = "errored"
49
+ TIMEDOUT = "timed out"
41
50
42
51
def __init__ (self , cmd , args_subst = None , args_append = None , logger = None ,
43
- excl_subst = False , work_dir = None , env_vars = None ):
52
+ excl_subst = False , work_dir = None , env_vars = None , timeout = None ):
44
53
self .cmd = cmd
45
54
self .state = "notrun"
46
55
self .excl_subst = excl_subst
47
56
self .work_dir = work_dir
48
57
self .env_vars = env_vars
58
+ self .timeout = timeout
59
+ self .pid = None
49
60
50
61
self .logger = logger or logging .getLogger (__name__ )
51
62
logging .basicConfig ()
@@ -61,6 +72,41 @@ def execute(self):
61
72
Execute the command and capture its output and return code.
62
73
"""
63
74
75
+ class TimeoutThread (threading .Thread ):
76
+ """
77
+ Wait until the timeout specified in seconds expires and kill
78
+ the process specified by the Popen object after that.
79
+ If timeout expires, TimeoutException is stored in the object
80
+ and can be retrieved by the caller.
81
+ """
82
+
83
+ def __init__ (self , logger , timeout , condition , p ):
84
+ super (TimeoutThread , self ).__init__ ()
85
+ self .timeout = timeout
86
+ self .popen = p
87
+ self .condition = condition
88
+ self .logger = logger
89
+ self .start ()
90
+ self .exception = None
91
+
92
+ def run (self ):
93
+ with self .condition :
94
+ if not self .condition .wait (self .timeout ):
95
+ p = self .popen
96
+ self .logger .info ("Terminating command {} with PID {} "
97
+ "after timeout of {} seconds" .
98
+ format (p .args , p .pid , self .timeout ))
99
+ p .terminate ()
100
+ self .exception = TimeoutException ("Command {} with pid"
101
+ " {} timed out" .
102
+ format (p .args ,
103
+ p .pid ))
104
+ else :
105
+ return None
106
+
107
+ def get_exception (self ):
108
+ return self .exception
109
+
64
110
class OutputThread (threading .Thread ):
65
111
"""
66
112
Capture data from subprocess.Popen(). This avoids hangs when
@@ -116,33 +162,59 @@ def close(self):
116
162
format (self .work_dir ), exc_info = True )
117
163
return
118
164
119
- othr = OutputThread ()
165
+ timeout_thread = None
166
+ output_thread = OutputThread ()
120
167
try :
168
+ start_time = time .time ()
121
169
self .logger .debug ("working directory = {}" .format (os .getcwd ()))
122
170
self .logger .debug ("command = {}" .format (self .cmd ))
123
171
if self .env_vars :
124
172
my_env = os .environ .copy ()
125
173
my_env .update (self .env_vars )
126
174
p = subprocess .Popen (self .cmd , stderr = subprocess .STDOUT ,
127
- stdout = othr , env = my_env )
175
+ stdout = output_thread , env = my_env )
128
176
else :
129
177
p = subprocess .Popen (self .cmd , stderr = subprocess .STDOUT ,
130
- stdout = othr )
178
+ stdout = output_thread )
179
+
180
+ self .pid = p .pid
181
+
182
+ if self .timeout :
183
+ condition = threading .Condition ()
184
+ self .logger .debug ("Setting timeout to {}" .format (self .timeout ))
185
+ timeout_thread = TimeoutThread (self .logger , self .timeout ,
186
+ condition , p )
187
+
188
+ self .logger .debug ("Waiting for process with PID {}" .format (p .pid ))
131
189
p .wait ()
190
+
191
+ if self .timeout :
192
+ e = timeout_thread .get_exception ()
193
+ if e :
194
+ raise e
132
195
except KeyboardInterrupt as e :
133
196
self .logger .info ("Got KeyboardException while processing " ,
134
197
exc_info = True )
135
198
self .state = Command .INTERRUPTED
136
199
except OSError as e :
137
200
self .logger .error ("Got OS error" , exc_info = True )
138
201
self .state = Command .ERRORED
202
+ except TimeoutException as e :
203
+ self .logger .error ("Timed out" )
204
+ self .state = Command .TIMEDOUT
139
205
else :
140
206
self .state = Command .FINISHED
141
207
self .returncode = int (p .returncode )
142
208
self .logger .debug ("{} -> {}" .format (self .cmd , self .getretcode ()))
143
209
finally :
144
- othr .close ()
145
- self .out = othr .getoutput ()
210
+ if self .timeout != 0 and timeout_thread :
211
+ with condition :
212
+ condition .notifyAll ()
213
+ output_thread .close ()
214
+ self .out = output_thread .getoutput ()
215
+ elapsed_time = time .time () - start_time
216
+ self .logger .debug ("Command {} took {} seconds" .
217
+ format (self .cmd , int (elapsed_time )))
146
218
147
219
if orig_work_dir :
148
220
try :
@@ -199,3 +271,6 @@ def getoutput(self):
199
271
200
272
def getstate (self ):
201
273
return self .state
274
+
275
+ def getpid (self ):
276
+ return self .pid
0 commit comments