Mercurial > ~astiob > upreckon > hgweb
comparison 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 |
comparison
equal
deleted
inserted
replaced
81:24752db487c5 | 82:06356af50bf9 |
---|---|
1 # Copyright (c) 2010-2011 Chortos-2 <chortos@inbox.lv> | |
2 | |
3 from __future__ import division, with_statement | |
4 import sys | |
5 | |
6 try: | |
7 from compat import * | |
8 import testcases # mutual import | |
9 except ImportError: | |
10 import __main__ | |
11 __main__.import_error(sys.exc_info()[1]) | |
12 | |
13 from __main__ import clock | |
14 from subprocess import Popen | |
15 import os, sys | |
16 | |
17 try: | |
18 from signal import SIGTERM, SIGKILL | |
19 except ImportError: | |
20 SIGTERM = 15 | |
21 SIGKILL = 9 | |
22 | |
23 __all__ = 'call', 'kill', 'terminate', 'pause' | |
24 | |
25 | |
26 if not sys.stdin.isatty(): | |
27 pause = lambda: sys.stdin.read(1) | |
28 catch_escape = False | |
29 else: | |
30 try: | |
31 from select import select | |
32 import termios, tty, atexit | |
33 except ImportError: | |
34 pause = None | |
35 catch_escape = False | |
36 else: | |
37 catch_escape = True | |
38 def cleanup(old=termios.tcgetattr(sys.stdin.fileno())): | |
39 termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, old) | |
40 atexit.register(cleanup) | |
41 tty.setcbreak(sys.stdin.fileno()) | |
42 def pause(): | |
43 sys.stdin.read(1) | |
44 | |
45 try: | |
46 from signal import SIGCHLD, signal, SIG_DFL | |
47 from select import select, error as SelectError | |
48 from errno import EINTR | |
49 from fcntl import fcntl, F_SETFD, F_GETFD | |
50 try: | |
51 import cPickle as pickle | |
52 except ImportError: | |
53 import pickle | |
54 except ImportError: | |
55 def call(*args, **kwargs): | |
56 case = kwargs.pop('case') | |
57 try: | |
58 case.process = Popen(*args, **kwargs) | |
59 except OSError: | |
60 raise CannotStartTestee(sys.exc_info()[1]) | |
61 case.time_started = clock() | |
62 if not case.maxtime: | |
63 while True: | |
64 exitcode, now = case.process.poll(), clock() | |
65 if exitcode is not None: | |
66 case.time_stopped = now | |
67 break | |
68 else: | |
69 time.sleep(.001) | |
70 else: | |
71 time_end = case.time_started + case.maxtime | |
72 while True: | |
73 exitcode, now = case.process.poll(), clock() | |
74 if exitcode is not None: | |
75 case.time_stopped = now | |
76 break | |
77 elif now >= time_end: | |
78 raise TimeLimitExceeded | |
79 else: | |
80 time.sleep(.001) | |
81 else: | |
82 try: | |
83 from fcntl import FD_CLOEXEC | |
84 except ImportError: | |
85 FD_CLOEXEC = 1 | |
86 | |
87 try: | |
88 from resource import getrusage, RUSAGE_SELF, RUSAGE_CHILDREN | |
89 except ImportError: | |
90 from time import clock as cpuclock | |
91 getrusage = lambda who: None | |
92 else: | |
93 def cpuclock(): | |
94 rusage = getrusage(RUSAGE_SELF) | |
95 return rusage.ru_utime + rusage.ru_stime | |
96 | |
97 try: | |
98 from resource import setrlimit | |
99 try: | |
100 from resource import RLIMIT_AS | |
101 except ImportError: | |
102 from resource import RLIMIT_VMEM | |
103 except ImportError: | |
104 setrlimit = None | |
105 | |
106 # Make SIGCHLD interrupt sleep() and select() | |
107 def bury_child(signum, frame): | |
108 try: | |
109 bury_child.case.time_stopped = clock() | |
110 except Exception: | |
111 pass | |
112 signal(SIGCHLD, bury_child) | |
113 | |
114 # If you want this to work portably, don't set any stdio argument to PIPE | |
115 def call(*args, **kwargs): | |
116 global last_rusage | |
117 bury_child.case = case = kwargs.pop('case') | |
118 read, write = os.pipe() | |
119 fcntl(write, F_SETFD, fcntl(write, F_GETFD) | FD_CLOEXEC) | |
120 def preexec_fn(): | |
121 os.close(read) | |
122 if setrlimit and case.maxmemory: | |
123 maxmemory = ceil(case.maxmemory * 1048576) | |
124 setrlimit(RLIMIT_AS, (maxmemory, maxmemory)) | |
125 # I would also set a CPU time limit but I do not want the time | |
126 # passing between the calls to fork and exec to be counted in | |
127 os.write(write, pickle.dumps((clock(), cpuclock()), 1)) | |
128 kwargs['preexec_fn'] = preexec_fn | |
129 old_rusage = getrusage(RUSAGE_CHILDREN) | |
130 last_rusage = None | |
131 try: | |
132 case.process = Popen(*args, **kwargs) | |
133 except OSError: | |
134 os.close(read) | |
135 raise testcases.CannotStartTestee(sys.exc_info()[1]) | |
136 finally: | |
137 os.close(write) | |
138 try: | |
139 if not catch_escape: | |
140 if case.maxwalltime: | |
141 time.sleep(case.maxwalltime) | |
142 if case.process.poll() is None: | |
143 raise testcases.WallTimeLimitExceeded | |
144 else: | |
145 case.process.wait() | |
146 else: | |
147 if not case.maxwalltime: | |
148 try: | |
149 while case.process.poll() is None: | |
150 if select((sys.stdin,), (), ())[0]: | |
151 if sys.stdin.read(1) == '\33': | |
152 raise testcases.CanceledByUser | |
153 except SelectError: | |
154 if sys.exc_info()[1].args[0] != EINTR: | |
155 raise | |
156 else: | |
157 case.process.poll() | |
158 else: | |
159 time_end = clock() + case.maxwalltime | |
160 try: | |
161 while case.process.poll() is None: | |
162 remaining = time_end - clock() | |
163 if remaining > 0: | |
164 if select((sys.stdin,), (), (), remaining)[0]: | |
165 if sys.stdin.read(1) == '\33': | |
166 raise testcases.CanceledByUser | |
167 else: | |
168 raise testcases.WallTimeLimitExceeded | |
169 except SelectError: | |
170 if sys.exc_info()[1].args[0] != EINTR: | |
171 raise | |
172 else: | |
173 case.process.poll() | |
174 finally: | |
175 case.time_started, cpustart = pickle.loads(os.read(read, 512)) | |
176 os.close(read) | |
177 del bury_child.case | |
178 new_rusage = getrusage(RUSAGE_CHILDREN) | |
179 if new_rusage and (case.maxcputime or not case.maxwalltime): | |
180 case.time_started = cpustart | |
181 case.time_stopped = new_rusage.ru_utime + new_rusage.ru_stime | |
182 case.time_limit_string = case.cpu_time_limit_string | |
183 if case.maxcputime and new_rusage: | |
184 oldtime = old_rusage.ru_utime + old_rusage.ru_stime | |
185 newtime = new_rusage.ru_utime + new_rusage.ru_stime | |
186 if newtime - oldtime - cpustart > case.maxcputime: | |
187 raise testcases.CPUTimeLimitExceeded | |
188 if case.maxmemory: | |
189 if sys.platform != 'darwin': | |
190 maxrss = case.maxmemory * 1024 | |
191 else: | |
192 maxrss = case.maxmemory * 1048576 | |
193 if last_rusage and last_rusage.ru_maxrss > maxrss: | |
194 raise testcases.MemoryLimitExceeded | |
195 elif (new_rusage and | |
196 new_rusage.ru_maxrss > old_rusage.ru_maxrss and | |
197 new_rusage.ru_maxrss > maxrss): | |
198 raise testcases.MemoryLimitExceeded | |
199 | |
200 # Emulate memory limits on platforms compatible with 4.3BSD but not XSI | |
201 # I say 'emulate' because the OS will allow excessive memory usage | |
202 # anyway; Upreckon will just treat the test case as not passed. | |
203 # To do this, we not only require os.wait4 to be present but also | |
204 # assume things about the implementation of subprocess.Popen. | |
205 try: | |
206 def waitpid_emu(pid, options, _wait4=os.wait4): | |
207 global last_rusage | |
208 pid, status, last_rusage = _wait4(pid, options) | |
209 return pid, status | |
210 _waitpid = os.waitpid | |
211 os.waitpid = waitpid_emu | |
212 try: | |
213 defaults = Popen._internal_poll.__func__.__defaults__ | |
214 except AttributeError: | |
215 # Python 2.5 | |
216 defaults = Popen._internal_poll.im_func.func_defaults | |
217 i = defaults.index(_waitpid) | |
218 defaults = defaults[:i] + (waitpid_emu,) + defaults[i+1:] | |
219 try: | |
220 Popen._internal_poll.__func__.__defaults__ = defaults | |
221 except AttributeError: | |
222 # Python 2.5 again | |
223 Popen._internal_poll.im_func.func_defaults = defaults | |
224 except (AttributeError, ValueError): | |
225 pass | |
226 | |
227 | |
228 def kill(process): | |
229 try: | |
230 process.kill() | |
231 except AttributeError: | |
232 os.kill(process.pid, SIGKILL) | |
233 | |
234 | |
235 def terminate(process): | |
236 try: | |
237 process.terminate() | |
238 except AttributeError: | |
239 os.kill(process.pid, SIGTERM) |