Thu, 15 Jan 2015 15:59:08 +0100
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) |