# killableprocess - subprocesses which can be reliably killed # # Parts of this module are copied from the subprocess.py file contained # in the Python distribution. # # Copyright (c) 2003-2004 by Peter Astrand # # Additions and modifications written by Benjamin Smedberg # are Copyright (c) 2006 by the Mozilla Foundation # # # More Modifications # Copyright (c) 2006-2007 by Mike Taylor # Copyright (c) 2007-2008 by Mikeal Rogers # # By obtaining, using, and/or copying this software and/or its # associated documentation, you agree that you have read, understood, # and will comply with the following terms and conditions: # # Permission to use, copy, modify, and distribute this software and # its associated documentation for any purpose and without fee is # hereby granted, provided that the above copyright notice appears in # all copies, and that both that copyright notice and this permission # notice appear in supporting documentation, and that the name of the # author not be used in advertising or publicity pertaining to # distribution of the software without specific, written prior # permission. # # THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """killableprocess - Subprocesses which can be reliably killed This module is a subclass of the builtin "subprocess" module. It allows processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method. It also adds a timeout argument to Wait() for a limited period of time before forcefully killing the process. Note: On Windows, this module requires Windows 2000 or higher (no support for Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with Python 2.5+ or available from http://python.net/crew/theller/ctypes/ """ import subprocess import sys import os import time import datetime import types import exceptions try: from subprocess import CalledProcessError except ImportError: # Python 2.4 doesn't implement CalledProcessError class CalledProcessError(Exception): """This exception is raised when a process run by check_call() returns a non-zero exit status. The exit status will be stored in the returncode attribute.""" def __init__(self, returncode, cmd): self.returncode = returncode self.cmd = cmd def __str__(self): return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) mswindows = (sys.platform == "win32") if mswindows: import winprocess else: import signal # This is normally defined in win32con, but we don't want # to incur the huge tree of dependencies (pywin32 and friends) # just to get one constant. So here's our hack STILL_ACTIVE = 259 def call(*args, **kwargs): waitargs = {} if "timeout" in kwargs: waitargs["timeout"] = kwargs.pop("timeout") return Popen(*args, **kwargs).wait(**waitargs) def check_call(*args, **kwargs): """Call a program with an optional timeout. If the program has a non-zero exit status, raises a CalledProcessError.""" retcode = call(*args, **kwargs) if retcode: cmd = kwargs.get("args") if cmd is None: cmd = args[0] raise CalledProcessError(retcode, cmd) if not mswindows: def DoNothing(*args): pass class Popen(subprocess.Popen): kill_called = False if mswindows: def _execute_child(self, *args_tuple): # workaround for bug 958609 if sys.hexversion < 0x02070600: # prior to 2.7.6 (args, executable, preexec_fn, close_fds, cwd, env, universal_newlines, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) = args_tuple to_close = set() else: # 2.7.6 and later (args, executable, preexec_fn, close_fds, cwd, env, universal_newlines, startupinfo, creationflags, shell, to_close, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) = args_tuple if not isinstance(args, types.StringTypes): args = subprocess.list2cmdline(args) # Always or in the create new process group creationflags |= winprocess.CREATE_NEW_PROCESS_GROUP if startupinfo is None: startupinfo = winprocess.STARTUPINFO() if None not in (p2cread, c2pwrite, errwrite): startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES startupinfo.hStdInput = int(p2cread) startupinfo.hStdOutput = int(c2pwrite) startupinfo.hStdError = int(errwrite) if shell: startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = winprocess.SW_HIDE comspec = os.environ.get("COMSPEC", "cmd.exe") args = comspec + " /c " + args # determine if we can create create a job canCreateJob = winprocess.CanCreateJobObject() # set process creation flags creationflags |= winprocess.CREATE_SUSPENDED creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT if canCreateJob: # Uncomment this line below to discover very useful things about your environment #print "++++ killableprocess: releng twistd patch not applied, we can create job objects" creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB # create the process hp, ht, pid, tid = winprocess.CreateProcess( executable, args, None, None, # No special security 1, # Must inherit handles! creationflags, winprocess.EnvironmentBlock(env), cwd, startupinfo) self._child_created = True self._handle = hp self._thread = ht self.pid = pid self.tid = tid if canCreateJob: # We create a new job for this process, so that we can kill # the process and any sub-processes self._job = winprocess.CreateJobObject() winprocess.AssignProcessToJobObject(self._job, int(hp)) else: self._job = None winprocess.ResumeThread(int(ht)) ht.Close() if p2cread is not None: p2cread.Close() if c2pwrite is not None: c2pwrite.Close() if errwrite is not None: errwrite.Close() time.sleep(.1) def kill(self, group=True): """Kill the process. If group=True, all sub-processes will also be killed.""" self.kill_called = True if mswindows: if group and self._job: winprocess.TerminateJobObject(self._job, 127) else: winprocess.TerminateProcess(self._handle, 127) self.returncode = 127 else: if group: try: os.killpg(self.pid, signal.SIGKILL) except: pass else: os.kill(self.pid, signal.SIGKILL) self.returncode = -9 def wait(self, timeout=None, group=True): """Wait for the process to terminate. Returns returncode attribute. If timeout seconds are reached and the process has not terminated, it will be forcefully killed. If timeout is -1, wait will not time out.""" if timeout is not None: # timeout is now in milliseconds timeout = timeout * 1000 starttime = datetime.datetime.utcnow() if mswindows: if timeout is None: timeout = -1 rc = winprocess.WaitForSingleObject(self._handle, timeout) if (rc == winprocess.WAIT_OBJECT_0 or rc == winprocess.WAIT_ABANDONED or rc == winprocess.WAIT_FAILED): # Object has either signaled, or the API call has failed. In # both cases we want to give the OS the benefit of the doubt # and supply a little time before we start shooting processes # with an M-16. # Returns 1 if running, 0 if not, -1 if timed out def check(): now = datetime.datetime.utcnow() diff = now - starttime if (diff.seconds * 1000000 + diff.microseconds) < (timeout * 1000): # (1000*1000) if self._job: if (winprocess.QueryInformationJobObject(self._job, 8)['BasicInfo']['ActiveProcesses'] > 0): # Job Object is still containing active processes return 1 else: # No job, we use GetExitCodeProcess, which will tell us if the process is still active self.returncode = winprocess.GetExitCodeProcess(self._handle) if (self.returncode == STILL_ACTIVE): # Process still active, continue waiting return 1 # Process not active, return 0 return 0 else: # Timed out, return -1 return -1 notdone = check() while notdone == 1: time.sleep(.5) notdone = check() if notdone == -1: # Then check timed out, we have a hung process, attempt # last ditch kill with explosives self.kill(group) else: # In this case waitforsingleobject timed out. We have to # take the process behind the woodshed and shoot it. self.kill(group) else: if sys.platform in ('linux2', 'sunos5', 'solaris') \ or sys.platform.startswith('freebsd'): def group_wait(timeout): try: os.waitpid(self.pid, 0) except OSError, e: pass # If wait has already been called on this pid, bad things happen return self.returncode elif sys.platform == 'darwin': def group_wait(timeout): try: count = 0 if timeout is None and self.kill_called: timeout = 10 # Have to set some kind of timeout or else this could go on forever if timeout is None: while 1: os.killpg(self.pid, signal.SIG_DFL) while ((count * 2) <= timeout): os.killpg(self.pid, signal.SIG_DFL) # count is increased by 500ms for every 0.5s of sleep time.sleep(.5); count += 500 except exceptions.OSError: return self.returncode if timeout is None: if group is True: return group_wait(timeout) else: subprocess.Popen.wait(self) return self.returncode returncode = False now = datetime.datetime.utcnow() diff = now - starttime while (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000) and ( returncode is False ): if group is True: return group_wait(timeout) else: if subprocess.poll() is not None: returncode = self.returncode time.sleep(.5) now = datetime.datetime.utcnow() diff = now - starttime return self.returncode return self.returncode # We get random maxint errors from subprocesses __del__ __del__ = lambda self: None def setpgid_preexec_fn(): os.setpgid(0, 0) def runCommand(cmd, **kwargs): if sys.platform != "win32": return Popen(cmd, preexec_fn=setpgid_preexec_fn, **kwargs) else: return Popen(cmd, **kwargs)