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