123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- # 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 <astrand@lysator.liu.se>
- #
- # Additions and modifications written by Benjamin Smedberg
- # <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation
- # <http://www.mozilla.org/>
- #
- # More Modifications
- # Copyright (c) 2006-2007 by Mike Taylor <bear@code-bear.com>
- # Copyright (c) 2007-2008 by Mikeal Rogers <mikeal@mozilla.com>
- #
- # 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.now()
- 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.now()
- diff = now - starttime
- if (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 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.now()
- 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.now()
- 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)
|