addon-sdk/source/python-lib/mozrunner/killableprocess.py

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:05eb310a3eab
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)

mercurial