Mercurial > ~astiob > upreckon > hgweb
diff upreckon/unix.py @ 146:d5b6708c1955
Distutils support, reorganization and cleaning up
* Removed command-line options -t and -u.
* Reorganized code:
o all modules are now in package upreckon;
o TestCaseNotPassed and its descendants now live in a separate
module exceptions;
o load_problem now lives in module problem.
* Commented out mentions of command-line option -c in --help.
* Added a distutils-based setup.py.
author | Oleg Oshmyan <chortos@inbox.lv> |
---|---|
date | Sat, 28 May 2011 14:24:25 +0100 |
parents | unix.py@ed4035661b85 |
children | 65b5c9390010 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/upreckon/unix.py Sat May 28 14:24:25 2011 +0100 @@ -0,0 +1,310 @@ +# 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)