view win32.py @ 80:809b77302b21

Win32-specific module with memory and CPU time limits The Win32-specific implementation of call() and friends now lives in module win32, looks clean and in addition is able to enforce memory and CPU time limits on NT kernels, in particular on Windows 2000 and up asking the system to terminate the process as soon as or (in the case of CPU time) almost as soon as the limits are broken. According to my observations, malloc() in the limited process does not return NULL when memory usage is close to the limit and instead crashes the process (which Upreckon happily translates into 'memory limit exceeded'). The catch is that the module is not actually used yet; coming soon.
author Oleg Oshmyan <chortos@inbox.lv>
date Wed, 16 Feb 2011 00:01:33 +0000
parents
children 24752db487c5
line wrap: on
line source

# Copyright (c) 2011 Chortos-2 <chortos@inbox.lv>

from __future__ import division, with_statement

try:
	from compat import *
except ImportError:
	import __main__
	__main__.import_error(sys.exc_info()[1])

from __main__ import clock
from ctypes import *
from ctypes.wintypes import *
from subprocess import Popen

# Defaults that may be overwritten by values from _subprocess
INFINITE = -1
STD_INPUT_HANDLE = -10
WAIT_OBJECT_0 = 0

try:
	from _subprocess import *
except ImportError:
	pass

try:
	from numbers import Integral
except ImportError:
	Integral = int, long

try:
	from collections import namedtuple
except ImportError:
	from operator import itemgetter
	class ProcessTimes(tuple):
		__slots__ = ()
		__new__ = lambda cls, kernel, user: tuple.__new__(cls, (kernel, user))
		__getnewargs__ = lambda self: tuple(self)
		kernel, user = (property(itemgetter(i)) for i in (0, 1))
else:
	ProcessTimes = namedtuple('ProcessTimes', 'kernel user')

__all__ = 'call', 'kill', 'terminate'


# Automatically convert _subprocess handle objects into low-level HANDLEs
# and replicate their functionality for our own use
try:
	_subprocess_handle = type(GetCurrentProcess())
except NameError:
	_subprocess_handle = Integral
class Handle(object):
	@staticmethod
	def from_param(handle):
		if isinstance(handle, (_subprocess_handle, Integral)):
			return HANDLE(int(handle))
		elif isinstance(handle, Handle):
			return HANDLE(handle.handle)
		elif isinstance(handle, HANDLE):
			return handle
		else:
			raise TypeError('cannot convert %s to a handle' %
			                type(handle).__name__)
	
	__slots__ = 'handle'
	
	def __init__(self, handle):
		if isinstance(handle, Integral):
			self.handle = handle
		elif isinstance(handle, HANDLE):
			self.handle = handle.value
		elif isinstance(handle, Handle):
			self.handle = handle.handle
		elif isinstance(handle, _subprocess_handle):
			handle = HANDLE(int(handle))
			flags = DWORD()
			try:
				if windll.kernel32.GetHandleInformation(handle, byref(flags)):
					flags = flags.value
				else:
					flags = 0
			except AttributeError:
				# Available on NT 3.51 and up, NT line only
				flags = 0
			proc = HANDLE(int(GetCurrentProcess()))
			handle = DuplicateHandle(proc, handle, proc, 0, flags & 1, 2)
			self.handle = handle.Detach()
		else:
			raise TypeError("Handle() argument must be a handle, not '%s'" %
			                type(name).__name__)
	
	def __int__(self):
		return int(self.handle)
	
	def Detach(self):
		handle = self.handle
		self.handle = None
		return handle
	
	# This is also __del__, so only locals are accessed
	def Close(self, _CloseHandle=windll.kernel32.CloseHandle, _HANDLE=HANDLE):
		if self.handle:
			_CloseHandle(_HANDLE(self.handle))
			self.handle = None
	__del__ = Close

CHAR = c_char
INVALID_HANDLE_VALUE = HANDLE(-1).value
LPDWORD = POINTER(DWORD)
LPFILETIME = POINTER(FILETIME)
SIZE_T = ULONG_PTR = WPARAM
ULONGLONG = c_ulonglong

