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