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)