view win32.py @ 125:10aa5a0e46bd

Fixed an exception raised instead of forceful process termination on Win32
author Oleg Oshmyan <chortos@inbox.lv>
date Sun, 15 May 2011 00:33:32 +0100
parents d6fd880207cb
children 42c8f5c152a5
line wrap: on
line source

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

from __future__ import division, with_statement

from compat import *
import testcases  # mutual import

from ctypes import *
from ctypes.wintypes import *
from msvcrt import getch as pause
import os, subprocess, sys

try:
	from _winreg import *
except ImportError:
	from winreg import *

try:
	from testcases import clock
except ImportError:
	from time import clock

# 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', 'pause', 'clock'


from functools import wraps
pathext = [''] + os.environ['PATHEXT'].split(';')
@wraps(subprocess.Popen)
def Popen(cmdline, *args, **kwargs):
	try:
		return subprocess.Popen(cmdline, *args, **kwargs)
	except WindowsError:
		for ext in pathext:
			path = cmdline[0] + ext
			newcmdline = type(cmdline)((path,)) + cmdline[1:]
			try:
				return subprocess.Popen(newcmdline, *args, **kwargs)
			except WindowsError:
				pass
			for branch in HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE:
				try:
					path = (R'SOFTWARE\Microsoft\Windows\CurrentVersion'
					        R'\App Paths\%s%s' % (cmdline[0], ext))
					path = QueryValue(branch, path)
					break
				except WindowsError:
					pass
			else:
				continue
			if path[0] == '"' == path[-1]:
				path = path[1:-1]
			newcmdline = type(cmdline)((path,)) + cmdline[1:]
			try:
				return subprocess.Popen(newcmdline, *args, **kwargs)
			except WindowsError:
				pass
		# I'd like to transparently re-raise the exception generated
		# on the very first try, but syntax differences preclude me from
		# doing so in Python 2 and it can't be done at all in Python 3
		raise


# 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(handle).__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, byref(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 = windll.kernel32.GetErrorMode()
except AttributeError:
	# GetErrorMode is available on Vista/2008 and up
	errmode = windll.kernel32.SetErrorMode(0)
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 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 testcases.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 testcases.WallTimeLimitExceeded
		else:
			case.process.wait()
	else:
		handles = case.process._handle, stdin
		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 + 1):
						ir = ReadConsoleInput(stdin)
						if (ir and
						    ir.EventType == KEY_EVENT and
						    ir.Event.KeyEvent.bKeyDown and
						    ir.Event.KeyEvent.wVirtualKeyCode == 27):
							raise testcases.CanceledByUser
				else:
					raise testcases.WallTimeLimitExceeded
		else:
			while case.process.poll() is None:
				if (WaitForMultipleObjects(handles, False, INFINITE) ==
				    WAIT_OBJECT_0 + 1):
					ir = ReadConsoleInput(stdin)
					if (ir and
					    ir.EventType == KEY_EVENT and
					    ir.Event.KeyEvent.bKeyDown and
					    ir.Event.KeyEvent.wVirtualKeyCode == 27):
						raise testcases.CanceledByUser
	case.time_stopped = clock()
	if GetProcessTimes and (case.maxcputime or not case.maxwalltime):
		try:
			times = GetProcessTimes(case.process._handle)
		except WindowsError:
			pass
		else:
			time = times.kernel + times.user
			case.time_stopped = time
			case.time_started = 0
			case.time_limit_string = case.cpu_time_limit_string
			if case.maxcputime and time > case.maxcputime:
				raise testcases.CPUTimeLimitExceeded
	if case.maxcputime and case.process.returncode == 1816:
		raise testcases.CPUTimeLimitExceeded
	if case.maxmemory and GetProcessMemoryInfo:
		try:
			counters = GetProcessMemoryInfo(case.process._handle)
		except WindowsError:
			pass
		else:
			if counters.PeakPagefileUsage > case.maxmemory * 1048576:
				raise testcases.MemoryLimitExceeded


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