try:
	unicode
except NameError:
	LPCTSTR = LPCWSTR
	unisuffix = 'W'
else:
	LPCTSTR = LPCSTR
	unisuffix = 'A'


prototype = WINFUNCTYPE(BOOL, Handle,
                        LPFILETIME, LPFILETIME, LPFILETIME, LPFILETIME)
flags = ((1, 'process'),
         (2, 'creation'), (2, 'exit'), (2, 'kernel'), (2, 'user'))
try:
	GetProcessTimes = prototype(('GetProcessTimes', windll.kernel32), flags)
except AttributeError:
	# Available on NT 3.5 and up, NT line only
	GetProcessTimes = None
else:
	def errcheck(result, func, args):
		if not result: raise WinError()
		ftimes = [t.dwHighDateTime << 32 | t.dwLowDateTime for t in args[3:]]
		kernel = ftimes[0] / 10000000
		user   = ftimes[1] / 10000000
		return ProcessTimes(kernel, user)
	GetProcessTimes.errcheck = errcheck


class PROCESS_MEMORY_COUNTERS(Structure):
	_fields_ = (('cb', DWORD),
	            ('PageFaultCount', DWORD),
	            ('PeakWorkingSetSize', SIZE_T),
	            ('WorkingSetSize', SIZE_T),
	            ('QuotaPeakPagedPoolUsage', SIZE_T),
	            ('QuotaPagedPoolUsage', SIZE_T),
	            ('QuotaPeakNonPagedPoolUsage', SIZE_T),
	            ('QuotaNonPagedPoolUsage', SIZE_T),
	            ('PagefileUsage', SIZE_T),
	            ('PeakPagefileUsage', SIZE_T))

prototype = WINFUNCTYPE(BOOL, Handle, POINTER(PROCESS_MEMORY_COUNTERS), DWORD)
flags = ((1, 'process'), (2, 'counters'),
         (5, 'cb', sizeof(PROCESS_MEMORY_COUNTERS)))
try:
	GetProcessMemoryInfo = prototype(('GetProcessMemoryInfo', windll.psapi),
	                                 flags)
except AttributeError:
	# Available on NT 4.0 and up, NT line only
	GetProcessMemoryInfo = None
else:
	def errcheck(result, func, args):
		if not result: raise WinError()
		return args
	GetProcessMemoryInfo.errcheck = errcheck


class _uChar_union(Union):
	_fields_ = (('UnicodeChar', WCHAR),
	            ('AsciiChar', CHAR))

class KEY_EVENT_RECORD(Structure):
	_fields_ = (('bKeyDown', BOOL),
	            ('wRepeatCount', WORD),
	            ('wVirtualKeyCode', WORD),
	            ('wVirtualScanCode', WORD),
	            ('uChar', _uChar_union),
	            ('dwControlKeyState', DWORD))

RIGHT_ALT_PRESSED  = 0x001
LEFT_ALT_PRESSED   = 0x002
RIGHT_CTRL_PRESSED = 0x004
LEFT_CTRL_PRESSED  = 0x008
SHIFT_PRESSED      = 0x010
NUMLOCK_ON         = 0x020
SCROLLLOCK_ON      = 0x040
CAPSLOCK_ON        = 0x080
ENHANCED_KEY       = 0x100

class _Event_union(Union):
	_fields_ = ('KeyEvent', KEY_EVENT_RECORD),

class INPUT_RECORD(Structure):
	_fields_ = (('EventType', WORD),
	            ('Event', _Event_union))

KEY_EVENT                = 0x01
MOUSE_EVENT              = 0x02
WINDOW_BUFFER_SIZE_EVENT = 0x04
MENU_EVENT               = 0x08
FOCUS_EVENT              = 0x10

prototype = WINFUNCTYPE(BOOL, Handle, POINTER(INPUT_RECORD), DWORD, LPDWORD)
flags = (1, 'input'), (2, 'buffer'), (5, 'length', 1), (2, 'number_read')
ReadConsoleInput = prototype(('ReadConsoleInputA', windll.kernel32), flags)
def errcheck(result, func, args):
	if not result: raise WinError()
	return args[1] if args[3] else None
