Mercurial > ~astiob > upreckon > hgweb
view upreckon/unix.py @ 205:166a23999bf7
Added confvar okexitcodemask; changed the validator protocol
Callable validators now return three-tuples (number granted, bool correct,
str comment) instead of two-tuples (number granted, str comment). They are
still allowed to return single numbers.
Callable validators must now explicitly raise
upreckon.exceptions.WrongAnswer if they want the verdict to be Wrong
Answer rather than Partly Correct.
okexitcodemask specifies a bitmask ANDed with the exit code of the
external validator to get a boolean flag showing whether the answer is to
be marked as 'OK' rather than 'partly correct'. The bits covered by the
bitmask are reset to zeroes before devising the number of points granted
from the resulting number.
author | Oleg Oshmyan <chortos@inbox.lv> |
---|---|
date | Wed, 17 Aug 2011 20:44:54 +0300 |
parents | d5b6708c1955 |
children | 65b5c9390010 |
line wrap: on
line source
# Copyright (c) 2010-2011 Chortos-2 <chortos@inbox.lv> from __future__ import division, with_statement from .compat import * from .exceptions import * from subprocess import Popen import os, sys, time if sys.platform.startswith('java'): from time import clock else: from time import time as clock try: from signal import SIGTERM, SIGKILL except ImportError: SIGTERM = 15 SIGKILL = 9 __all__ = 'call', 'kill', 'pause', 'clock' try: from signal import SIGCHLD, SIG_DFL, signal, set_wakeup_fd from select import select, error as SelectError from errno import EAGAIN, EINTR from fcntl import fcntl, F_SETFD, F_GETFD, F_SETFL, F_GETFL from os import O_NONBLOCK 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.maxwalltime: 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.maxwalltime while True: exitcode, now = case.process.poll(), clock() if exitcode is not None: case.time_stopped = now break elif now >= time_end: raise WallTimeLimitExceeded else: time.sleep(.001) else: try: from fcntl import FD_CLOEXEC except ImportError: FD_CLOEXEC = 1 try: from signal import siginterrupt except ImportError: # Sucks. siginterrupt = lambda signalnum, flag: None 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 as RLIMIT_AS except ImportError: setrlimit = None # Make SIGCHLD interrupt sleep() and select() sigchld_pipe_read, sigchld_pipe_write = os.pipe() fcntl(sigchld_pipe_read, F_SETFL, fcntl(sigchld_pipe_read, F_GETFL) | O_NONBLOCK) fcntl(sigchld_pipe_write, F_SETFL, fcntl(sigchld_pipe_write, F_GETFL) | O_NONBLOCK) def bury_child(signum, frame): try: bury_child.case.time_stopped = clock() except Exception: pass signal(SIGCHLD, bury_child) set_wakeup_fd(sigchld_pipe_write) class SignalIgnorer(object): def __enter__(self): signal(SIGCHLD, SIG_DFL) def __exit__(self, exc_type, exc_value, traceback): signal(SIGCHLD, bury_child) signal_ignorer = SignalIgnorer() __all__ += 'signal_ignorer', # 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 # So how the hell do I actually make use of pass_fds? # On 3.1-, calling Popen with pass_fds prints an exception # from Popen.__del__ to stderr. On 3.2, Popen without close_fds # or pass_fds creates a child and fails but that of course # generates a SIGCHLD, which causes problems, and I have # no process ID to wait upon to negate the changes made # by the SIGCHLD handler. kwargs['close_fds'] = False old_rusage = getrusage(RUSAGE_CHILDREN) last_rusage = None while True: try: os.read(sigchld_pipe_read, 512) except OSError: if sys.exc_info()[1].errno == EAGAIN: break else: raise siginterrupt(SIGCHLD, False) try: case.process = Popen(*args, **kwargs) except OSError: os.close(read) raise CannotStartTestee(sys.exc_info()[1]) finally: siginterrupt(SIGCHLD, True) os.close(write) try: if not catch_escape: if case.maxwalltime: try: select((sigchld_pipe_read,), (), (), case.maxwalltime) except SelectError: if sys.exc_info()[1].args[0] != EINTR: raise # subprocess in Python 2.6- is not guarded against EINTR try: if case.process.poll() is None: raise WallTimeLimitExceeded except OSError: if sys.exc_info()[1].errno != EINTR: raise else: case.process.poll() else: wait(case.process) else: if not case.maxwalltime: try: while case.process.poll() is None: s = select((sys.stdin, sigchld_pipe_read), (), ()) if (s[0] == [sys.stdin] and sys.stdin.read(1) == '\33'): raise CanceledByUser except (SelectError, IOError, OSError): 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: s = select((sys.stdin, sigchld_pipe_read), (), (), remaining) if (s[0] == [sys.stdin] and sys.stdin.read(1) == '\33'): raise CanceledByUser else: raise WallTimeLimitExceeded except (SelectError, IOError, OSError): 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 (case.maxwalltime and case.time_stopped - case.time_started > case.maxwalltime): raise WallTimeLimitExceeded if new_rusage: time_started = old_rusage.ru_utime + old_rusage.ru_stime + cpustart time_stopped = new_rusage.ru_utime + new_rusage.ru_stime # Yes, this actually happens if time_started > time_stopped: time_started = time_stopped if case.maxcputime or not case.maxwalltime: case.time_started = time_started case.time_stopped = time_stopped case.time_limit_string = case.cpu_time_limit_string if (case.maxcputime and time_stopped - time_started > case.maxcputime): raise 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 MemoryLimitExceeded elif (new_rusage and new_rusage.ru_maxrss > old_rusage.ru_maxrss and new_rusage.ru_maxrss > maxrss): raise 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) wait(process) # subprocess in Python 2.6- is not guarded against EINTR try: from errno import EINTR except ImportError: wait = Popen.wait else: def wait(process): while True: try: return process.wait() except OSError: if sys.exc_info()[1].errno != EINTR: raise try: from ._unix import * except ImportError: 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 = lambda: sys.stdin.read(1) 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)