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 |