1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/python-lib/mozrunner/killableprocess.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,329 @@ 1.4 +# killableprocess - subprocesses which can be reliably killed 1.5 +# 1.6 +# Parts of this module are copied from the subprocess.py file contained 1.7 +# in the Python distribution. 1.8 +# 1.9 +# Copyright (c) 2003-2004 by Peter Astrand <astrand@lysator.liu.se> 1.10 +# 1.11 +# Additions and modifications written by Benjamin Smedberg 1.12 +# <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation 1.13 +# <http://www.mozilla.org/> 1.14 +# 1.15 +# More Modifications 1.16 +# Copyright (c) 2006-2007 by Mike Taylor <bear@code-bear.com> 1.17 +# Copyright (c) 2007-2008 by Mikeal Rogers <mikeal@mozilla.com> 1.18 +# 1.19 +# By obtaining, using, and/or copying this software and/or its 1.20 +# associated documentation, you agree that you have read, understood, 1.21 +# and will comply with the following terms and conditions: 1.22 +# 1.23 +# Permission to use, copy, modify, and distribute this software and 1.24 +# its associated documentation for any purpose and without fee is 1.25 +# hereby granted, provided that the above copyright notice appears in 1.26 +# all copies, and that both that copyright notice and this permission 1.27 +# notice appear in supporting documentation, and that the name of the 1.28 +# author not be used in advertising or publicity pertaining to 1.29 +# distribution of the software without specific, written prior 1.30 +# permission. 1.31 +# 1.32 +# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, 1.33 +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. 1.34 +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR 1.35 +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 1.36 +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, 1.37 +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION 1.38 +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 1.39 + 1.40 +"""killableprocess - Subprocesses which can be reliably killed 1.41 + 1.42 +This module is a subclass of the builtin "subprocess" module. It allows 1.43 +processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method. 1.44 + 1.45 +It also adds a timeout argument to Wait() for a limited period of time before 1.46 +forcefully killing the process. 1.47 + 1.48 +Note: On Windows, this module requires Windows 2000 or higher (no support for 1.49 +Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with 1.50 +Python 2.5+ or available from http://python.net/crew/theller/ctypes/ 1.51 +""" 1.52 + 1.53 +import subprocess 1.54 +import sys 1.55 +import os 1.56 +import time 1.57 +import datetime 1.58 +import types 1.59 +import exceptions 1.60 + 1.61 +try: 1.62 + from subprocess import CalledProcessError 1.63 +except ImportError: 1.64 + # Python 2.4 doesn't implement CalledProcessError 1.65 + class CalledProcessError(Exception): 1.66 + """This exception is raised when a process run by check_call() returns 1.67 + a non-zero exit status. The exit status will be stored in the 1.68 + returncode attribute.""" 1.69 + def __init__(self, returncode, cmd): 1.70 + self.returncode = returncode 1.71 + self.cmd = cmd 1.72 + def __str__(self): 1.73 + return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) 1.74 + 1.75 +mswindows = (sys.platform == "win32") 1.76 + 1.77 +if mswindows: 1.78 + import winprocess 1.79 +else: 1.80 + import signal 1.81 + 1.82 +# This is normally defined in win32con, but we don't want 1.83 +# to incur the huge tree of dependencies (pywin32 and friends) 1.84 +# just to get one constant. So here's our hack 1.85 +STILL_ACTIVE = 259 1.86 + 1.87 +def call(*args, **kwargs): 1.88 + waitargs = {} 1.89 + if "timeout" in kwargs: 1.90 + waitargs["timeout"] = kwargs.pop("timeout") 1.91 + 1.92 + return Popen(*args, **kwargs).wait(**waitargs) 1.93 + 1.94 +def check_call(*args, **kwargs): 1.95 + """Call a program with an optional timeout. If the program has a non-zero 1.96 + exit status, raises a CalledProcessError.""" 1.97 + 1.98 + retcode = call(*args, **kwargs) 1.99 + if retcode: 1.100 + cmd = kwargs.get("args") 1.101 + if cmd is None: 1.102 + cmd = args[0] 1.103 + raise CalledProcessError(retcode, cmd) 1.104 + 1.105 +if not mswindows: 1.106 + def DoNothing(*args): 1.107 + pass 1.108 + 1.109 +class Popen(subprocess.Popen): 1.110 + kill_called = False 1.111 + if mswindows: 1.112 + def _execute_child(self, *args_tuple): 1.113 + # workaround for bug 958609 1.114 + if sys.hexversion < 0x02070600: # prior to 2.7.6 1.115 + (args, executable, preexec_fn, close_fds, 1.116 + cwd, env, universal_newlines, startupinfo, 1.117 + creationflags, shell, 1.118 + p2cread, p2cwrite, 1.119 + c2pread, c2pwrite, 1.120 + errread, errwrite) = args_tuple 1.121 + to_close = set() 1.122 + else: # 2.7.6 and later 1.123 + (args, executable, preexec_fn, close_fds, 1.124 + cwd, env, universal_newlines, startupinfo, 1.125 + creationflags, shell, to_close, 1.126 + p2cread, p2cwrite, 1.127 + c2pread, c2pwrite, 1.128 + errread, errwrite) = args_tuple 1.129 + 1.130 + if not isinstance(args, types.StringTypes): 1.131 + args = subprocess.list2cmdline(args) 1.132 + 1.133 + # Always or in the create new process group 1.134 + creationflags |= winprocess.CREATE_NEW_PROCESS_GROUP 1.135 + 1.136 + if startupinfo is None: 1.137 + startupinfo = winprocess.STARTUPINFO() 1.138 + 1.139 + if None not in (p2cread, c2pwrite, errwrite): 1.140 + startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES 1.141 + 1.142 + startupinfo.hStdInput = int(p2cread) 1.143 + startupinfo.hStdOutput = int(c2pwrite) 1.144 + startupinfo.hStdError = int(errwrite) 1.145 + if shell: 1.146 + startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW 1.147 + startupinfo.wShowWindow = winprocess.SW_HIDE 1.148 + comspec = os.environ.get("COMSPEC", "cmd.exe") 1.149 + args = comspec + " /c " + args 1.150 + 1.151 + # determine if we can create create a job 1.152 + canCreateJob = winprocess.CanCreateJobObject() 1.153 + 1.154 + # set process creation flags 1.155 + creationflags |= winprocess.CREATE_SUSPENDED 1.156 + creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT 1.157 + if canCreateJob: 1.158 + # Uncomment this line below to discover very useful things about your environment 1.159 + #print "++++ killableprocess: releng twistd patch not applied, we can create job objects" 1.160 + creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB 1.161 + 1.162 + # create the process 1.163 + hp, ht, pid, tid = winprocess.CreateProcess( 1.164 + executable, args, 1.165 + None, None, # No special security 1.166 + 1, # Must inherit handles! 1.167 + creationflags, 1.168 + winprocess.EnvironmentBlock(env), 1.169 + cwd, startupinfo) 1.170 + self._child_created = True 1.171 + self._handle = hp 1.172 + self._thread = ht 1.173 + self.pid = pid 1.174 + self.tid = tid 1.175 + 1.176 + if canCreateJob: 1.177 + # We create a new job for this process, so that we can kill 1.178 + # the process and any sub-processes 1.179 + self._job = winprocess.CreateJobObject() 1.180 + winprocess.AssignProcessToJobObject(self._job, int(hp)) 1.181 + else: 1.182 + self._job = None 1.183 + 1.184 + winprocess.ResumeThread(int(ht)) 1.185 + ht.Close() 1.186 + 1.187 + if p2cread is not None: 1.188 + p2cread.Close() 1.189 + if c2pwrite is not None: 1.190 + c2pwrite.Close() 1.191 + if errwrite is not None: 1.192 + errwrite.Close() 1.193 + time.sleep(.1) 1.194 + 1.195 + def kill(self, group=True): 1.196 + """Kill the process. If group=True, all sub-processes will also be killed.""" 1.197 + self.kill_called = True 1.198 + 1.199 + if mswindows: 1.200 + if group and self._job: 1.201 + winprocess.TerminateJobObject(self._job, 127) 1.202 + else: 1.203 + winprocess.TerminateProcess(self._handle, 127) 1.204 + self.returncode = 127 1.205 + else: 1.206 + if group: 1.207 + try: 1.208 + os.killpg(self.pid, signal.SIGKILL) 1.209 + except: pass 1.210 + else: 1.211 + os.kill(self.pid, signal.SIGKILL) 1.212 + self.returncode = -9 1.213 + 1.214 + def wait(self, timeout=None, group=True): 1.215 + """Wait for the process to terminate. Returns returncode attribute. 1.216 + If timeout seconds are reached and the process has not terminated, 1.217 + it will be forcefully killed. If timeout is -1, wait will not 1.218 + time out.""" 1.219 + if timeout is not None: 1.220 + # timeout is now in milliseconds 1.221 + timeout = timeout * 1000 1.222 + 1.223 + starttime = datetime.datetime.now() 1.224 + 1.225 + if mswindows: 1.226 + if timeout is None: 1.227 + timeout = -1 1.228 + rc = winprocess.WaitForSingleObject(self._handle, timeout) 1.229 + 1.230 + if (rc == winprocess.WAIT_OBJECT_0 or 1.231 + rc == winprocess.WAIT_ABANDONED or 1.232 + rc == winprocess.WAIT_FAILED): 1.233 + # Object has either signaled, or the API call has failed. In 1.234 + # both cases we want to give the OS the benefit of the doubt 1.235 + # and supply a little time before we start shooting processes 1.236 + # with an M-16. 1.237 + 1.238 + # Returns 1 if running, 0 if not, -1 if timed out 1.239 + def check(): 1.240 + now = datetime.datetime.now() 1.241 + diff = now - starttime 1.242 + if (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000): 1.243 + if self._job: 1.244 + if (winprocess.QueryInformationJobObject(self._job, 8)['BasicInfo']['ActiveProcesses'] > 0): 1.245 + # Job Object is still containing active processes 1.246 + return 1 1.247 + else: 1.248 + # No job, we use GetExitCodeProcess, which will tell us if the process is still active 1.249 + self.returncode = winprocess.GetExitCodeProcess(self._handle) 1.250 + if (self.returncode == STILL_ACTIVE): 1.251 + # Process still active, continue waiting 1.252 + return 1 1.253 + # Process not active, return 0 1.254 + return 0 1.255 + else: 1.256 + # Timed out, return -1 1.257 + return -1 1.258 + 1.259 + notdone = check() 1.260 + while notdone == 1: 1.261 + time.sleep(.5) 1.262 + notdone = check() 1.263 + 1.264 + if notdone == -1: 1.265 + # Then check timed out, we have a hung process, attempt 1.266 + # last ditch kill with explosives 1.267 + self.kill(group) 1.268 + 1.269 + else: 1.270 + # In this case waitforsingleobject timed out. We have to 1.271 + # take the process behind the woodshed and shoot it. 1.272 + self.kill(group) 1.273 + 1.274 + else: 1.275 + if sys.platform in ('linux2', 'sunos5', 'solaris') \ 1.276 + or sys.platform.startswith('freebsd'): 1.277 + def group_wait(timeout): 1.278 + try: 1.279 + os.waitpid(self.pid, 0) 1.280 + except OSError, e: 1.281 + pass # If wait has already been called on this pid, bad things happen 1.282 + return self.returncode 1.283 + elif sys.platform == 'darwin': 1.284 + def group_wait(timeout): 1.285 + try: 1.286 + count = 0 1.287 + if timeout is None and self.kill_called: 1.288 + timeout = 10 # Have to set some kind of timeout or else this could go on forever 1.289 + if timeout is None: 1.290 + while 1: 1.291 + os.killpg(self.pid, signal.SIG_DFL) 1.292 + while ((count * 2) <= timeout): 1.293 + os.killpg(self.pid, signal.SIG_DFL) 1.294 + # count is increased by 500ms for every 0.5s of sleep 1.295 + time.sleep(.5); count += 500 1.296 + except exceptions.OSError: 1.297 + return self.returncode 1.298 + 1.299 + if timeout is None: 1.300 + if group is True: 1.301 + return group_wait(timeout) 1.302 + else: 1.303 + subprocess.Popen.wait(self) 1.304 + return self.returncode 1.305 + 1.306 + returncode = False 1.307 + 1.308 + now = datetime.datetime.now() 1.309 + diff = now - starttime 1.310 + while (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000) and ( returncode is False ): 1.311 + if group is True: 1.312 + return group_wait(timeout) 1.313 + else: 1.314 + if subprocess.poll() is not None: 1.315 + returncode = self.returncode 1.316 + time.sleep(.5) 1.317 + now = datetime.datetime.now() 1.318 + diff = now - starttime 1.319 + return self.returncode 1.320 + 1.321 + return self.returncode 1.322 + # We get random maxint errors from subprocesses __del__ 1.323 + __del__ = lambda self: None 1.324 + 1.325 +def setpgid_preexec_fn(): 1.326 + os.setpgid(0, 0) 1.327 + 1.328 +def runCommand(cmd, **kwargs): 1.329 + if sys.platform != "win32": 1.330 + return Popen(cmd, preexec_fn=setpgid_preexec_fn, **kwargs) 1.331 + else: 1.332 + return Popen(cmd, **kwargs)