Mercurial > ~astiob > upreckon > hgweb
comparison unix.py @ 82:06356af50bf9
Finished testcases reorganization and CPU time limit implementation
We now have:
* Win32-specific code in the win32 module (including bug fixes),
* UNIX-specific and generic code in the unix module,
* a much cleaner testcases module,
* wait4-based resource limits working on Python 3 (this is a bug fix),
* no warning/error reported on non-Win32 when -x is not passed
but standard input does not come from a terminal,
* the maxtime configuration variable replaced with two new variables
named maxcputime and maxwalltime,
* CPU time reported if it can be determined unless an error occurs sooner
than it is determined (e. g. if the wall-clock time limit is exceeded),
* memory limits enforced even if Upreckon's forking already breaks them,
* CPU time limits and private virtual memory limits honoured on Win32,
* CPU time limits honoured on UNIX(-like) platforms supporting wait4
or getrusage,
* address space limits honoured on UNIX(-like) platforms supporting
setrlimit with RLIMIT_AS/RLIMIT_VMEM,
* resident set size limits honoured on UNIX(-like) platforms supporting
wait4.
| author | Oleg Oshmyan <chortos@inbox.lv> |
|---|---|
| date | Wed, 23 Feb 2011 23:35:27 +0000 |
| parents | |
| children | 741ae3391b61 |
comparison
equal
deleted
inserted
replaced
| 81:24752db487c5 | 82:06356af50bf9 |
|---|---|
| 1 # Copyright (c) 2010-2011 Chortos-2 <chortos@inbox.lv> | |
| 2 | |
| 3 from __future__ import division, with_statement | |
| 4 import sys | |
| 5 | |
| 6 try: | |
| 7 from compat import * | |
| 8 import testcases # mutual import | |
| 9 except ImportError: | |
| 10 import __main__ | |
| 11 __main__.import_error(sys.exc_info()[1]) | |
| 12 | |
| 13 from __main__ import clock | |
| 14 from subprocess import Popen | |
| 15 import os, sys | |
| 16 | |
| 17 try: | |
| 18 from signal import SIGTERM, SIGKILL | |
| 19 except ImportError: | |
| 20 SIGTERM = 15 | |
| 21 SIGKILL = 9 | |
| 22 | |
| 23 __all__ = 'call', 'kill', 'terminate', 'pause' | |
| 24 | |
| 25 | |
| 26 if not sys.stdin.isatty(): | |
| 27 pause = lambda: sys.stdin.read(1) | |
| 28 catch_escape = False | |
| 29 else: | |
| 30 try: | |
| 31 from select import select | |
| 32 import termios, tty, atexit | |
| 33 except ImportError: | |
| 34 pause = None | |
| 35 catch_escape = False | |
| 36 else: | |
| 37 catch_escape = True | |
| 38 def cleanup(old=termios.tcgetattr(sys.stdin.fileno())): | |
| 39 termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, old) | |
| 40 atexit.register(cleanup) | |
| 41 tty.setcbreak(sys.stdin.fileno()) | |
| 42 def pause(): | |
| 43 sys.stdin.read(1) | |
| 44 | |
| 45 try: | |
| 46 from signal import SIGCHLD, signal, SIG_DFL | |
| 47 from select import select, error as SelectError | |
| 48 from errno import EINTR | |
| 49 from fcntl import fcntl, F_SETFD, F_GETFD | |
| 50 try: | |
| 51 import cPickle as pickle | |
| 52 except ImportError: | |
| 53 import pickle | |
| 54 except ImportError: | |
| 55 def call(*args, **kwargs): | |
| 56 case = kwargs.pop('case') | |
| 57 try: | |
| 58 case.process = Popen(*args, **kwargs) | |
| 59 except OSError: | |
| 60 raise CannotStartTestee(sys.exc_info()[1]) | |
| 61 case.time_started = clock() | |
| 62 if not case.maxtime: | |
| 63 while True: | |
| 64 exitcode, now = case.process.poll(), clock() | |
| 65 if exitcode is not None: | |
| 66 case.time_stopped = now | |
| 67 break | |
| 68 else: | |
| 69 time.sleep(.001) | |
| 70 else: | |
| 71 time_end = case.time_started + case.maxtime | |
| 72 while True: | |
| 73 exitcode, now = case.process.poll(), clock() | |
| 74 if exitcode is not None: | |
| 75 case.time_stopped = now | |
| 76 break | |
| 77 elif now >= time_end: | |
| 78 raise TimeLimitExceeded | |
| 79 else: | |
| 80 time.sleep(.001) | |
| 81 else: | |
| 82 try: | |
| 83 from fcntl import FD_CLOEXEC | |
| 84 except ImportError: | |
| 85 FD_CLOEXEC = 1 | |
| 86 | |
| 87 try: | |
| 88 from resource import getrusage, RUSAGE_SELF, RUSAGE_CHILDREN | |
| 89 except ImportError: | |
| 90 from time import clock as cpuclock | |
| 91 getrusage = lambda who: None | |
| 92 else: | |
| 93 def cpuclock(): | |
| 94 rusage = getrusage(RUSAGE_SELF) | |
| 95 return rusage.ru_utime + rusage.ru_stime | |
| 96 | |
| 97 try: | |
| 98 from resource import setrlimit | |
| 99 try: | |
| 100 from resource import RLIMIT_AS | |
| 101 except ImportError: | |
| 102 from resource import RLIMIT_VMEM | |
| 103 except ImportError: | |
| 104 setrlimit = None | |
| 105 | |
| 106 # Make SIGCHLD interrupt sleep() and select() | |
| 107 def bury_child(signum, frame): | |
| 108 try: | |
| 109 bury_child.case.time_stopped = clock() | |
| 110 except Exception: | |
| 111 pass | |
| 112 signal(SIGCHLD, bury_child) | |
| 113 | |
| 114 # If you want this to work portably, don't set any stdio argument to PIPE | |
| 115 def call(*args, **kwargs): | |
| 116 global last_rusage | |
| 117 bury_child.case = case = kwargs.pop('case') | |
| 118 read, write = os.pipe() | |
| 119 fcntl(write, F_SETFD, fcntl(write, F_GETFD) | FD_CLOEXEC) | |
| 120 def preexec_fn(): | |
| 121 os.close(read) | |
| 122 if setrlimit and case.maxmemory: | |
| 123 maxmemory = ceil(case.maxmemory * 1048576) | |
| 124 setrlimit(RLIMIT_AS, (maxmemory, maxmemory)) | |
| 125 # I would also set a CPU time limit but I do not want the time | |
| 126 # passing between the calls to fork and exec to be counted in | |
| 127 os.write(write, pickle.dumps((clock(), cpuclock()), 1)) | |
| 128 kwargs['preexec_fn'] = preexec_fn | |
| 129 old_rusage = getrusage(RUSAGE_CHILDREN) | |
| 130 last_rusage = None | |
| 131 try: | |
| 132 case.process = Popen(*args, **kwargs) | |
| 133 except OSError: | |
| 134 os.close(read) | |
| 135 raise testcases.CannotStartTestee(sys.exc_info()[1]) | |
| 136 finally: | |
| 137 os.close(write) | |
| 138 try: | |
| 139 if not catch_escape: | |
| 140 if case.maxwalltime: | |
| 141 time.sleep(case.maxwalltime) | |
| 142 if case.process.poll() is None: | |
| 143 raise testcases.WallTimeLimitExceeded | |
| 144 else: | |
| 145 case.process.wait() | |
| 146 else: | |
| 147 if not case.maxwalltime: | |
| 148 try: | |
| 149 while case.process.poll() is None: | |
| 150 if select((sys.stdin,), (), ())[0]: | |
| 151 if sys.stdin.read(1) == '\33': | |
| 152 raise testcases.CanceledByUser | |
| 153 except SelectError: | |
| 154 if sys.exc_info()[1].args[0] != EINTR: | |
| 155 raise | |
| 156 else: | |
| 157 case.process.poll() | |
| 158 else: | |
| 159 time_end = clock() + case.maxwalltime | |
| 160 try: | |
| 161 while case.process.poll() is None: | |
| 162 remaining = time_end - clock() | |
| 163 if remaining > 0: | |
| 164 if select((sys.stdin,), (), (), remaining)[0]: | |
| 165 if sys.stdin.read(1) == '\33': | |
| 166 raise testcases.CanceledByUser | |
| 167 else: | |
| 168 raise testcases.WallTimeLimitExceeded | |
| 169 except SelectError: | |
| 170 if sys.exc_info()[1].args[0] != EINTR: | |
| 171 raise | |
| 172 else: | |
| 173 case.process.poll() | |
| 174 finally: | |
| 175 case.time_started, cpustart = pickle.loads(os.read(read, 512)) | |
| 176 os.close(read) | |
| 177 del bury_child.case | |
| 178 new_rusage = getrusage(RUSAGE_CHILDREN) | |
| 179 if new_rusage and (case.maxcputime or not case.maxwalltime): | |
| 180 case.time_started = cpustart | |
| 181 case.time_stopped = new_rusage.ru_utime + new_rusage.ru_stime | |
| 182 case.time_limit_string = case.cpu_time_limit_string | |
| 183 if case.maxcputime and new_rusage: | |
| 184 oldtime = old_rusage.ru_utime + old_rusage.ru_stime | |
| 185 newtime = new_rusage.ru_utime + new_rusage.ru_stime | |
| 186 if newtime - oldtime - cpustart > case.maxcputime: | |
| 187 raise testcases.CPUTimeLimitExceeded | |
| 188 if case.maxmemory: | |
| 189 if sys.platform != 'darwin': | |
| 190 maxrss = case.maxmemory * 1024 | |
| 191 else: | |
| 192 maxrss = case.maxmemory * 1048576 | |
| 193 if last_rusage and last_rusage.ru_maxrss > maxrss: | |
| 194 raise testcases.MemoryLimitExceeded | |
| 195 elif (new_rusage and | |
| 196 new_rusage.ru_maxrss > old_rusage.ru_maxrss and | |
| 197 new_rusage.ru_maxrss > maxrss): | |
| 198 raise testcases.MemoryLimitExceeded | |
| 199 | |
| 200 # Emulate memory limits on platforms compatible with 4.3BSD but not XSI | |
| 201 # I say 'emulate' because the OS will allow excessive memory usage | |
| 202 # anyway; Upreckon will just treat the test case as not passed. | |
| 203 # To do this, we not only require os.wait4 to be present but also | |
| 204 # assume things about the implementation of subprocess.Popen. | |
| 205 try: | |
| 206 def waitpid_emu(pid, options, _wait4=os.wait4): | |
| 207 global last_rusage | |
| 208 pid, status, last_rusage = _wait4(pid, options) | |
| 209 return pid, status | |
| 210 _waitpid = os.waitpid | |
| 211 os.waitpid = waitpid_emu | |
| 212 try: | |
| 213 defaults = Popen._internal_poll.__func__.__defaults__ | |
| 214 except AttributeError: | |
| 215 # Python 2.5 | |
| 216 defaults = Popen._internal_poll.im_func.func_defaults | |
| 217 i = defaults.index(_waitpid) | |
| 218 defaults = defaults[:i] + (waitpid_emu,) + defaults[i+1:] | |
| 219 try: | |
| 220 Popen._internal_poll.__func__.__defaults__ = defaults | |
| 221 except AttributeError: | |
| 222 # Python 2.5 again | |
| 223 Popen._internal_poll.im_func.func_defaults = defaults | |
| 224 except (AttributeError, ValueError): | |
| 225 pass | |
| 226 | |
| 227 | |
| 228 def kill(process): | |
| 229 try: | |
| 230 process.kill() | |
| 231 except AttributeError: | |
| 232 os.kill(process.pid, SIGKILL) | |
| 233 | |
| 234 | |
| 235 def terminate(process): | |
| 236 try: | |
| 237 process.terminate() | |
| 238 except AttributeError: | |
| 239 os.kill(process.pid, SIGTERM) |
