Mercurial > ~astiob > upreckon > hgweb
view 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 |
line wrap: on
line source
# Copyright (c) 2010-2011 Chortos-2 <chortos@inbox.lv> from __future__ import division, with_statement import sys try: from compat import * import testcases # mutual import except ImportError: import __main__ __main__.import_error(sys.exc_info()[1]) from __main__ import clock from subprocess import Popen import os, sys try: from signal import SIGTERM, SIGKILL except ImportError: SIGTERM = 15 SIGKILL = 9 __all__ = 'call', 'kill', 'terminate', 'pause' if not sys.stdin.isatty(): pause = lambda: sys.stdin.read(1) catch_escape = False else: try: from select import select import termios, tty, atexit except ImportError: pause = None catch_escape = False else: catch_escape = True def cleanup(old=termios.tcgetattr(sys.stdin.fileno())): termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, old) atexit.register(cleanup) tty.setcbreak(sys.stdin.fileno()) def pause(): sys.stdin.read(1) try: from signal import SIGCHLD, signal, SIG_DFL from select import select, error as SelectError from errno import EINTR from fcntl import fcntl, F_SETFD, F_GETFD try: import cPickle as pickle except ImportError: import pickle except ImportError: def call(*args, **kwargs): case = kwargs.pop('case') try: case.process = Popen(*args, **kwargs) except OSError: raise CannotStartTestee(sys.exc_info()[1]) case.time_started = clock() if not case.maxtime: while True: exitcode, now = case.process.poll(), clock() if exitcode is not None: case.time_stopped = now break else: time.sleep(.001) else: time_end = case.time_started + case.maxtime while True: exitcode, now = case.process.poll(), clock() if exitcode is not None: case.time_stopped = now break elif now >= time_end: raise TimeLimitExceeded else: time.sleep(.001) else: try: from fcntl import FD_CLOEXEC except ImportError: FD_CLOEXEC = 1 try: from resource import getrusage, RUSAGE_SELF, RUSAGE_CHILDREN except ImportError: from time import clock as cpuclock getrusage = lambda who: None else: def cpuclock(): rusage = getrusage(RUSAGE_SELF) return rusage.ru_utime + rusage.ru_stime try: from resource import setrlimit try: from resource import RLIMIT_AS except ImportError: from resource import RLIMIT_VMEM except ImportError: setrlimit = None # Make SIGCHLD interrupt sleep() and select() def bury_child(signum, frame): try: bury_child.case.time_stopped = clock() except Exception: pass signal(SIGCHLD, bury_child) # If you want this to work portably, don't set any stdio argument to PIPE def call(*args, **kwargs): global last_rusage bury_child.case = case = kwargs.pop('case') read, write = os.pipe() fcntl(write, F_SETFD, fcntl(write, F_GETFD) | FD_CLOEXEC) def preexec_fn(): os.close(read) if setrlimit and case.maxmemory: maxmemory = ceil(case.maxmemory * 1048576) setrlimit(RLIMIT_AS, (maxmemory, maxmemory)) # I would also set a CPU time limit but I do not want the time # passing between the calls to fork and exec to be counted in os.write(write, pickle.dumps((clock(), cpuclock()), 1)) kwargs['preexec_fn'] = preexec_fn old_rusage = getrusage(RUSAGE_CHILDREN) last_rusage = None try: case.process = Popen(*args, **kwargs) except OSError: os.close(read) raise testcases.CannotStartTestee(sys.exc_info()[1]) finally: os.close(write) try: if not catch_escape: if case.maxwalltime: time.sleep(case.maxwalltime) if case.process.poll() is None: raise testcases.WallTimeLimitExceeded else: case.process.wait() else: if not case.maxwalltime: try: while case.process.poll() is None: if select((sys.stdin,), (), ())[0]: if sys.stdin.read(1) == '\33': raise testcases.CanceledByUser except SelectError: if sys.exc_info()[1].args[0] != EINTR: raise else: case.process.poll() else: time_end = clock() + case.maxwalltime try: while case.process.poll() is None: remaining = time_end - clock() if remaining > 0: if select((sys.stdin,), (), (), remaining)[0]: if sys.stdin.read(1) == '\33': raise testcases.CanceledByUser else: raise testcases.WallTimeLimitExceeded except SelectError: if sys.exc_info()[1].args[0] != EINTR: raise else: case.process.poll() finally: case.time_started, cpustart = pickle.loads(os.read(read, 512)) os.close(read) del bury_child.case new_rusage = getrusage(RUSAGE_CHILDREN) if new_rusage and (case.maxcputime or not case.maxwalltime): case.time_started = cpustart case.time_stopped = new_rusage.ru_utime + new_rusage.ru_stime case.time_limit_string = case.cpu_time_limit_string if case.maxcputime and new_rusage: oldtime = old_rusage.ru_utime + old_rusage.ru_stime newtime = new_rusage.ru_utime + new_rusage.ru_stime if newtime - oldtime - cpustart > case.maxcputime: raise testcases.CPUTimeLimitExceeded if case.maxmemory: if sys.platform != 'darwin': maxrss = case.maxmemory * 1024 else: maxrss = case.maxmemory * 1048576 if last_rusage and last_rusage.ru_maxrss > maxrss: raise testcases.MemoryLimitExceeded elif (new_rusage and new_rusage.ru_maxrss > old_rusage.ru_maxrss and new_rusage.ru_maxrss > maxrss): raise testcases.MemoryLimitExceeded # Emulate memory limits on platforms compatible with 4.3BSD but not XSI # I say 'emulate' because the OS will allow excessive memory usage # anyway; Upreckon will just treat the test case as not passed. # To do this, we not only require os.wait4 to be present but also # assume things about the implementation of subprocess.Popen. try: def waitpid_emu(pid, options, _wait4=os.wait4): global last_rusage pid, status, last_rusage = _wait4(pid, options) return pid, status _waitpid = os.waitpid os.waitpid = waitpid_emu try: defaults = Popen._internal_poll.__func__.__defaults__ except AttributeError: # Python 2.5 defaults = Popen._internal_poll.im_func.func_defaults i = defaults.index(_waitpid) defaults = defaults[:i] + (waitpid_emu,) + defaults[i+1:] try: Popen._internal_poll.__func__.__defaults__ = defaults except AttributeError: # Python 2.5 again Popen._internal_poll.im_func.func_defaults = defaults except (AttributeError, ValueError): pass def kill(process): try: process.kill() except AttributeError: os.kill(process.pid, SIGKILL) def terminate(process): try: process.terminate() except AttributeError: os.kill(process.pid, SIGTERM)