ReadConsoleInput.errcheck = errcheck


prototype = WINFUNCTYPE(BOOL, Handle)
flags = (1, 'input'),
FlushConsoleInputBuffer = prototype(('FlushConsoleInputBuffer',
                                     windll.kernel32), flags)
def errcheck(result, func, args):
	if not result: raise WinError()
FlushConsoleInputBuffer.errcheck = errcheck


prototype = WINFUNCTYPE(BOOL, Handle, DWORD)
flags = (1, 'console'), (1, 'mode')
SetConsoleMode = prototype(('SetConsoleMode', windll.kernel32), flags)
def errcheck(result, func, args):
	if not result: raise WinError()
SetConsoleMode.errcheck = errcheck

ENABLE_PROCESSED_INPUT = 0x001
ENABLE_LINE_INPUT      = 0x002
ENABLE_ECHO_INPUT      = 0x004
ENABLE_WINDOW_INPUT    = 0x008
ENABLE_MOUSE_INPUT     = 0x010
ENABLE_INSERT_MODE     = 0x020
ENABLE_QUICK_EDIT_MODE = 0x040
ENABLE_EXTENDED_FLAGS  = 0x080

ENABLE_PROCESSED_OUTPUT   = 1
ENABLE_WRAP_AT_EOL_OUTPUT = 2


prototype = WINFUNCTYPE(HANDLE, c_void_p, LPCTSTR)
flags = (5, 'attributes'), (1, 'name')
try:
	CreateJobObject = prototype(('CreateJobObject'+unisuffix, windll.kernel32),
	                            flags)
except AttributeError:
	# Available on 2000 and up, NT line only
	CreateJobObject = lambda name: None
else:
	def errcheck(result, func, args):
		if not result: raise WinError()
		return Handle(result)
	CreateJobObject.errcheck = errcheck


prototype = WINFUNCTYPE(BOOL, Handle, Handle)
flags = (1, 'job'), (1, 'handle')
try:
	AssignProcessToJobObject = prototype(('AssignProcessToJobObject',
	                                      windll.kernel32), flags)
except AttributeError:
	# Available on 2000 and up, NT line only
	AssignProcessToJobObject = lambda job, handle: None
else:
	def errcheck(result, func, args):
		if not result: raise WinError()
	AssignProcessToJobObject.errcheck = errcheck


class JOBOBJECT_BASIC_LIMIT_INFORMATION(Structure):
	_fields_ = (('PerProcessUserTimeLimit', LARGE_INTEGER),
	            ('PerJobUserTimeLimit', LARGE_INTEGER),
	            ('LimitFlags', DWORD),
	            ('MinimumWorkingSetSize', SIZE_T),
	            ('MaximumWorkingSetSize', SIZE_T),
	            ('ActiveProcessLimit', DWORD),
	            ('Affinity', ULONG_PTR),
	            ('PriorityClass', DWORD),
	            ('SchedulingClass', DWORD))

JOB_OBJECT_LIMIT_WORKINGSET                 = 0x0001
JOB_OBJECT_LIMIT_PROCESS_TIME               = 0x0002
JOB_OBJECT_LIMIT_JOB_TIME                   = 0x0004
JOB_OBJECT_LIMIT_ACTIVE_PROCESS             = 0x0008
JOB_OBJECT_LIMIT_AFFINITY                   = 0x0010
JOB_OBJECT_LIMIT_PRIORITY_CLASS             = 0x0020
JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME          = 0x0040
JOB_OBJECT_LIMIT_SCHEDULING_CLASS           = 0x0080
JOB_OBJECT_LIMIT_PROCESS_MEMORY             = 0x0100
JOB_OBJECT_LIMIT_JOB_MEMORY                 = 0x0200
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x0400
JOB_OBJECT_LIMIT_BREAKAWAY_OK               = 0x0800
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK        = 0x1000
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE          = 0x2000
JOB_OBJECT_LIMIT_SUBSET_AFFINITY            = 0x4000

