rbx.cp 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rbx/__init__.py +0 -0
- rbx/annotations.py +127 -0
- rbx/autoenum.py +333 -0
- rbx/box/__init__.py +0 -0
- rbx/box/builder.py +77 -0
- rbx/box/cd.py +37 -0
- rbx/box/checkers.py +134 -0
- rbx/box/code.py +185 -0
- rbx/box/compile.py +56 -0
- rbx/box/conftest.py +42 -0
- rbx/box/contest/__init__.py +0 -0
- rbx/box/contest/build_contest_statements.py +347 -0
- rbx/box/contest/contest_package.py +76 -0
- rbx/box/contest/contest_utils.py +20 -0
- rbx/box/contest/main.py +179 -0
- rbx/box/contest/schema.py +155 -0
- rbx/box/contest/statements.py +82 -0
- rbx/box/creation.py +72 -0
- rbx/box/download.py +64 -0
- rbx/box/environment.py +345 -0
- rbx/box/extensions.py +26 -0
- rbx/box/generators.py +478 -0
- rbx/box/generators_test.py +63 -0
- rbx/box/main.py +449 -0
- rbx/box/package.py +316 -0
- rbx/box/packaging/boca/extension.py +27 -0
- rbx/box/packaging/boca/packager.py +245 -0
- rbx/box/packaging/contest_main.py +82 -0
- rbx/box/packaging/main.py +68 -0
- rbx/box/packaging/packager.py +117 -0
- rbx/box/packaging/polygon/packager.py +320 -0
- rbx/box/packaging/polygon/test.py +81 -0
- rbx/box/packaging/polygon/xml_schema.py +106 -0
- rbx/box/presets/__init__.py +503 -0
- rbx/box/presets/fetch.py +70 -0
- rbx/box/presets/lock_schema.py +20 -0
- rbx/box/presets/schema.py +59 -0
- rbx/box/schema.py +394 -0
- rbx/box/solutions.py +792 -0
- rbx/box/solutions_test.py +41 -0
- rbx/box/statements/__init__.py +0 -0
- rbx/box/statements/build_statements.py +359 -0
- rbx/box/statements/builders.py +375 -0
- rbx/box/statements/joiners.py +113 -0
- rbx/box/statements/latex.py +47 -0
- rbx/box/statements/latex_jinja.py +214 -0
- rbx/box/statements/schema.py +138 -0
- rbx/box/stresses.py +292 -0
- rbx/box/stressing/__init__.py +0 -0
- rbx/box/stressing/finder_parser.py +359 -0
- rbx/box/stressing/generator_parser.py +258 -0
- rbx/box/testcases.py +54 -0
- rbx/box/ui/__init__.py +0 -0
- rbx/box/ui/captured_log.py +372 -0
- rbx/box/ui/css/app.tcss +48 -0
- rbx/box/ui/main.py +38 -0
- rbx/box/ui/run.py +209 -0
- rbx/box/validators.py +245 -0
- rbx/box/validators_test.py +15 -0
- rbx/checker.py +128 -0
- rbx/clone.py +197 -0
- rbx/config.py +271 -0
- rbx/conftest.py +38 -0
- rbx/console.py +27 -0
- rbx/create.py +37 -0
- rbx/edit.py +24 -0
- rbx/grading/__init__.py +0 -0
- rbx/grading/caching.py +356 -0
- rbx/grading/conftest.py +33 -0
- rbx/grading/judge/__init__.py +0 -0
- rbx/grading/judge/cacher.py +503 -0
- rbx/grading/judge/digester.py +35 -0
- rbx/grading/judge/sandbox.py +748 -0
- rbx/grading/judge/sandboxes/__init__.py +0 -0
- rbx/grading/judge/sandboxes/isolate.py +683 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
- rbx/grading/judge/sandboxes/timeit.py +217 -0
- rbx/grading/judge/storage.py +284 -0
- rbx/grading/judge/test.py +38 -0
- rbx/grading/judge/testiso.py +54 -0
- rbx/grading/steps.py +522 -0
- rbx/grading/steps_with_caching.py +59 -0
- rbx/grading/steps_with_caching_run_test.py +429 -0
- rbx/grading_utils.py +148 -0
- rbx/hydration.py +101 -0
- rbx/main.py +122 -0
- rbx/metadata.py +105 -0
- rbx/providers/__init__.py +43 -0
- rbx/providers/codeforces.py +73 -0
- rbx/providers/provider.py +26 -0
- rbx/resources/checkers/boilerplate.cpp +20 -0
- rbx/resources/default_config.json +48 -0
- rbx/resources/envs/default.rbx.yml +37 -0
- rbx/resources/envs/isolate.rbx.yml +37 -0
- rbx/resources/packagers/boca/checker.sh +43 -0
- rbx/resources/packagers/boca/compare +53 -0
- rbx/resources/packagers/boca/compile/c +172 -0
- rbx/resources/packagers/boca/compile/cc +173 -0
- rbx/resources/packagers/boca/compile/cpp +172 -0
- rbx/resources/packagers/boca/compile/java +194 -0
- rbx/resources/packagers/boca/compile/kt +155 -0
- rbx/resources/packagers/boca/compile/pas +172 -0
- rbx/resources/packagers/boca/compile/py2 +173 -0
- rbx/resources/packagers/boca/compile/py3 +173 -0
- rbx/resources/packagers/boca/run/c +128 -0
- rbx/resources/packagers/boca/run/cc +128 -0
- rbx/resources/packagers/boca/run/cpp +128 -0
- rbx/resources/packagers/boca/run/java +194 -0
- rbx/resources/packagers/boca/run/kt +159 -0
- rbx/resources/packagers/boca/run/py2 +166 -0
- rbx/resources/packagers/boca/run/py3 +166 -0
- rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
- rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
- rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
- rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
- rbx/resources/presets/default/preset.rbx.yml +12 -0
- rbx/resources/presets/default/problem/.gitignore +6 -0
- rbx/resources/presets/default/problem/gen.cpp +9 -0
- rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
- rbx/resources/presets/default/problem/random.py +3 -0
- rbx/resources/presets/default/problem/random.txt +2 -0
- rbx/resources/presets/default/problem/sols/main.cpp +9 -0
- rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
- rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
- rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
- rbx/resources/presets/default/problem/statement/projecao.png +0 -0
- rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
- rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
- rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
- rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
- rbx/resources/presets/default/problem/validator.cpp +16 -0
- rbx/resources/presets/default/problem/wcmp.cpp +34 -0
- rbx/resources/templates/template.cpp +19 -0
- rbx/run.py +45 -0
- rbx/schema.py +64 -0
- rbx/submit.py +61 -0
- rbx/submitors/__init__.py +18 -0
- rbx/submitors/codeforces.py +120 -0
- rbx/submitors/submitor.py +25 -0
- rbx/test.py +347 -0
- rbx/testcase.py +70 -0
- rbx/testcase_rendering.py +79 -0
- rbx/testdata/box1/gen1.cpp +7 -0
- rbx/testdata/box1/gen2.cpp +9 -0
- rbx/testdata/box1/genScript.py +2 -0
- rbx/testdata/box1/hard-tle.sol.cpp +26 -0
- rbx/testdata/box1/ole.cpp +17 -0
- rbx/testdata/box1/problem.rbx.yml +39 -0
- rbx/testdata/box1/re.sol.cpp +23 -0
- rbx/testdata/box1/sol.cpp +22 -0
- rbx/testdata/box1/tests/1.in +1 -0
- rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
- rbx/testdata/box1/tle.sol.cpp +35 -0
- rbx/testdata/box1/validator.cpp +11 -0
- rbx/testdata/box1/wa.sol.cpp +22 -0
- rbx/testdata/caching/executable.py +1 -0
- rbx/testdata/compatible +0 -0
- rbx/testing_utils.py +65 -0
- rbx/utils.py +162 -0
- rbx_cp-0.5.0.dist-info/LICENSE +201 -0
- rbx_cp-0.5.0.dist-info/METADATA +89 -0
- rbx_cp-0.5.0.dist-info/RECORD +164 -0
- rbx_cp-0.5.0.dist-info/WHEEL +4 -0
- rbx_cp-0.5.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,310 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import importlib
|
4
|
+
import importlib.resources
|
5
|
+
import logging
|
6
|
+
import pathlib
|
7
|
+
import shutil
|
8
|
+
import signal
|
9
|
+
import subprocess
|
10
|
+
import sys
|
11
|
+
import tempfile
|
12
|
+
from typing import Any, Dict, List, Optional
|
13
|
+
|
14
|
+
from rbx.grading.judge.cacher import FileCacher
|
15
|
+
from rbx.grading.judge.sandbox import (
|
16
|
+
SandboxBase,
|
17
|
+
SandboxParams,
|
18
|
+
)
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class StupidSandbox(SandboxBase):
|
24
|
+
"""A stupid sandbox implementation. It has very few features and
|
25
|
+
is not secure against things like box escaping and fork
|
26
|
+
bombs. Yet, it is very portable and has no dependencies, so it's
|
27
|
+
very useful for testing. Using in real contests is strongly
|
28
|
+
discouraged.
|
29
|
+
|
30
|
+
"""
|
31
|
+
|
32
|
+
exec_num: int
|
33
|
+
popen: Optional[subprocess.Popen]
|
34
|
+
returncode: Optional[int]
|
35
|
+
log: Optional[Dict[str, str]]
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
file_cacher: Optional[FileCacher] = None,
|
40
|
+
name: Optional[str] = None,
|
41
|
+
temp_dir: Optional[pathlib.Path] = None,
|
42
|
+
params: Optional[SandboxParams] = None,
|
43
|
+
):
|
44
|
+
"""Initialization.
|
45
|
+
|
46
|
+
For arguments documentation, see SandboxBase.__init__.
|
47
|
+
|
48
|
+
"""
|
49
|
+
if not temp_dir:
|
50
|
+
temp_dir = pathlib.Path(tempfile.gettempdir())
|
51
|
+
SandboxBase.__init__(self, file_cacher, name, temp_dir, params)
|
52
|
+
|
53
|
+
# Make box directory
|
54
|
+
self._path = pathlib.Path(
|
55
|
+
tempfile.mkdtemp(dir=str(self.temp_dir), prefix='rbx-%s-' % (self.name))
|
56
|
+
)
|
57
|
+
self.initialize()
|
58
|
+
|
59
|
+
self.exec_num = -1
|
60
|
+
self.popen = None
|
61
|
+
self.log = None
|
62
|
+
self.returncode = None
|
63
|
+
|
64
|
+
logger.debug("Sandbox in `%s' created, using stupid box.", self._path)
|
65
|
+
|
66
|
+
# Box parameters
|
67
|
+
self.chdir = self._path
|
68
|
+
|
69
|
+
def initialize(self):
|
70
|
+
self._path.mkdir(parents=True, exist_ok=True)
|
71
|
+
|
72
|
+
def get_timeit_executable(self) -> pathlib.Path:
|
73
|
+
with importlib.resources.as_file(
|
74
|
+
importlib.resources.files('rbx')
|
75
|
+
/ 'grading'
|
76
|
+
/ 'judge'
|
77
|
+
/ 'sandboxes'
|
78
|
+
/ 'timeit.py'
|
79
|
+
) as file:
|
80
|
+
return file
|
81
|
+
|
82
|
+
def get_timeit_args(self) -> List[str]:
|
83
|
+
args = []
|
84
|
+
if self.params.timeout:
|
85
|
+
timeout_in_s = self.params.timeout / 1000
|
86
|
+
if self.params.extra_timeout:
|
87
|
+
timeout_in_s += self.params.extra_timeout / 1000
|
88
|
+
args.append(f'-t{timeout_in_s:.3f}')
|
89
|
+
if self.params.wallclock_timeout:
|
90
|
+
walltimeout_in_s = self.params.wallclock_timeout / 1000
|
91
|
+
args.append(f'-w{walltimeout_in_s:.3f}')
|
92
|
+
if self.params.address_space:
|
93
|
+
args.append(f'-m{self.params.address_space}')
|
94
|
+
if self.params.stdin_file:
|
95
|
+
args.append(f'-i{self.params.stdin_file}')
|
96
|
+
if self.params.stdout_file:
|
97
|
+
args.append(f'-o{self.params.stdout_file}')
|
98
|
+
if self.params.stderr_file:
|
99
|
+
args.append(f'-e{self.params.stderr_file}')
|
100
|
+
if self.params.fsize:
|
101
|
+
args.append(f'-f{self.params.fsize}')
|
102
|
+
if self.chdir:
|
103
|
+
args.append(f'-c{self.chdir}')
|
104
|
+
return args
|
105
|
+
|
106
|
+
def get_root_path(self) -> pathlib.Path:
|
107
|
+
"""Return the toplevel path of the sandbox.
|
108
|
+
|
109
|
+
return (Path): the root path.
|
110
|
+
|
111
|
+
"""
|
112
|
+
return self._path
|
113
|
+
|
114
|
+
def get_execution_time(self) -> Optional[float]:
|
115
|
+
"""Return the time spent in the sandbox.
|
116
|
+
|
117
|
+
return (float): time spent in the sandbox.
|
118
|
+
|
119
|
+
"""
|
120
|
+
if self.log is None:
|
121
|
+
return None
|
122
|
+
return float(self.log['time'])
|
123
|
+
|
124
|
+
def get_execution_wall_clock_time(self) -> Optional[float]:
|
125
|
+
"""Return the total time from the start of the sandbox to the
|
126
|
+
conclusion of the task.
|
127
|
+
|
128
|
+
return (float): total time the sandbox was alive.
|
129
|
+
|
130
|
+
"""
|
131
|
+
if self.log is None:
|
132
|
+
return None
|
133
|
+
return float(self.log['time-wall'])
|
134
|
+
|
135
|
+
def use_soft_timeout(self) -> bool:
|
136
|
+
return True
|
137
|
+
|
138
|
+
def get_memory_used(self) -> Optional[int]:
|
139
|
+
"""Return the memory used by the sandbox.
|
140
|
+
|
141
|
+
return (int): memory used by the sandbox (in bytes).
|
142
|
+
|
143
|
+
"""
|
144
|
+
if self.log is None:
|
145
|
+
return None
|
146
|
+
return int(self.log['mem']) * 1024
|
147
|
+
|
148
|
+
def get_killing_signal(self) -> int:
|
149
|
+
"""Return the signal that killed the sandboxed process.
|
150
|
+
|
151
|
+
return (int): offending signal, or 0.
|
152
|
+
|
153
|
+
"""
|
154
|
+
assert self.log is not None
|
155
|
+
if 'exit-sig' not in self.log:
|
156
|
+
return 0
|
157
|
+
return int(self.log['exit-sig'])
|
158
|
+
|
159
|
+
def get_status_list(self) -> List[str]:
|
160
|
+
"""Reads the sandbox log file, and set and return the status
|
161
|
+
of the sandbox.
|
162
|
+
|
163
|
+
return (list): list of statuses of the sandbox.
|
164
|
+
|
165
|
+
"""
|
166
|
+
assert self.log is not None
|
167
|
+
if 'status' in self.log:
|
168
|
+
return self.log['status'].split(',')
|
169
|
+
return []
|
170
|
+
|
171
|
+
# This sandbox only discriminates between processes terminating
|
172
|
+
# properly or being killed with a signal; all other exceptional
|
173
|
+
# conditions (RAM or CPU limitations, ...) result in some signal
|
174
|
+
# being delivered to the process
|
175
|
+
def get_exit_status(self) -> str:
|
176
|
+
"""Get information about how the sandbox terminated.
|
177
|
+
|
178
|
+
return (string): the main reason why the sandbox terminated.
|
179
|
+
|
180
|
+
"""
|
181
|
+
if self.returncode != 0:
|
182
|
+
return self.EXIT_SANDBOX_ERROR
|
183
|
+
status_list = self.get_status_list()
|
184
|
+
if 'WT' in status_list:
|
185
|
+
return self.EXIT_TIMEOUT_WALL
|
186
|
+
if 'TO' in status_list:
|
187
|
+
return self.EXIT_TIMEOUT
|
188
|
+
if 'OL' in status_list:
|
189
|
+
return self.EXIT_OUTPUT_LIMIT_EXCEEDED
|
190
|
+
if 'ML' in status_list:
|
191
|
+
return self.EXIT_MEMORY_LIMIT_EXCEEDED
|
192
|
+
if 'SG' in status_list:
|
193
|
+
return self.EXIT_SIGNAL
|
194
|
+
if 'RE' in status_list:
|
195
|
+
return self.EXIT_NONZERO_RETURN
|
196
|
+
return self.EXIT_OK
|
197
|
+
|
198
|
+
def get_exit_code(self) -> int:
|
199
|
+
"""Return the exit code of the sandboxed process.
|
200
|
+
|
201
|
+
return (float): exitcode, or 0.
|
202
|
+
|
203
|
+
"""
|
204
|
+
assert self.log is not None
|
205
|
+
return int(self.log['exit-code'])
|
206
|
+
|
207
|
+
def get_human_exit_description(self) -> str:
|
208
|
+
"""Get the status of the sandbox and return a human-readable
|
209
|
+
string describing it.
|
210
|
+
|
211
|
+
return (string): human-readable explaination of why the
|
212
|
+
sandbox terminated.
|
213
|
+
|
214
|
+
"""
|
215
|
+
status = self.get_exit_status()
|
216
|
+
if status == self.EXIT_OK:
|
217
|
+
return (
|
218
|
+
'Execution successfully finished (with exit code %d)'
|
219
|
+
% self.get_exit_code()
|
220
|
+
)
|
221
|
+
elif status == self.EXIT_SANDBOX_ERROR:
|
222
|
+
return 'Execution failed because of sandbox error'
|
223
|
+
elif status == self.EXIT_TIMEOUT:
|
224
|
+
return 'Execution timed out'
|
225
|
+
elif status == self.EXIT_TIMEOUT_WALL:
|
226
|
+
return 'Execution timed out (wall clock limit exceeded)'
|
227
|
+
elif status == self.EXIT_SIGNAL:
|
228
|
+
return 'Execution killed with signal %s' % self.get_killing_signal()
|
229
|
+
elif status == self.EXIT_NONZERO_RETURN:
|
230
|
+
return 'Execution failed because the return code was nonzero'
|
231
|
+
elif status == self.EXIT_OUTPUT_LIMIT_EXCEEDED:
|
232
|
+
return 'Execution exceeded output limit'
|
233
|
+
return ''
|
234
|
+
|
235
|
+
def get_current_log_name(self) -> pathlib.Path:
|
236
|
+
return pathlib.Path(f'logs.{self.exec_num}')
|
237
|
+
|
238
|
+
def hydrate_logs(self):
|
239
|
+
self.log = None
|
240
|
+
if not self.relative_path(self.get_current_log_name()).is_file():
|
241
|
+
return
|
242
|
+
self.log = {}
|
243
|
+
raw_log = self.get_file_to_string(self.get_current_log_name(), maxlen=None)
|
244
|
+
for line in raw_log.splitlines():
|
245
|
+
items = line.split(':', 1)
|
246
|
+
if len(items) != 2:
|
247
|
+
continue
|
248
|
+
key, value = items
|
249
|
+
self.log[key] = value.strip()
|
250
|
+
|
251
|
+
def execute_without_std(
|
252
|
+
self,
|
253
|
+
command: List[str],
|
254
|
+
) -> bool:
|
255
|
+
"""Execute the given command in the sandbox using
|
256
|
+
subprocess.Popen and discarding standard input, output and
|
257
|
+
error. More specifically, the standard input gets closed just
|
258
|
+
after the execution has started; standard output and error are
|
259
|
+
read until the end, in a way that prevents the execution from
|
260
|
+
being blocked because of insufficient buffering.
|
261
|
+
|
262
|
+
command ([string]): executable filename and arguments of the
|
263
|
+
command.
|
264
|
+
|
265
|
+
return (bool): True if the sandbox didn't report errors
|
266
|
+
(caused by the sandbox itself), False otherwise
|
267
|
+
|
268
|
+
"""
|
269
|
+
|
270
|
+
self.exec_num += 1
|
271
|
+
|
272
|
+
logger.debug(
|
273
|
+
"Executing program in sandbox with command: `%s'.", ' '.join(command)
|
274
|
+
)
|
275
|
+
with open(
|
276
|
+
self.relative_path(self.cmd_file), 'at', encoding='utf-8'
|
277
|
+
) as commands:
|
278
|
+
commands.write('%s\n' % command)
|
279
|
+
|
280
|
+
real_command = (
|
281
|
+
[
|
282
|
+
sys.executable,
|
283
|
+
str(self.get_timeit_executable().resolve()),
|
284
|
+
str(self.relative_path(self.get_current_log_name()).resolve()),
|
285
|
+
]
|
286
|
+
+ self.get_timeit_args()
|
287
|
+
+ command
|
288
|
+
)
|
289
|
+
self.returncode = subprocess.call(
|
290
|
+
real_command,
|
291
|
+
stdin=subprocess.PIPE,
|
292
|
+
stdout=subprocess.PIPE,
|
293
|
+
stderr=subprocess.STDOUT,
|
294
|
+
)
|
295
|
+
self.hydrate_logs()
|
296
|
+
return self.translate_box_exitcode(self.returncode)
|
297
|
+
|
298
|
+
def translate_box_exitcode(self, exitcode: int) -> bool:
|
299
|
+
# SIGALRM can be safely ignored, just in case it leaks away.
|
300
|
+
return super().translate_box_exitcode(exitcode) or -exitcode == signal.SIGALRM
|
301
|
+
|
302
|
+
def debug_message(self) -> Any:
|
303
|
+
return f'returncode = {self.returncode}\nlogs = {self.log}\ntimeit_args = {self.get_timeit_args()}'
|
304
|
+
|
305
|
+
def cleanup(self, delete=False):
|
306
|
+
"""See Sandbox.cleanup()."""
|
307
|
+
# This sandbox doesn't have any cleanup, but we might want to delete.
|
308
|
+
if delete:
|
309
|
+
logger.debug('Deleting sandbox in %s.', self._path)
|
310
|
+
shutil.rmtree(str(self._path))
|
@@ -0,0 +1,217 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import os
|
3
|
+
import pathlib
|
4
|
+
import resource
|
5
|
+
import signal
|
6
|
+
import stat
|
7
|
+
import sys
|
8
|
+
from math import ceil
|
9
|
+
from time import monotonic
|
10
|
+
from typing import List, Optional
|
11
|
+
|
12
|
+
|
13
|
+
@dataclasses.dataclass()
|
14
|
+
class Options:
|
15
|
+
output_file: str
|
16
|
+
argv: List[str]
|
17
|
+
chdir: Optional[str] = None
|
18
|
+
stdin_file: Optional[str] = None
|
19
|
+
stdout_file: Optional[str] = None
|
20
|
+
stderr_file: Optional[str] = None
|
21
|
+
time_limit: Optional[float] = None
|
22
|
+
wall_time_limit: Optional[float] = None # seconds
|
23
|
+
memory_limit: Optional[int] = None # kb, but passed in args as mb
|
24
|
+
fs_limit: Optional[int] = None # kb
|
25
|
+
|
26
|
+
|
27
|
+
def exit_with(code: int):
|
28
|
+
sys.exit(code)
|
29
|
+
|
30
|
+
|
31
|
+
def parse_opts() -> Options:
|
32
|
+
options = Options(output_file=sys.argv[1], argv=[])
|
33
|
+
num_opts = 0
|
34
|
+
while num_opts + 2 < len(sys.argv) and sys.argv[num_opts + 2].startswith('-'):
|
35
|
+
# Process option
|
36
|
+
opt = sys.argv[num_opts + 2]
|
37
|
+
if opt.startswith('-t'):
|
38
|
+
options.time_limit = float(opt[2:])
|
39
|
+
elif opt.startswith('-w'):
|
40
|
+
options.wall_time_limit = float(opt[2:])
|
41
|
+
elif opt.startswith('-m'):
|
42
|
+
options.memory_limit = int(opt[2:]) * 1024
|
43
|
+
elif opt.startswith('-i'):
|
44
|
+
options.stdin_file = opt[2:]
|
45
|
+
elif opt.startswith('-o'):
|
46
|
+
options.stdout_file = opt[2:]
|
47
|
+
elif opt.startswith('-e'):
|
48
|
+
options.stderr_file = opt[2:]
|
49
|
+
elif opt.startswith('-c'):
|
50
|
+
options.chdir = opt[2:]
|
51
|
+
elif opt.startswith('-f'):
|
52
|
+
options.fs_limit = int(opt[2:])
|
53
|
+
else:
|
54
|
+
raise Exception(f'Invalid option {opt}')
|
55
|
+
num_opts += 1
|
56
|
+
options.argv = sys.argv[num_opts + 2 :]
|
57
|
+
return options
|
58
|
+
|
59
|
+
|
60
|
+
def get_memory_usage(ru: resource.struct_rusage) -> int:
|
61
|
+
used = ceil((ru.ru_maxrss + ru.ru_ixrss + ru.ru_idrss + ru.ru_isrss) / 1024)
|
62
|
+
return used
|
63
|
+
|
64
|
+
|
65
|
+
def get_cpu_time(ru: resource.struct_rusage) -> float:
|
66
|
+
return ru.ru_utime + ru.ru_stime
|
67
|
+
|
68
|
+
|
69
|
+
def _get_file_size(filename: Optional[str]) -> int:
|
70
|
+
if filename is None:
|
71
|
+
return 0
|
72
|
+
path = pathlib.Path(filename)
|
73
|
+
if not path.is_file():
|
74
|
+
return 0
|
75
|
+
return path.stat().st_size
|
76
|
+
|
77
|
+
|
78
|
+
def get_file_sizes(options: Options):
|
79
|
+
return _get_file_size(options.stdout_file) + _get_file_size(options.stderr_file)
|
80
|
+
|
81
|
+
|
82
|
+
def set_rlimits(options: Options):
|
83
|
+
if options.time_limit is not None:
|
84
|
+
time_limit_in_ms = int(options.time_limit * 1000)
|
85
|
+
rlimit_cpu = int((time_limit_in_ms + 999) // 1000)
|
86
|
+
resource.setrlimit(resource.RLIMIT_CPU, (rlimit_cpu, rlimit_cpu + 1))
|
87
|
+
if options.fs_limit is not None:
|
88
|
+
fs_limit = options.fs_limit * 1024 # in bytes
|
89
|
+
resource.setrlimit(resource.RLIMIT_FSIZE, (fs_limit + 1, fs_limit * 2))
|
90
|
+
|
91
|
+
|
92
|
+
def redirect_fds(options: Options):
|
93
|
+
files = [options.stdin_file, options.stdout_file, options.stderr_file]
|
94
|
+
|
95
|
+
for i, file in enumerate(files):
|
96
|
+
if file is None:
|
97
|
+
continue
|
98
|
+
open_args = [
|
99
|
+
os.O_WRONLY | os.O_TRUNC | os.O_CREAT,
|
100
|
+
stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IWUSR,
|
101
|
+
]
|
102
|
+
if i == 0:
|
103
|
+
# stdin
|
104
|
+
open_args = [os.O_RDONLY]
|
105
|
+
fd = os.open(
|
106
|
+
file,
|
107
|
+
*open_args,
|
108
|
+
)
|
109
|
+
os.dup2(fd, i)
|
110
|
+
os.close(fd)
|
111
|
+
|
112
|
+
|
113
|
+
def wait_and_finish(
|
114
|
+
pid: int,
|
115
|
+
options: Options,
|
116
|
+
start_time: float,
|
117
|
+
alarm_msg: Optional[List[Optional[str]]] = None,
|
118
|
+
):
|
119
|
+
_, exitstatus, ru = os.wait4(pid, 0)
|
120
|
+
wall_time = monotonic() - start_time
|
121
|
+
cpu_time = get_cpu_time(ru)
|
122
|
+
memory_used = get_memory_usage(ru)
|
123
|
+
file_sizes = get_file_sizes(options)
|
124
|
+
|
125
|
+
entries = []
|
126
|
+
exitcode = os.waitstatus_to_exitcode(exitstatus)
|
127
|
+
entries.append(f'exit-code: {exitcode}')
|
128
|
+
if exitcode < 0:
|
129
|
+
entries.append(f'exit-sig: {-exitcode}')
|
130
|
+
|
131
|
+
status = set()
|
132
|
+
if exitcode > 0:
|
133
|
+
status.add('RE')
|
134
|
+
if exitcode < 0:
|
135
|
+
status.add('SG')
|
136
|
+
if options.time_limit is not None and (
|
137
|
+
cpu_time > options.time_limit or -exitcode == 24
|
138
|
+
):
|
139
|
+
status.add('TO')
|
140
|
+
cpu_time = max(cpu_time, options.time_limit)
|
141
|
+
if options.wall_time_limit is not None and wall_time > options.wall_time_limit:
|
142
|
+
status.add('WT')
|
143
|
+
status.add('TO')
|
144
|
+
if options.memory_limit is not None and memory_used > options.memory_limit:
|
145
|
+
status.add('ML')
|
146
|
+
if options.fs_limit is not None and file_sizes > options.fs_limit * 1024:
|
147
|
+
status.add('OL')
|
148
|
+
|
149
|
+
if status:
|
150
|
+
status_str = ','.join(status)
|
151
|
+
entries.append(f'status: {status_str}')
|
152
|
+
|
153
|
+
if alarm_msg:
|
154
|
+
alarm_str = ','.join(msg for msg in alarm_msg if msg is not None)
|
155
|
+
if alarm_str:
|
156
|
+
entries.append(f'alarm-msg: {alarm_str}')
|
157
|
+
|
158
|
+
entries.append(f'time: {cpu_time:.3f}')
|
159
|
+
entries.append(f'time-wall: {wall_time:.3f}')
|
160
|
+
entries.append(f'mem: {memory_used}')
|
161
|
+
entries.append(f'file: {file_sizes}')
|
162
|
+
|
163
|
+
output_file = pathlib.Path(sys.argv[1])
|
164
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
165
|
+
output_file.write_text('\n'.join(entries) + '\n')
|
166
|
+
|
167
|
+
|
168
|
+
def main():
|
169
|
+
options = parse_opts()
|
170
|
+
|
171
|
+
start_time = monotonic()
|
172
|
+
sub_pid = os.fork()
|
173
|
+
if sub_pid == 0:
|
174
|
+
if options.chdir is not None:
|
175
|
+
os.chdir(options.chdir)
|
176
|
+
set_rlimits(options)
|
177
|
+
redirect_fds(options)
|
178
|
+
os.execvp(options.argv[0], options.argv)
|
179
|
+
|
180
|
+
alarm_msg: List[Optional[str]] = [None]
|
181
|
+
|
182
|
+
def handle_alarm(*args, **kwargs):
|
183
|
+
nonlocal alarm_msg
|
184
|
+
wall_time = monotonic() - start_time
|
185
|
+
if options.wall_time_limit is not None and wall_time > options.wall_time_limit:
|
186
|
+
alarm_msg[0] = 'wall timelimit'
|
187
|
+
os.kill(sub_pid, 9)
|
188
|
+
return
|
189
|
+
ru = resource.getrusage(resource.RUSAGE_CHILDREN)
|
190
|
+
if options.time_limit is not None:
|
191
|
+
cpu_time = get_cpu_time(ru)
|
192
|
+
if cpu_time > options.time_limit:
|
193
|
+
alarm_msg[0] = 'timelimit'
|
194
|
+
os.kill(sub_pid, 9)
|
195
|
+
return
|
196
|
+
if options.memory_limit is not None:
|
197
|
+
memory_used = get_memory_usage(ru)
|
198
|
+
if memory_used > options.memory_limit:
|
199
|
+
alarm_msg[0] = 'memorylimit'
|
200
|
+
os.kill(sub_pid, 9)
|
201
|
+
return
|
202
|
+
|
203
|
+
signal.alarm(1)
|
204
|
+
|
205
|
+
signal.alarm(1)
|
206
|
+
signal.signal(signal.SIGALRM, handle_alarm)
|
207
|
+
wait_and_finish(sub_pid, options, start_time, alarm_msg=alarm_msg)
|
208
|
+
|
209
|
+
# Cancel alarm before exiting to avoid surprises.
|
210
|
+
signal.alarm(0)
|
211
|
+
|
212
|
+
# Exit gracefully.
|
213
|
+
sys.exit()
|
214
|
+
|
215
|
+
|
216
|
+
if __name__ == '__main__':
|
217
|
+
main()
|