addon-sdk/source/python-lib/mozrunner/killableprocess.py

changeset 0
6474c204b198
     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)

mercurial