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