class IO_COUNTERS(Structure):
	_fields_ = (('ReadOperationCount', ULONGLONG),
	            ('WriteOperationCount', ULONGLONG),
	            ('OtherOperationCount', ULONGLONG),
	            ('ReadTransferCount', ULONGLONG),
	            ('WriteTransferCount', ULONGLONG),
	            ('OtherTransferCount', ULONGLONG))

class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(Structure):
	_fields_ = (('BasicLimitInformation', JOBOBJECT_BASIC_LIMIT_INFORMATION),
	            ('IoInfo', IO_COUNTERS),
	            ('ProcessMemoryLimit', SIZE_T),
	            ('JobMemoryLimit', SIZE_T),
	            ('PeakProcessMemoryUsed', SIZE_T),
	            ('PeakJobMemoryUsed', SIZE_T))

prototype = WINFUNCTYPE(BOOL, HANDLE, c_int, c_void_p, DWORD)
flags = (1, 'job'), (1, 'infoclass'), (1, 'info'), (1, 'infosize')
try:
	_setjobinfo = prototype(('SetInformationJobObject',windll.kernel32), flags)
except AttributeError:
	# Available on 2000 and up, NT line only
	SetInformationJobObject = lambda job, infoclass, info: None
else:
	def errcheck(result, func, args):
		if not result: raise WinError()
	_setjobinfo.errcheck = errcheck
	def SetInformationJobObject(job, infoclass, info):
		return _setjobinfo(job, infoclass, info, sizeof(info))

(
	JobObjectBasicAccountingInformation,
	JobObjectBasicLimitInformation,
	JobObjectBasicProcessIdList,
	JobObjectBasicUIRestrictions,
	JobObjectSecurityLimitInformation,   
	JobObjectEndOfJobTimeInformation,
	JobObjectAssociateCompletionPortInformation,
	JobObjectBasicAndIoAccountingInformation,
	JobObjectExtendedLimitInformation,
	JobObjectJobSetInformation,
	MaxJobObjectInfoClass
) = range(1, 12)


prototype = WINFUNCTYPE(DWORD, DWORD, POINTER(HANDLE), BOOL, DWORD)
flags = (1, 'count'), (1, 'handles'), (1, 'wait_all'), (1, 'milliseconds')
_wait_multiple = prototype(('WaitForMultipleObjects', windll.kernel32), flags)
def errcheck(result, func, args):
	if result == WAIT_FAILED: raise WinError()
	return args
_wait_multiple.errcheck = errcheck
def WaitForMultipleObjects(handles, wait_all, timeout):
	n = len(handles)
	handles = (Handle.from_param(handle) for handle in handles)
	timeout = ceil(timeout * 1000)
	return _wait_multiple(n, (HANDLE * n)(*handles), wait_all, timeout)

# WAIT_OBJECT_0 defined at the top of the file
WAIT_ABANDONED_0 = 0x00000080
WAIT_TIMEOUT     = 0x00000102
WAIT_FAILED      = 0xFFFFFFFF


try:
	_wait_single = WaitForSingleObject
except NameError:
	prototype = WINFUNCTYPE(DWORD, Handle, DWORD)
	flags = (1, 'handle'), (1, 'milliseconds')
	_wait_single = prototype(('WaitForSingleObject', windll.kernel32), flags)
	def errcheck(result, func, args):
		if result == WAIT_FAILED: raise WinError()
		return args
	_wait_single.errcheck = errcheck
def WaitForSingleObject(handle, timeout):
	return _wait_single(handle, ceil(timeout * 1000))


try:
	GetStdHandle
except NameError:
	prototype = WINFUNCTYPE(HANDLE, DWORD)
	flags = (1, 'which'),
	GetStdHandle = prototype(('GetStdHandle', windll.kernel32), flags)
	def errcheck(result, func, args):
		if result == INVALID_HANDLE_VALUE: raise WinError()
		return args if result else None
	GetStdHandle.errcheck = errcheck


