| |
1 # killableprocess - subprocesses which can be reliably killed |
| |
2 # |
| |
3 # Parts of this module are copied from the subprocess.py file contained |
| |
4 # in the Python distribution. |
| |
5 # |
| |
6 # Copyright (c) 2003-2004 by Peter Astrand <astrand@lysator.liu.se> |
| |
7 # |
| |
8 # Additions and modifications written by Benjamin Smedberg |
| |
9 # <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation |
| |
10 # <http://www.mozilla.org/> |
| |
11 # |
| |
12 # More Modifications |
| |
13 # Copyright (c) 2006-2007 by Mike Taylor <bear@code-bear.com> |
| |
14 # Copyright (c) 2007-2008 by Mikeal Rogers <mikeal@mozilla.com> |
| |
15 # |
| |
16 # By obtaining, using, and/or copying this software and/or its |
| |
17 # associated documentation, you agree that you have read, understood, |
| |
18 # and will comply with the following terms and conditions: |
| |
19 # |
| |
20 # Permission to use, copy, modify, and distribute this software and |
| |
21 # its associated documentation for any purpose and without fee is |
| |
22 # hereby granted, provided that the above copyright notice appears in |
| |
23 # all copies, and that both that copyright notice and this permission |
| |
24 # notice appear in supporting documentation, and that the name of the |
| |
25 # author not be used in advertising or publicity pertaining to |
| |
26 # distribution of the software without specific, written prior |
| |
27 # permission. |
| |
28 # |
| |
29 # THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, |
| |
30 # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. |
| |
31 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR |
| |
32 # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS |
| |
33 # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, |
| |
34 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION |
| |
35 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| |
36 |
| |
37 """killableprocess - Subprocesses which can be reliably killed |
| |
38 |
| |
39 This module is a subclass of the builtin "subprocess" module. It allows |
| |
40 processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method. |
| |
41 |
| |
42 It also adds a timeout argument to Wait() for a limited period of time before |
| |
43 forcefully killing the process. |
| |
44 |
| |
45 Note: On Windows, this module requires Windows 2000 or higher (no support for |
| |
46 Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with |
| |
47 Python 2.5+ or available from http://python.net/crew/theller/ctypes/ |
| |
48 """ |
| |
49 |
| |
50 import subprocess |
| |
51 import sys |
| |
52 import os |
| |
53 import time |
| |
54 import datetime |
| |
55 import types |
| |
56 import exceptions |
| |
57 |
| |
58 try: |
| |
59 from subprocess import CalledProcessError |
| |
60 except ImportError: |
| |
61 # Python 2.4 doesn't implement CalledProcessError |
| |
62 class CalledProcessError(Exception): |
| |
63 """This exception is raised when a process run by check_call() returns |
| |
64 a non-zero exit status. The exit status will be stored in the |
| |
65 returncode attribute.""" |
| |
66 def __init__(self, returncode, cmd): |
| |
67 self.returncode = returncode |
| |
68 self.cmd = cmd |
| |
69 def __str__(self): |
| |
70 return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) |
| |
71 |
| |
72 mswindows = (sys.platform == "win32") |
| |
73 |
| |
74 if mswindows: |
| |
75 import winprocess |
| |
76 else: |
| |
77 import signal |
| |
78 |
| |
79 # This is normally defined in win32con, but we don't want |
| |
80 # to incur the huge tree of dependencies (pywin32 and friends) |
| |
81 # just to get one constant. So here's our hack |
| |
82 STILL_ACTIVE = 259 |
| |
83 |
| |
84 def call(*args, **kwargs): |
| |
85 waitargs = {} |
| |
86 if "timeout" in kwargs: |
| |
87 waitargs["timeout"] = kwargs.pop("timeout") |
| |
88 |
| |
89 return Popen(*args, **kwargs).wait(**waitargs) |
| |
90 |
| |
91 def check_call(*args, **kwargs): |
| |
92 """Call a program with an optional timeout. If the program has a non-zero |
| |
93 exit status, raises a CalledProcessError.""" |
| |
94 |
| |
95 retcode = call(*args, **kwargs) |
| |
96 if retcode: |
| |
97 cmd = kwargs.get("args") |
| |
98 if cmd is None: |
| |
99 cmd = args[0] |
| |
100 raise CalledProcessError(retcode, cmd) |
| |
101 |
| |
102 if not mswindows: |
| |
103 def DoNothing(*args): |
| |
104 pass |
| |
105 |
| |
106 class Popen(subprocess.Popen): |
| |
107 kill_called = False |
| |
108 if mswindows: |
| |
109 def _execute_child(self, *args_tuple): |
| |
110 # workaround for bug 958609 |
| |
111 if sys.hexversion < 0x02070600: # prior to 2.7.6 |
| |
112 (args, executable, preexec_fn, close_fds, |
| |
113 cwd, env, universal_newlines, startupinfo, |
| |
114 creationflags, shell, |
| |
115 p2cread, p2cwrite, |
| |
116 c2pread, c2pwrite, |
| |
117 errread, errwrite) = args_tuple |
| |
118 to_close = set() |
| |
119 else: # 2.7.6 and later |
| |
120 (args, executable, preexec_fn, close_fds, |
| |
121 cwd, env, universal_newlines, startupinfo, |
| |
122 creationflags, shell, to_close, |
| |
123 p2cread, p2cwrite, |
| |
124 c2pread, c2pwrite, |
| |
125 errread, errwrite) = args_tuple |
| |
126 |
| |
127 if not isinstance(args, types.StringTypes): |
| |
128 args = subprocess.list2cmdline(args) |
| |
129 |
| |
130 # Always or in the create new process group |
| |
131 creationflags |= winprocess.CREATE_NEW_PROCESS_GROUP |
| |
132 |
| |
133 if startupinfo is None: |
| |
134 startupinfo = winprocess.STARTUPINFO() |
| |
135 |
| |
136 if None not in (p2cread, c2pwrite, errwrite): |
| |
137 startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES |
| |
138 |
| |
139 startupinfo.hStdInput = int(p2cread) |
| |
140 startupinfo.hStdOutput = int(c2pwrite) |
| |
141 startupinfo.hStdError = int(errwrite) |
| |
142 if shell: |
| |
143 startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW |
| |
144 startupinfo.wShowWindow = winprocess.SW_HIDE |
| |
145 comspec = os.environ.get("COMSPEC", "cmd.exe") |
| |
146 args = comspec + " /c " + args |
| |
147 |
| |
148 # determine if we can create create a job |
| |
149 canCreateJob = winprocess.CanCreateJobObject() |
| |
150 |
| |
151 # set process creation flags |
| |
152 creationflags |= winprocess.CREATE_SUSPENDED |
| |
153 creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT |
| |
154 if canCreateJob: |
| |
155 # Uncomment this line below to discover very useful things about your environment |
| |
156 #print "++++ killableprocess: releng twistd patch not applied, we can create job objects" |
| |
157 creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB |
| |
158 |
| |
159 # create the process |
| |
160 hp, ht, pid, tid = winprocess.CreateProcess( |
| |
161 executable, args, |
| |
162 None, None, # No special security |
| |
163 1, # Must inherit handles! |
| |
164 creationflags, |
| |
165 winprocess.EnvironmentBlock(env), |
| |
166 cwd, startupinfo) |
| |
167 self._child_created = True |
| |
168 self._handle = hp |
| |
169 self._thread = ht |
| |
170 self.pid = pid |
| |
171 self.tid = tid |
| |
172 |
| |
173 if canCreateJob: |
| |
174 # We create a new job for this process, so that we can kill |
| |
175 # the process and any sub-processes |
| |
176 self._job = winprocess.CreateJobObject() |
| |
177 winprocess.AssignProcessToJobObject(self._job, int(hp)) |
| |
178 else: |
| |
179 self._job = None |
| |
180 |
| |
181 winprocess.ResumeThread(int(ht)) |
| |
182 ht.Close() |
| |
183 |
| |
184 if p2cread is not None: |
| |
185 p2cread.Close() |
| |
186 if c2pwrite is not None: |
| |
187 c2pwrite.Close() |
| |
188 if errwrite is not None: |
| |
189 errwrite.Close() |
| |
190 time.sleep(.1) |
| |
191 |
| |
192 def kill(self, group=True): |
| |
193 """Kill the process. If group=True, all sub-processes will also be killed.""" |
| |
194 self.kill_called = True |
| |
195 |
| |
196 if mswindows: |
| |
197 if group and self._job: |
| |
198 winprocess.TerminateJobObject(self._job, 127) |
| |
199 else: |
| |
200 winprocess.TerminateProcess(self._handle, 127) |
| |
201 self.returncode = 127 |
| |
202 else: |
| |
203 if group: |
| |
204 try: |
| |
205 os.killpg(self.pid, signal.SIGKILL) |
| |
206 except: pass |
| |
207 else: |
| |
208 os.kill(self.pid, signal.SIGKILL) |
| |
209 self.returncode = -9 |
| |
210 |
| |
211 def wait(self, timeout=None, group=True): |
| |
212 """Wait for the process to terminate. Returns returncode attribute. |
| |
213 If timeout seconds are reached and the process has not terminated, |
| |
214 it will be forcefully killed. If timeout is -1, wait will not |
| |
215 time out.""" |
| |
216 if timeout is not None: |
| |
217 # timeout is now in milliseconds |
| |
218 timeout = timeout * 1000 |
| |
219 |
| |
220 starttime = datetime.datetime.now() |
| |
221 |
| |
222 if mswindows: |
| |
223 if timeout is None: |
| |
224 timeout = -1 |
| |
225 rc = winprocess.WaitForSingleObject(self._handle, timeout) |
| |
226 |
| |
227 if (rc == winprocess.WAIT_OBJECT_0 or |
| |
228 rc == winprocess.WAIT_ABANDONED or |
| |
229 rc == winprocess.WAIT_FAILED): |
| |
230 # Object has either signaled, or the API call has failed. In |
| |
231 # both cases we want to give the OS the benefit of the doubt |
| |
232 # and supply a little time before we start shooting processes |
| |
233 # with an M-16. |
| |
234 |
| |
235 # Returns 1 if running, 0 if not, -1 if timed out |
| |
236 def check(): |
| |
237 now = datetime.datetime.now() |
| |
238 diff = now - starttime |
| |
239 if (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000): |
| |
240 if self._job: |
| |
241 if (winprocess.QueryInformationJobObject(self._job, 8)['BasicInfo']['ActiveProcesses'] > 0): |
| |
242 # Job Object is still containing active processes |
| |
243 return 1 |
| |
244 else: |
| |
245 # No job, we use GetExitCodeProcess, which will tell us if the process is still active |
| |
246 self.returncode = winprocess.GetExitCodeProcess(self._handle) |
| |
247 if (self.returncode == STILL_ACTIVE): |
| |
248 # Process still active, continue waiting |
| |
249 return 1 |
| |
250 # Process not active, return 0 |
| |
251 return 0 |
| |
252 else: |
| |
253 # Timed out, return -1 |
| |
254 return -1 |
| |
255 |
| |
256 notdone = check() |
| |
257 while notdone == 1: |
| |
258 time.sleep(.5) |
| |
259 notdone = check() |
| |
260 |
| |
261 if notdone == -1: |
| |
262 # Then check timed out, we have a hung process, attempt |
| |
263 # last ditch kill with explosives |
| |
264 self.kill(group) |
| |
265 |
| |
266 else: |
| |
267 # In this case waitforsingleobject timed out. We have to |
| |
268 # take the process behind the woodshed and shoot it. |
| |
269 self.kill(group) |
| |
270 |
| |
271 else: |
| |
272 if sys.platform in ('linux2', 'sunos5', 'solaris') \ |
| |
273 or sys.platform.startswith('freebsd'): |
| |
274 def group_wait(timeout): |
| |
275 try: |
| |
276 os.waitpid(self.pid, 0) |
| |
277 except OSError, e: |
| |
278 pass # If wait has already been called on this pid, bad things happen |
| |
279 return self.returncode |
| |
280 elif sys.platform == 'darwin': |
| |
281 def group_wait(timeout): |
| |
282 try: |
| |
283 count = 0 |
| |
284 if timeout is None and self.kill_called: |
| |
285 timeout = 10 # Have to set some kind of timeout or else this could go on forever |
| |
286 if timeout is None: |
| |
287 while 1: |
| |
288 os.killpg(self.pid, signal.SIG_DFL) |
| |
289 while ((count * 2) <= timeout): |
| |
290 os.killpg(self.pid, signal.SIG_DFL) |
| |
291 # count is increased by 500ms for every 0.5s of sleep |
| |
292 time.sleep(.5); count += 500 |
| |
293 except exceptions.OSError: |
| |
294 return self.returncode |
| |
295 |
| |
296 if timeout is None: |
| |
297 if group is True: |
| |
298 return group_wait(timeout) |
| |
299 else: |
| |
300 subprocess.Popen.wait(self) |
| |
301 return self.returncode |
| |
302 |
| |
303 returncode = False |
| |
304 |
| |
305 now = datetime.datetime.now() |
| |
306 diff = now - starttime |
| |
307 while (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000) and ( returncode is False ): |
| |
308 if group is True: |
| |
309 return group_wait(timeout) |
| |
310 else: |
| |
311 if subprocess.poll() is not None: |
| |
312 returncode = self.returncode |
| |
313 time.sleep(.5) |
| |
314 now = datetime.datetime.now() |
| |
315 diff = now - starttime |
| |
316 return self.returncode |
| |
317 |
| |
318 return self.returncode |
| |
319 # We get random maxint errors from subprocesses __del__ |
| |
320 __del__ = lambda self: None |
| |
321 |
| |
322 def setpgid_preexec_fn(): |
| |
323 os.setpgid(0, 0) |
| |
324 |
| |
325 def runCommand(cmd, **kwargs): |
| |
326 if sys.platform != "win32": |
| |
327 return Popen(cmd, preexec_fn=setpgid_preexec_fn, **kwargs) |
| |
328 else: |
| |
329 return Popen(cmd, **kwargs) |