comparison 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
comparison
equal deleted inserted replaced
79:ee8a99dcaaed 80:809b77302b21
1 # Copyright (c) 2011 Chortos-2 <chortos@inbox.lv>
2
3 from __future__ import division, with_statement
4
5 try:
6 from compat import *
7 except ImportError:
8 import __main__
9 __main__.import_error(sys.exc_info()[1])
10
11 from __main__ import clock
12 from ctypes import *
13 from ctypes.wintypes import *
14 from subprocess import Popen
15
16 # Defaults that may be overwritten by values from _subprocess
17 INFINITE = -1
18 STD_INPUT_HANDLE = -10
19 WAIT_OBJECT_0 = 0
20
21 try:
22 from _subprocess import *
23 except ImportError:
24 pass
25
26 try:
27 from numbers import Integral
28 except ImportError:
29 Integral = int, long
30
31 try:
32 from collections import namedtuple
33 except ImportError:
34 from operator import itemgetter
35 class ProcessTimes(tuple):
36 __slots__ = ()
37 __new__ = lambda cls, kernel, user: tuple.__new__(cls, (kernel, user))
38 __getnewargs__ = lambda self: tuple(self)
39 kernel, user = (property(itemgetter(i)) for i in (0, 1))
40 else:
41 ProcessTimes = namedtuple('ProcessTimes', 'kernel user')
42
43 __all__ = 'call', 'kill', 'terminate'
44
45
46 # Automatically convert _subprocess handle objects into low-level HANDLEs
47 # and replicate their functionality for our own use
48 try:
49 _subprocess_handle = type(GetCurrentProcess())
50 except NameError:
51 _subprocess_handle = Integral
52 class Handle(object):
53 @staticmethod
54 def from_param(handle):
55 if isinstance(handle, (_subprocess_handle, Integral)):
56 return HANDLE(int(handle))
57 elif isinstance(handle, Handle):
58 return HANDLE(handle.handle)
59 elif isinstance(handle, HANDLE):
60 return handle
61 else:
62 raise TypeError('cannot convert %s to a handle' %
63 type(handle).__name__)
64
65 __slots__ = 'handle'
66
67 def __init__(self, handle):
68 if isinstance(handle, Integral):
69 self.handle = handle
70 elif isinstance(handle, HANDLE):
71 self.handle = handle.value
72 elif isinstance(handle, Handle):
73 self.handle = handle.handle
74 elif isinstance(handle, _subprocess_handle):
75 handle = HANDLE(int(handle))
76 flags = DWORD()
77 try:
78 if windll.kernel32.GetHandleInformation(handle, byref(flags)):
79 flags = flags.value
80 else:
81 flags = 0
82 except AttributeError:
83 # Available on NT 3.51 and up, NT line only
84 flags = 0
85 proc = HANDLE(int(GetCurrentProcess()))
86 handle = DuplicateHandle(proc, handle, proc, 0, flags & 1, 2)
87 self.handle = handle.Detach()
88 else:
89 raise TypeError("Handle() argument must be a handle, not '%s'" %
90 type(name).__name__)
91
92 def __int__(self):
93 return int(self.handle)
94
95 def Detach(self):
96 handle = self.handle
97 self.handle = None
98 return handle
99
100 # This is also __del__, so only locals are accessed
101 def Close(self, _CloseHandle=windll.kernel32.CloseHandle, _HANDLE=HANDLE):
102 if self.handle:
103 _CloseHandle(_HANDLE(self.handle))
104 self.handle = None
105 __del__ = Close
106
107 CHAR = c_char
108 INVALID_HANDLE_VALUE = HANDLE(-1).value
109 LPDWORD = POINTER(DWORD)
110 LPFILETIME = POINTER(FILETIME)
111 SIZE_T = ULONG_PTR = WPARAM
112 ULONGLONG = c_ulonglong
113
114 try:
115 unicode
116 except NameError:
117 LPCTSTR = LPCWSTR
118 unisuffix = 'W'
119 else:
120 LPCTSTR = LPCSTR
121 unisuffix = 'A'
122
123
124 prototype = WINFUNCTYPE(BOOL, Handle,
125 LPFILETIME, LPFILETIME, LPFILETIME, LPFILETIME)
126 flags = ((1, 'process'),
127 (2, 'creation'), (2, 'exit'), (2, 'kernel'), (2, 'user'))
128 try:
129 GetProcessTimes = prototype(('GetProcessTimes', windll.kernel32), flags)
130 except AttributeError:
131 # Available on NT 3.5 and up, NT line only
132 GetProcessTimes = None
133 else:
134 def errcheck(result, func, args):
135 if not result: raise WinError()
136 ftimes = [t.dwHighDateTime << 32 | t.dwLowDateTime for t in args[3:]]
137 kernel = ftimes[0] / 10000000
138 user = ftimes[1] / 10000000
139 return ProcessTimes(kernel, user)
140 GetProcessTimes.errcheck = errcheck
141
142
143 class PROCESS_MEMORY_COUNTERS(Structure):
144 _fields_ = (('cb', DWORD),
145 ('PageFaultCount', DWORD),
146 ('PeakWorkingSetSize', SIZE_T),
147 ('WorkingSetSize', SIZE_T),
148 ('QuotaPeakPagedPoolUsage', SIZE_T),
149 ('QuotaPagedPoolUsage', SIZE_T),
150 ('QuotaPeakNonPagedPoolUsage', SIZE_T),
151 ('QuotaNonPagedPoolUsage', SIZE_T),
152 ('PagefileUsage', SIZE_T),
153 ('PeakPagefileUsage', SIZE_T))
154
155 prototype = WINFUNCTYPE(BOOL, Handle, POINTER(PROCESS_MEMORY_COUNTERS), DWORD)
156 flags = ((1, 'process'), (2, 'counters'),
157 (5, 'cb', sizeof(PROCESS_MEMORY_COUNTERS)))
158 try:
159 GetProcessMemoryInfo = prototype(('GetProcessMemoryInfo', windll.psapi),
160 flags)
161 except AttributeError:
162 # Available on NT 4.0 and up, NT line only
163 GetProcessMemoryInfo = None
164 else:
165 def errcheck(result, func, args):
166 if not result: raise WinError()
167 return args
168 GetProcessMemoryInfo.errcheck = errcheck
169
170
171 class _uChar_union(Union):
172 _fields_ = (('UnicodeChar', WCHAR),
173 ('AsciiChar', CHAR))
174
175 class KEY_EVENT_RECORD(Structure):
176 _fields_ = (('bKeyDown', BOOL),
177 ('wRepeatCount', WORD),
178 ('wVirtualKeyCode', WORD),
179 ('wVirtualScanCode', WORD),
180 ('uChar', _uChar_union),
181 ('dwControlKeyState', DWORD))
182
183 RIGHT_ALT_PRESSED = 0x001
184 LEFT_ALT_PRESSED = 0x002
185 RIGHT_CTRL_PRESSED = 0x004
186 LEFT_CTRL_PRESSED = 0x008
187 SHIFT_PRESSED = 0x010
188 NUMLOCK_ON = 0x020
189 SCROLLLOCK_ON = 0x040
190 CAPSLOCK_ON = 0x080
191 ENHANCED_KEY = 0x100
192
193 class _Event_union(Union):
194 _fields_ = ('KeyEvent', KEY_EVENT_RECORD),
195
196 class INPUT_RECORD(Structure):
197 _fields_ = (('EventType', WORD),
198 ('Event', _Event_union))
199
200 KEY_EVENT = 0x01
201 MOUSE_EVENT = 0x02
202 WINDOW_BUFFER_SIZE_EVENT = 0x04
203 MENU_EVENT = 0x08
204 FOCUS_EVENT = 0x10
205
206 prototype = WINFUNCTYPE(BOOL, Handle, POINTER(INPUT_RECORD), DWORD, LPDWORD)
207 flags = (1, 'input'), (2, 'buffer'), (5, 'length', 1), (2, 'number_read')
208 ReadConsoleInput = prototype(('ReadConsoleInputA', windll.kernel32), flags)
209 def errcheck(result, func, args):
210 if not result: raise WinError()
211 return args[1] if args[3] else None
212 ReadConsoleInput.errcheck = errcheck
213
214
215 prototype = WINFUNCTYPE(BOOL, Handle)
216 flags = (1, 'input'),
217 FlushConsoleInputBuffer = prototype(('FlushConsoleInputBuffer',
218 windll.kernel32), flags)
219 def errcheck(result, func, args):
220 if not result: raise WinError()
221 FlushConsoleInputBuffer.errcheck = errcheck
222
223
224 prototype = WINFUNCTYPE(BOOL, Handle, DWORD)
225 flags = (1, 'console'), (1, 'mode')
226 SetConsoleMode = prototype(('SetConsoleMode', windll.kernel32), flags)
227 def errcheck(result, func, args):
228 if not result: raise WinError()
229 SetConsoleMode.errcheck = errcheck
230
231 ENABLE_PROCESSED_INPUT = 0x001
232 ENABLE_LINE_INPUT = 0x002
233 ENABLE_ECHO_INPUT = 0x004
234 ENABLE_WINDOW_INPUT = 0x008
235 ENABLE_MOUSE_INPUT = 0x010
236 ENABLE_INSERT_MODE = 0x020
237 ENABLE_QUICK_EDIT_MODE = 0x040
238 ENABLE_EXTENDED_FLAGS = 0x080
239
240 ENABLE_PROCESSED_OUTPUT = 1
241 ENABLE_WRAP_AT_EOL_OUTPUT = 2
242
243
244 prototype = WINFUNCTYPE(HANDLE, c_void_p, LPCTSTR)
245 flags = (5, 'attributes'), (1, 'name')
246 try:
247 CreateJobObject = prototype(('CreateJobObject'+unisuffix, windll.kernel32),
248 flags)
249 except AttributeError:
250 # Available on 2000 and up, NT line only
251 CreateJobObject = lambda name: None
252 else:
253 def errcheck(result, func, args):
254 if not result: raise WinError()
255 return Handle(result)
256 CreateJobObject.errcheck = errcheck
257
258
259 prototype = WINFUNCTYPE(BOOL, Handle, Handle)
260 flags = (1, 'job'), (1, 'handle')
261 try:
262 AssignProcessToJobObject = prototype(('AssignProcessToJobObject',
263 windll.kernel32), flags)
264 except AttributeError:
265 # Available on 2000 and up, NT line only
266 AssignProcessToJobObject = lambda job, handle: None
267 else:
268 def errcheck(result, func, args):
269 if not result: raise WinError()
270 AssignProcessToJobObject.errcheck = errcheck
271
272
273 class JOBOBJECT_BASIC_LIMIT_INFORMATION(Structure):
274 _fields_ = (('PerProcessUserTimeLimit', LARGE_INTEGER),
275 ('PerJobUserTimeLimit', LARGE_INTEGER),
276 ('LimitFlags', DWORD),
277 ('MinimumWorkingSetSize', SIZE_T),
278 ('MaximumWorkingSetSize', SIZE_T),
279 ('ActiveProcessLimit', DWORD),
280 ('Affinity', ULONG_PTR),
281 ('PriorityClass', DWORD),
282 ('SchedulingClass', DWORD))
283
284 JOB_OBJECT_LIMIT_WORKINGSET = 0x0001
285 JOB_OBJECT_LIMIT_PROCESS_TIME = 0x0002
286 JOB_OBJECT_LIMIT_JOB_TIME = 0x0004
287 JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x0008
288 JOB_OBJECT_LIMIT_AFFINITY = 0x0010
289 JOB_OBJECT_LIMIT_PRIORITY_CLASS = 0x0020
290 JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME = 0x0040
291 JOB_OBJECT_LIMIT_SCHEDULING_CLASS = 0x0080
292 JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x0100
293 JOB_OBJECT_LIMIT_JOB_MEMORY = 0x0200
294 JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x0400
295 JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x0800
296 JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x1000
297 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000
298 JOB_OBJECT_LIMIT_SUBSET_AFFINITY = 0x4000
299
300 class IO_COUNTERS(Structure):
301 _fields_ = (('ReadOperationCount', ULONGLONG),
302 ('WriteOperationCount', ULONGLONG),
303 ('OtherOperationCount', ULONGLONG),
304 ('ReadTransferCount', ULONGLONG),
305 ('WriteTransferCount', ULONGLONG),
306 ('OtherTransferCount', ULONGLONG))
307
308 class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(Structure):
309 _fields_ = (('BasicLimitInformation', JOBOBJECT_BASIC_LIMIT_INFORMATION),
310 ('IoInfo', IO_COUNTERS),
311 ('ProcessMemoryLimit', SIZE_T),
312 ('JobMemoryLimit', SIZE_T),
313 ('PeakProcessMemoryUsed', SIZE_T),
314 ('PeakJobMemoryUsed', SIZE_T))
315
316 prototype = WINFUNCTYPE(BOOL, HANDLE, c_int, c_void_p, DWORD)
317 flags = (1, 'job'), (1, 'infoclass'), (1, 'info'), (1, 'infosize')
318 try:
319 _setjobinfo = prototype(('SetInformationJobObject',windll.kernel32), flags)
320 except AttributeError:
321 # Available on 2000 and up, NT line only
322 SetInformationJobObject = lambda job, infoclass, info: None
323 else:
324 def errcheck(result, func, args):
325 if not result: raise WinError()
326 _setjobinfo.errcheck = errcheck
327 def SetInformationJobObject(job, infoclass, info):
328 return _setjobinfo(job, infoclass, info, sizeof(info))
329
330 (
331 JobObjectBasicAccountingInformation,
332 JobObjectBasicLimitInformation,
333 JobObjectBasicProcessIdList,
334 JobObjectBasicUIRestrictions,
335 JobObjectSecurityLimitInformation,
336 JobObjectEndOfJobTimeInformation,
337 JobObjectAssociateCompletionPortInformation,
338 JobObjectBasicAndIoAccountingInformation,
339 JobObjectExtendedLimitInformation,
340 JobObjectJobSetInformation,
341 MaxJobObjectInfoClass
342 ) = range(1, 12)
343
344
345 prototype = WINFUNCTYPE(DWORD, DWORD, POINTER(HANDLE), BOOL, DWORD)
346 flags = (1, 'count'), (1, 'handles'), (1, 'wait_all'), (1, 'milliseconds')
347 _wait_multiple = prototype(('WaitForMultipleObjects', windll.kernel32), flags)
348 def errcheck(result, func, args):
349 if result == WAIT_FAILED: raise WinError()
350 return args
351 _wait_multiple.errcheck = errcheck
352 def WaitForMultipleObjects(handles, wait_all, timeout):
353 n = len(handles)
354 handles = (Handle.from_param(handle) for handle in handles)
355 timeout = ceil(timeout * 1000)
356 return _wait_multiple(n, (HANDLE * n)(*handles), wait_all, timeout)
357
358 # WAIT_OBJECT_0 defined at the top of the file
359 WAIT_ABANDONED_0 = 0x00000080
360 WAIT_TIMEOUT = 0x00000102
361 WAIT_FAILED = 0xFFFFFFFF
362
363
364 try:
365 _wait_single = WaitForSingleObject
366 except NameError:
367 prototype = WINFUNCTYPE(DWORD, Handle, DWORD)
368 flags = (1, 'handle'), (1, 'milliseconds')
369 _wait_single = prototype(('WaitForSingleObject', windll.kernel32), flags)
370 def errcheck(result, func, args):
371 if result == WAIT_FAILED: raise WinError()
372 return args
373 _wait_single.errcheck = errcheck
374 def WaitForSingleObject(handle, timeout):
375 return _wait_single(handle, ceil(timeout * 1000))
376
377
378 try:
379 GetStdHandle
380 except NameError:
381 prototype = WINFUNCTYPE(HANDLE, DWORD)
382 flags = (1, 'which'),
383 GetStdHandle = prototype(('GetStdHandle', windll.kernel32), flags)
384 def errcheck(result, func, args):
385 if result == INVALID_HANDLE_VALUE: raise WinError()
386 return args if result else None
387 GetStdHandle.errcheck = errcheck
388
389
390 try:
391 TerminateProcess
392 except NameError:
393 prototype = WINFUNCTYPE(BOOL, Handle, UINT)
394 flags = (1, 'process'), (1, 'exitcode')
395 TerminateProcess = prototype(('TerminateProcess', windll.kernel32), flags)
396 def errcheck(result, func, args):
397 if not result: raise WinError()
398 TerminateProcess.errcheck = errcheck
399
400
401 # Do not show error messages due to errors in the program being tested
402 try:
403 errmode = ctypes.windll.kernel32.GetErrorMode()
404 except AttributeError:
405 # GetErrorMode is available on Vista/2008 and up
406 errmode = ctypes.windll.kernel32.SetErrorMode(0)
407 ctypes.windll.kernel32.SetErrorMode(errmode | 0x8003)
408
409 stdin = GetStdHandle(STD_INPUT_HANDLE)
410 try:
411 SetConsoleMode(stdin, ENABLE_PROCESSED_INPUT)
412 except WindowsError:
413 console_input = False
414 else:
415 console_input = True
416 FlushConsoleInputBuffer(stdin)
417
418 def kill(process):
419 try:
420 process.terminate()
421 except AttributeError:
422 TerminateProcess(process._handle)
423 terminate = kill
424
425 def call(*args, **kwargs):
426 case = kwargs.pop('case')
427 job = CreateJobObject(None)
428 flags = 0
429 if case.maxcputime:
430 flags |= JOB_OBJECT_LIMIT_PROCESS_TIME
431 if case.maxmemory:
432 flags |= JOB_OBJECT_LIMIT_PROCESS_MEMORY
433 limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION(
434 JOBOBJECT_BASIC_LIMIT_INFORMATION(
435 PerProcessUserTimeLimit=ceil((case.maxcputime or 0)*10000000),
436 LimitFlags=flags,
437 ),
438 ProcessMemoryLimit=ceil((case.maxmemory or 0)*1048576),
439 )
440 SetInformationJobObject(job, JobObjectExtendedLimitInformation, limits)
441 try:
442 case.process = Popen(*args, **kwargs)
443 except OSError:
444 raise CannotStartTestee(sys.exc_info()[1])
445 case.time_started = clock()
446 AssignProcessToJobObject(job, case.process._handle)
447 if not console_input:
448 if case.maxwalltime:
449 if (WaitForSingleObject(case.process._handle, case.maxwalltime) !=
450 WAIT_OBJECT_0):
451 raise TimeLimitExceeded
452 else:
453 case.process.wait()
454 else:
455 handles = stdin, case.process._handle
456 if case.maxwalltime:
457 time_end = clock() + case.maxwalltime
458 while case.process.poll() is None:
459 remaining = time_end - clock()
460 if remaining > 0:
461 if (WaitForMultipleObjects(handles, False, remaining) ==
462 WAIT_OBJECT_0):
463 ir = ReadConsoleInput(stdin)
464 if (ir and
465 ir.EventType == 1 and
466 ir.Event.KeyEvent.bKeyDown and
467 ir.Event.KeyEvent.wVirtualKeyCode == 27):
468 raise CanceledByUser
469 else:
470 raise TimeLimitExceeded
471 else:
472 while case.process.poll() is None:
473 if (WaitForMultipleObjects(handles, False, INFINITE) ==
474 WAIT_OBJECT_0):
475 ir = ReadConsoleInput(stdin)
476 if (ir and
477 ir.EventType == 1 and
478 ir.Event.KeyEvent.bKeyDown and
479 ir.Event.KeyEvent.wVirtualKeyCode == 27):
480 raise CanceledByUser
481 case.time_stopped = clock()
482 if case.maxcputime and GetProcessTimes:
483 try:
484 times = GetProcessTimes(case.process._handle)
485 except WindowsError:
486 pass
487 else:
488 if times.kernel + times.user > case.maxcputime:
489 raise TimeLimitExceeded
490 if case.maxmemory and GetProcessMemoryInfo:
491 try:
492 counters = GetProcessMemoryInfo(case.process._handle)
493 except WindowsError:
494 pass
495 else:
496 if counters.PeakPagefileUsage > case.maxmemory * 1048576:
497 raise MemoryLimitExceeded