Mercurial > ~astiob > upreckon > hgweb
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unix.py Wed Feb 23 23:35:27 2011 +0000 @@ -0,0 +1,239 @@ +# 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) \ No newline at end of file