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

Thu, 15 Jan 2015 15:59:08 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 15:59:08 +0100
branch
TOR_BUG_9701
changeset 10
ac0c01689b40
permissions
-rw-r--r--

Implement a real Private Browsing Mode condition by changing the API/ABI;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

michael@0 1 # killableprocess - subprocesses which can be reliably killed
michael@0 2 #
michael@0 3 # Parts of this module are copied from the subprocess.py file contained
michael@0 4 # in the Python distribution.
michael@0 5 #
michael@0 6 # Copyright (c) 2003-2004 by Peter Astrand <astrand@lysator.liu.se>
michael@0 7 #
michael@0 8 # Additions and modifications written by Benjamin Smedberg
michael@0 9 # <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation
michael@0 10 # <http://www.mozilla.org/>
michael@0 11 #
michael@0 12 # More Modifications
michael@0 13 # Copyright (c) 2006-2007 by Mike Taylor <bear@code-bear.com>
michael@0 14 # Copyright (c) 2007-2008 by Mikeal Rogers <mikeal@mozilla.com>
michael@0 15 #
michael@0 16 # By obtaining, using, and/or copying this software and/or its
michael@0 17 # associated documentation, you agree that you have read, understood,
michael@0 18 # and will comply with the following terms and conditions:
michael@0 19 #
michael@0 20 # Permission to use, copy, modify, and distribute this software and
michael@0 21 # its associated documentation for any purpose and without fee is
michael@0 22 # hereby granted, provided that the above copyright notice appears in
michael@0 23 # all copies, and that both that copyright notice and this permission
michael@0 24 # notice appear in supporting documentation, and that the name of the
michael@0 25 # author not be used in advertising or publicity pertaining to
michael@0 26 # distribution of the software without specific, written prior
michael@0 27 # permission.
michael@0 28 #
michael@0 29 # THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
michael@0 30 # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
michael@0 31 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR
michael@0 32 # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
michael@0 33 # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
michael@0 34 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
michael@0 35 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
michael@0 36
michael@0 37 """killableprocess - Subprocesses which can be reliably killed
michael@0 38
michael@0 39 This module is a subclass of the builtin "subprocess" module. It allows
michael@0 40 processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method.
michael@0 41
michael@0 42 It also adds a timeout argument to Wait() for a limited period of time before
michael@0 43 forcefully killing the process.
michael@0 44
michael@0 45 Note: On Windows, this module requires Windows 2000 or higher (no support for
michael@0 46 Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with
michael@0 47 Python 2.5+ or available from http://python.net/crew/theller/ctypes/
michael@0 48 """
michael@0 49
michael@0 50 import subprocess
michael@0 51 import sys
michael@0 52 import os
michael@0 53 import time
michael@0 54 import datetime
michael@0 55 import types
michael@0 56 import exceptions
michael@0 57
michael@0 58 try:
michael@0 59 from subprocess import CalledProcessError
michael@0 60 except ImportError:
michael@0 61 # Python 2.4 doesn't implement CalledProcessError
michael@0 62 class CalledProcessError(Exception):
michael@0 63 """This exception is raised when a process run by check_call() returns
michael@0 64 a non-zero exit status. The exit status will be stored in the
michael@0 65 returncode attribute."""
michael@0 66 def __init__(self, returncode, cmd):
michael@0 67 self.returncode = returncode
michael@0 68 self.cmd = cmd
michael@0 69 def __str__(self):
michael@0 70 return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
michael@0 71
michael@0 72 mswindows = (sys.platform == "win32")
michael@0 73
michael@0 74 if mswindows:
michael@0 75 import winprocess
michael@0 76 else:
michael@0 77 import signal
michael@0 78
michael@0 79 # This is normally defined in win32con, but we don't want
michael@0 80 # to incur the huge tree of dependencies (pywin32 and friends)
michael@0 81 # just to get one constant. So here's our hack
michael@0 82 STILL_ACTIVE = 259
michael@0 83
michael@0 84 def call(*args, **kwargs):
michael@0 85 waitargs = {}
michael@0 86 if "timeout" in kwargs:
michael@0 87 waitargs["timeout"] = kwargs.pop("timeout")
michael@0 88
michael@0 89 return Popen(*args, **kwargs).wait(**waitargs)
michael@0 90
michael@0 91 def check_call(*args, **kwargs):
michael@0 92 """Call a program with an optional timeout. If the program has a non-zero
michael@0 93 exit status, raises a CalledProcessError."""
michael@0 94
michael@0 95 retcode = call(*args, **kwargs)
michael@0 96 if retcode:
michael@0 97 cmd = kwargs.get("args")
michael@0 98 if cmd is None:
michael@0 99 cmd = args[0]
michael@0 100 raise CalledProcessError(retcode, cmd)
michael@0 101
michael@0 102 if not mswindows:
michael@0 103 def DoNothing(*args):
michael@0 104 pass
michael@0 105
michael@0 106 class Popen(subprocess.Popen):
michael@0 107 kill_called = False
michael@0 108 if mswindows:
michael@0 109 def _execute_child(self, *args_tuple):
michael@0 110 # workaround for bug 958609
michael@0 111 if sys.hexversion < 0x02070600: # prior to 2.7.6
michael@0 112 (args, executable, preexec_fn, close_fds,
michael@0 113 cwd, env, universal_newlines, startupinfo,
michael@0 114 creationflags, shell,
michael@0 115 p2cread, p2cwrite,
michael@0 116 c2pread, c2pwrite,
michael@0 117 errread, errwrite) = args_tuple
michael@0 118 to_close = set()
michael@0 119 else: # 2.7.6 and later
michael@0 120 (args, executable, preexec_fn, close_fds,
michael@0 121 cwd, env, universal_newlines, startupinfo,
michael@0 122 creationflags, shell, to_close,
michael@0 123 p2cread, p2cwrite,
michael@0 124 c2pread, c2pwrite,
michael@0 125 errread, errwrite) = args_tuple
michael@0 126
michael@0 127 if not isinstance(args, types.StringTypes):
michael@0 128 args = subprocess.list2cmdline(args)
michael@0 129
michael@0 130 # Always or in the create new process group
michael@0 131 creationflags |= winprocess.CREATE_NEW_PROCESS_GROUP
michael@0 132
michael@0 133 if startupinfo is None:
michael@0 134 startupinfo = winprocess.STARTUPINFO()
michael@0 135
michael@0 136 if None not in (p2cread, c2pwrite, errwrite):
michael@0 137 startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES
michael@0 138
michael@0 139 startupinfo.hStdInput = int(p2cread)
michael@0 140 startupinfo.hStdOutput = int(c2pwrite)
michael@0 141 startupinfo.hStdError = int(errwrite)
michael@0 142 if shell:
michael@0 143 startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW
michael@0 144 startupinfo.wShowWindow = winprocess.SW_HIDE
michael@0 145 comspec = os.environ.get("COMSPEC", "cmd.exe")
michael@0 146 args = comspec + " /c " + args
michael@0 147
michael@0 148 # determine if we can create create a job
michael@0 149 canCreateJob = winprocess.CanCreateJobObject()
michael@0 150
michael@0 151 # set process creation flags
michael@0 152 creationflags |= winprocess.CREATE_SUSPENDED
michael@0 153 creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT
michael@0 154 if canCreateJob:
michael@0 155 # Uncomment this line below to discover very useful things about your environment
michael@0 156 #print "++++ killableprocess: releng twistd patch not applied, we can create job objects"
michael@0 157 creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB
michael@0 158
michael@0 159 # create the process
michael@0 160 hp, ht, pid, tid = winprocess.CreateProcess(
michael@0 161 executable, args,
michael@0 162 None, None, # No special security
michael@0 163 1, # Must inherit handles!
michael@0 164 creationflags,
michael@0 165 winprocess.EnvironmentBlock(env),
michael@0 166 cwd, startupinfo)
michael@0 167 self._child_created = True
michael@0 168 self._handle = hp
michael@0 169 self._thread = ht
michael@0 170 self.pid = pid
michael@0 171 self.tid = tid
michael@0 172
michael@0 173 if canCreateJob:
michael@0 174 # We create a new job for this process, so that we can kill
michael@0 175 # the process and any sub-processes
michael@0 176 self._job = winprocess.CreateJobObject()
michael@0 177 winprocess.AssignProcessToJobObject(self._job, int(hp))
michael@0 178 else:
michael@0 179 self._job = None
michael@0 180
michael@0 181 winprocess.ResumeThread(int(ht))
michael@0 182 ht.Close()
michael@0 183
michael@0 184 if p2cread is not None:
michael@0 185 p2cread.Close()
michael@0 186 if c2pwrite is not None:
michael@0 187 c2pwrite.Close()
michael@0 188 if errwrite is not None:
michael@0 189 errwrite.Close()
michael@0 190 time.sleep(.1)
michael@0 191
michael@0 192 def kill(self, group=True):
michael@0 193 """Kill the process. If group=True, all sub-processes will also be killed."""
michael@0 194 self.kill_called = True
michael@0 195
michael@0 196 if mswindows:
michael@0 197 if group and self._job:
michael@0 198 winprocess.TerminateJobObject(self._job, 127)
michael@0 199 else:
michael@0 200 winprocess.TerminateProcess(self._handle, 127)
michael@0 201 self.returncode = 127
michael@0 202 else:
michael@0 203 if group:
michael@0 204 try:
michael@0 205 os.killpg(self.pid, signal.SIGKILL)
michael@0 206 except: pass
michael@0 207 else:
michael@0 208 os.kill(self.pid, signal.SIGKILL)
michael@0 209 self.returncode = -9
michael@0 210
michael@0 211 def wait(self, timeout=None, group=True):
michael@0 212 """Wait for the process to terminate. Returns returncode attribute.
michael@0 213 If timeout seconds are reached and the process has not terminated,
michael@0 214 it will be forcefully killed. If timeout is -1, wait will not
michael@0 215 time out."""
michael@0 216 if timeout is not None:
michael@0 217 # timeout is now in milliseconds
michael@0 218 timeout = timeout * 1000
michael@0 219
michael@0 220 starttime = datetime.datetime.now()
michael@0 221
michael@0 222 if mswindows:
michael@0 223 if timeout is None:
michael@0 224 timeout = -1
michael@0 225 rc = winprocess.WaitForSingleObject(self._handle, timeout)
michael@0 226
michael@0 227 if (rc == winprocess.WAIT_OBJECT_0 or
michael@0 228 rc == winprocess.WAIT_ABANDONED or
michael@0 229 rc == winprocess.WAIT_FAILED):
michael@0 230 # Object has either signaled, or the API call has failed. In
michael@0 231 # both cases we want to give the OS the benefit of the doubt
michael@0 232 # and supply a little time before we start shooting processes
michael@0 233 # with an M-16.
michael@0 234
michael@0 235 # Returns 1 if running, 0 if not, -1 if timed out
michael@0 236 def check():
michael@0 237 now = datetime.datetime.now()
michael@0 238 diff = now - starttime
michael@0 239 if (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000):
michael@0 240 if self._job:
michael@0 241 if (winprocess.QueryInformationJobObject(self._job, 8)['BasicInfo']['ActiveProcesses'] > 0):
michael@0 242 # Job Object is still containing active processes
michael@0 243 return 1
michael@0 244 else:
michael@0 245 # No job, we use GetExitCodeProcess, which will tell us if the process is still active
michael@0 246 self.returncode = winprocess.GetExitCodeProcess(self._handle)
michael@0 247 if (self.returncode == STILL_ACTIVE):
michael@0 248 # Process still active, continue waiting
michael@0 249 return 1
michael@0 250 # Process not active, return 0
michael@0 251 return 0
michael@0 252 else:
michael@0 253 # Timed out, return -1
michael@0 254 return -1
michael@0 255
michael@0 256 notdone = check()
michael@0 257 while notdone == 1:
michael@0 258 time.sleep(.5)
michael@0 259 notdone = check()
michael@0 260
michael@0 261 if notdone == -1:
michael@0 262 # Then check timed out, we have a hung process, attempt
michael@0 263 # last ditch kill with explosives
michael@0 264 self.kill(group)
michael@0 265
michael@0 266 else:
michael@0 267 # In this case waitforsingleobject timed out. We have to
michael@0 268 # take the process behind the woodshed and shoot it.
michael@0 269 self.kill(group)
michael@0 270
michael@0 271 else:
michael@0 272 if sys.platform in ('linux2', 'sunos5', 'solaris') \
michael@0 273 or sys.platform.startswith('freebsd'):
michael@0 274 def group_wait(timeout):
michael@0 275 try:
michael@0 276 os.waitpid(self.pid, 0)
michael@0 277 except OSError, e:
michael@0 278 pass # If wait has already been called on this pid, bad things happen
michael@0 279 return self.returncode
michael@0 280 elif sys.platform == 'darwin':
michael@0 281 def group_wait(timeout):
michael@0 282 try:
michael@0 283 count = 0
michael@0 284 if timeout is None and self.kill_called:
michael@0 285 timeout = 10 # Have to set some kind of timeout or else this could go on forever
michael@0 286 if timeout is None:
michael@0 287 while 1:
michael@0 288 os.killpg(self.pid, signal.SIG_DFL)
michael@0 289 while ((count * 2) <= timeout):
michael@0 290 os.killpg(self.pid, signal.SIG_DFL)
michael@0 291 # count is increased by 500ms for every 0.5s of sleep
michael@0 292 time.sleep(.5); count += 500
michael@0 293 except exceptions.OSError:
michael@0 294 return self.returncode
michael@0 295
michael@0 296 if timeout is None:
michael@0 297 if group is True:
michael@0 298 return group_wait(timeout)
michael@0 299 else:
michael@0 300 subprocess.Popen.wait(self)
michael@0 301 return self.returncode
michael@0 302
michael@0 303 returncode = False
michael@0 304
michael@0 305 now = datetime.datetime.now()
michael@0 306 diff = now - starttime
michael@0 307 while (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000) and ( returncode is False ):
michael@0 308 if group is True:
michael@0 309 return group_wait(timeout)
michael@0 310 else:
michael@0 311 if subprocess.poll() is not None:
michael@0 312 returncode = self.returncode
michael@0 313 time.sleep(.5)
michael@0 314 now = datetime.datetime.now()
michael@0 315 diff = now - starttime
michael@0 316 return self.returncode
michael@0 317
michael@0 318 return self.returncode
michael@0 319 # We get random maxint errors from subprocesses __del__
michael@0 320 __del__ = lambda self: None
michael@0 321
michael@0 322 def setpgid_preexec_fn():
michael@0 323 os.setpgid(0, 0)
michael@0 324
michael@0 325 def runCommand(cmd, **kwargs):
michael@0 326 if sys.platform != "win32":
michael@0 327 return Popen(cmd, preexec_fn=setpgid_preexec_fn, **kwargs)
michael@0 328 else:
michael@0 329 return Popen(cmd, **kwargs)

mercurial