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