Index: pylib/mozrunner/killableprocess.py |
=================================================================== |
--- pylib/mozrunner/killableprocess.py (revision 0) |
+++ pylib/mozrunner/killableprocess.py (revision 0) |
@@ -0,0 +1,292 @@ |
+# 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 |
+ |
+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, executable, preexec_fn, close_fds, |
+ cwd, env, universal_newlines, startupinfo, |
+ creationflags, shell, |
+ p2cread, p2cwrite, |
+ c2pread, c2pwrite, |
+ errread, errwrite): |
+ 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: |
+ 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: |
+ try: |
+ winprocess.TerminateProcess(self._handle, 127) |
+ except: |
+ # TODO: better error handling here |
+ pass |
+ 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 |
+ |
+ if self.returncode is not None: |
+ return self.returncode |
+ |
+ starttime = datetime.datetime.now() |
+ |
+ if mswindows: |
+ if timeout is None: |
+ timeout = -1 |
+ rc = winprocess.WaitForSingleObject(self._handle, timeout) |
+ |
+ if rc != winprocess.WAIT_TIMEOUT: |
+ 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): |
+ return True |
+ else: |
+ return True |
+ return False |
+ while check(): |
+ time.sleep(.5) |
+ |
+ now = datetime.datetime.now() |
+ diff = now - starttime |
+ if (diff.seconds * 1000 * 1000 + diff.microseconds) > (timeout * 1000): |
+ self.kill(group) |
+ else: |
+ self.returncode = winprocess.GetExitCodeProcess(self._handle) |
+ else: |
+ if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')): |
+ 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) |