OLD | NEW |
| (Empty) |
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 Pop
en.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 def call(*args, **kwargs): | |
80 waitargs = {} | |
81 if "timeout" in kwargs: | |
82 waitargs["timeout"] = kwargs.pop("timeout") | |
83 | |
84 return Popen(*args, **kwargs).wait(**waitargs) | |
85 | |
86 def check_call(*args, **kwargs): | |
87 """Call a program with an optional timeout. If the program has a non-zero | |
88 exit status, raises a CalledProcessError.""" | |
89 | |
90 retcode = call(*args, **kwargs) | |
91 if retcode: | |
92 cmd = kwargs.get("args") | |
93 if cmd is None: | |
94 cmd = args[0] | |
95 raise CalledProcessError(retcode, cmd) | |
96 | |
97 if not mswindows: | |
98 def DoNothing(*args): | |
99 pass | |
100 | |
101 class Popen(subprocess.Popen): | |
102 kill_called = False | |
103 if mswindows: | |
104 def _execute_child(self, args, executable, preexec_fn, close_fds, | |
105 cwd, env, universal_newlines, startupinfo, | |
106 creationflags, shell, | |
107 p2cread, p2cwrite, | |
108 c2pread, c2pwrite, | |
109 errread, errwrite): | |
110 if not isinstance(args, types.StringTypes): | |
111 args = subprocess.list2cmdline(args) | |
112 | |
113 # Always or in the create new process group | |
114 creationflags |= winprocess.CREATE_NEW_PROCESS_GROUP | |
115 | |
116 if startupinfo is None: | |
117 startupinfo = winprocess.STARTUPINFO() | |
118 | |
119 if None not in (p2cread, c2pwrite, errwrite): | |
120 startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES | |
121 | |
122 startupinfo.hStdInput = int(p2cread) | |
123 startupinfo.hStdOutput = int(c2pwrite) | |
124 startupinfo.hStdError = int(errwrite) | |
125 if shell: | |
126 startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW | |
127 startupinfo.wShowWindow = winprocess.SW_HIDE | |
128 comspec = os.environ.get("COMSPEC", "cmd.exe") | |
129 args = comspec + " /c " + args | |
130 | |
131 # determine if we can create create a job | |
132 canCreateJob = winprocess.CanCreateJobObject() | |
133 | |
134 # set process creation flags | |
135 creationflags |= winprocess.CREATE_SUSPENDED | |
136 creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT | |
137 if canCreateJob: | |
138 creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB | |
139 | |
140 # create the process | |
141 hp, ht, pid, tid = winprocess.CreateProcess( | |
142 executable, args, | |
143 None, None, # No special security | |
144 1, # Must inherit handles! | |
145 creationflags, | |
146 winprocess.EnvironmentBlock(env), | |
147 cwd, startupinfo) | |
148 self._child_created = True | |
149 self._handle = hp | |
150 self._thread = ht | |
151 self.pid = pid | |
152 self.tid = tid | |
153 | |
154 if canCreateJob: | |
155 # We create a new job for this process, so that we can kill | |
156 # the process and any sub-processes | |
157 self._job = winprocess.CreateJobObject() | |
158 winprocess.AssignProcessToJobObject(self._job, int(hp)) | |
159 else: | |
160 self._job = None | |
161 | |
162 winprocess.ResumeThread(int(ht)) | |
163 ht.Close() | |
164 | |
165 if p2cread is not None: | |
166 p2cread.Close() | |
167 if c2pwrite is not None: | |
168 c2pwrite.Close() | |
169 if errwrite is not None: | |
170 errwrite.Close() | |
171 time.sleep(.1) | |
172 | |
173 def kill(self, group=True): | |
174 """Kill the process. If group=True, all sub-processes will also be kille
d.""" | |
175 self.kill_called = True | |
176 if mswindows: | |
177 if group and self._job: | |
178 winprocess.TerminateJobObject(self._job, 127) | |
179 else: | |
180 try: | |
181 winprocess.TerminateProcess(self._handle, 127) | |
182 except: | |
183 # TODO: better error handling here | |
184 pass | |
185 self.returncode = 127 | |
186 else: | |
187 if group: | |
188 try: | |
189 os.killpg(self.pid, signal.SIGKILL) | |
190 except: pass | |
191 else: | |
192 os.kill(self.pid, signal.SIGKILL) | |
193 self.returncode = -9 | |
194 | |
195 def wait(self, timeout=None, group=True): | |
196 """Wait for the process to terminate. Returns returncode attribute. | |
197 If timeout seconds are reached and the process has not terminated, | |
198 it will be forcefully killed. If timeout is -1, wait will not | |
199 time out.""" | |
200 | |
201 if timeout is not None: | |
202 # timeout is now in milliseconds | |
203 timeout = timeout * 1000 | |
204 | |
205 if self.returncode is not None: | |
206 return self.returncode | |
207 | |
208 starttime = datetime.datetime.now() | |
209 | |
210 if mswindows: | |
211 if timeout is None: | |
212 timeout = -1 | |
213 rc = winprocess.WaitForSingleObject(self._handle, timeout) | |
214 | |
215 if rc != winprocess.WAIT_TIMEOUT: | |
216 def check(): | |
217 now = datetime.datetime.now() | |
218 diff = now - starttime | |
219 if (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeo
ut * 1000): | |
220 if self._job: | |
221 if (winprocess.QueryInformationJobObject(self._job,
8)['BasicInfo']['ActiveProcesses'] > 0): | |
222 return True | |
223 else: | |
224 return True | |
225 return False | |
226 while check(): | |
227 time.sleep(.5) | |
228 | |
229 now = datetime.datetime.now() | |
230 diff = now - starttime | |
231 if (diff.seconds * 1000 * 1000 + diff.microseconds) > (timeout * 100
0): | |
232 self.kill(group) | |
233 else: | |
234 self.returncode = winprocess.GetExitCodeProcess(self._handle) | |
235 else: | |
236 if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solari
s')): | |
237 def group_wait(timeout): | |
238 try: | |
239 os.waitpid(self.pid, 0) | |
240 except OSError, e: | |
241 pass # If wait has already been called on this pid, bad
things happen | |
242 return self.returncode | |
243 elif sys.platform == 'darwin': | |
244 def group_wait(timeout): | |
245 try: | |
246 count = 0 | |
247 if timeout is None and self.kill_called: | |
248 timeout = 10 # Have to set some kind of timeout or e
lse this could go on forever | |
249 if timeout is None: | |
250 while 1: | |
251 os.killpg(self.pid, signal.SIG_DFL) | |
252 while ((count * 2) <= timeout): | |
253 os.killpg(self.pid, signal.SIG_DFL) | |
254 # count is increased by 500ms for every 0.5s of slee
p | |
255 time.sleep(.5); count += 500 | |
256 except exceptions.OSError: | |
257 return self.returncode | |
258 | |
259 if timeout is None: | |
260 if group is True: | |
261 return group_wait(timeout) | |
262 else: | |
263 subprocess.Popen.wait(self) | |
264 return self.returncode | |
265 | |
266 returncode = False | |
267 | |
268 now = datetime.datetime.now() | |
269 diff = now - starttime | |
270 while (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout *
1000) and ( returncode is False ): | |
271 if group is True: | |
272 return group_wait(timeout) | |
273 else: | |
274 if subprocess.poll() is not None: | |
275 returncode = self.returncode | |
276 time.sleep(.5) | |
277 now = datetime.datetime.now() | |
278 diff = now - starttime | |
279 return self.returncode | |
280 | |
281 return self.returncode | |
282 # We get random maxint errors from subprocesses __del__ | |
283 __del__ = lambda self: None | |
284 | |
285 def setpgid_preexec_fn(): | |
286 os.setpgid(0, 0) | |
287 | |
288 def runCommand(cmd, **kwargs): | |
289 if sys.platform != "win32": | |
290 return Popen(cmd, preexec_fn=setpgid_preexec_fn, **kwargs) | |
291 else: | |
292 return Popen(cmd, **kwargs) | |
OLD | NEW |