try:
	TerminateProcess
except NameError:
	prototype = WINFUNCTYPE(BOOL, Handle, UINT)
	flags = (1, 'process'), (1, 'exitcode')
	TerminateProcess = prototype(('TerminateProcess', windll.kernel32), flags)
	def errcheck(result, func, args):
		if not result: raise WinError()
	TerminateProcess.errcheck = errcheck


# Do not show error messages due to errors in the program being tested
try:
	errmode = ctypes.windll.kernel32.GetErrorMode()
except AttributeError:
	# GetErrorMode is available on Vista/2008 and up
	errmode = ctypes.windll.kernel32.SetErrorMode(0)
ctypes.windll.kernel32.SetErrorMode(errmode | 0x8003)

stdin = GetStdHandle(STD_INPUT_HANDLE)
try:
	SetConsoleMode(stdin, ENABLE_PROCESSED_INPUT)
except WindowsError:
	console_input = False
else:
	console_input = True
	FlushConsoleInputBuffer(stdin)

def kill(process):
	try:
		process.terminate()
	except AttributeError:
		TerminateProcess(process._handle)
terminate = kill

def call(*args, **kwargs):
	case = kwargs.pop('case')
	job = CreateJobObject(None)
	flags = 0
	if case.maxcputime:
		flags |= JOB_OBJECT_LIMIT_PROCESS_TIME
	if case.maxmemory:
		flags |= JOB_OBJECT_LIMIT_PROCESS_MEMORY
	limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION(
		JOBOBJECT_BASIC_LIMIT_INFORMATION(
			PerProcessUserTimeLimit=ceil((case.maxcputime or 0)*10000000),
			LimitFlags=flags,
		),
		ProcessMemoryLimit=ceil((case.maxmemory or 0)*1048576),
	)
	SetInformationJobObject(job, JobObjectExtendedLimitInformation, limits)
	try:
		case.process = Popen(*args, **kwargs)
	except OSError:
		raise CannotStartTestee(sys.exc_info()[1])
	case.time_started = clock()
	AssignProcessToJobObject(job, case.process._handle)
	if not console_input:
		if case.maxwalltime:
			if (WaitForSingleObject(case.process._handle, case.maxwalltime) !=
			    WAIT_OBJECT_0):
				raise TimeLimitExceeded
		else:
			case.process.wait()
	else:
		handles = stdin, case.process._handle
		if case.maxwalltime:
			time_end = clock() + case.maxwalltime
			while case.process.poll() is None:
				remaining = time_end - clock()
				if remaining > 0:
					if (WaitForMultipleObjects(handles, False, remaining) ==
					    WAIT_OBJECT_0):
						ir = ReadConsoleInput(stdin)
						if (ir and
						    ir.EventType == 1 and
						    ir.Event.KeyEvent.bKeyDown and
						    ir.Event.KeyEvent.wVirtualKeyCode == 27):
							raise CanceledByUser
				else:
					raise TimeLimitExceeded
		else:
			while case.process.poll() is None:
				if (WaitForMultipleObjects(handles, False, INFINITE) ==
				    WAIT_OBJECT_0):
					ir = ReadConsoleInput(stdin)
					if (ir and
					    ir.EventType == 1 and
					    ir.Event.KeyEvent.bKeyDown and
					    ir.Event.KeyEvent.wVirtualKeyCode == 27):
						raise CanceledByUser
	case.time_stopped = clock()
	if case.maxcputime and GetProcessTimes:
		try:
			times = GetProcessTimes(case.process._handle)
		except WindowsError:
			pass
		else:
			if times.kernel + times.user > case.maxcputime:
				raise TimeLimitExceeded
	if case.maxmemory and GetProcessMemoryInfo:
		try:
			counters = GetProcessMemoryInfo(case.process._handle)
		except WindowsError:
			pass
		else:
			if counters.PeakPagefileUsage > case.maxmemory * 1048576:
				raise MemoryLimitExceeded