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.
Files changed (164) hide show
  1. rbx/__init__.py +0 -0
  2. rbx/annotations.py +127 -0
  3. rbx/autoenum.py +333 -0
  4. rbx/box/__init__.py +0 -0
  5. rbx/box/builder.py +77 -0
  6. rbx/box/cd.py +37 -0
  7. rbx/box/checkers.py +134 -0
  8. rbx/box/code.py +185 -0
  9. rbx/box/compile.py +56 -0
  10. rbx/box/conftest.py +42 -0
  11. rbx/box/contest/__init__.py +0 -0
  12. rbx/box/contest/build_contest_statements.py +347 -0
  13. rbx/box/contest/contest_package.py +76 -0
  14. rbx/box/contest/contest_utils.py +20 -0
  15. rbx/box/contest/main.py +179 -0
  16. rbx/box/contest/schema.py +155 -0
  17. rbx/box/contest/statements.py +82 -0
  18. rbx/box/creation.py +72 -0
  19. rbx/box/download.py +64 -0
  20. rbx/box/environment.py +345 -0
  21. rbx/box/extensions.py +26 -0
  22. rbx/box/generators.py +478 -0
  23. rbx/box/generators_test.py +63 -0
  24. rbx/box/main.py +449 -0
  25. rbx/box/package.py +316 -0
  26. rbx/box/packaging/boca/extension.py +27 -0
  27. rbx/box/packaging/boca/packager.py +245 -0
  28. rbx/box/packaging/contest_main.py +82 -0
  29. rbx/box/packaging/main.py +68 -0
  30. rbx/box/packaging/packager.py +117 -0
  31. rbx/box/packaging/polygon/packager.py +320 -0
  32. rbx/box/packaging/polygon/test.py +81 -0
  33. rbx/box/packaging/polygon/xml_schema.py +106 -0
  34. rbx/box/presets/__init__.py +503 -0
  35. rbx/box/presets/fetch.py +70 -0
  36. rbx/box/presets/lock_schema.py +20 -0
  37. rbx/box/presets/schema.py +59 -0
  38. rbx/box/schema.py +394 -0
  39. rbx/box/solutions.py +792 -0
  40. rbx/box/solutions_test.py +41 -0
  41. rbx/box/statements/__init__.py +0 -0
  42. rbx/box/statements/build_statements.py +359 -0
  43. rbx/box/statements/builders.py +375 -0
  44. rbx/box/statements/joiners.py +113 -0
  45. rbx/box/statements/latex.py +47 -0
  46. rbx/box/statements/latex_jinja.py +214 -0
  47. rbx/box/statements/schema.py +138 -0
  48. rbx/box/stresses.py +292 -0
  49. rbx/box/stressing/__init__.py +0 -0
  50. rbx/box/stressing/finder_parser.py +359 -0
  51. rbx/box/stressing/generator_parser.py +258 -0
  52. rbx/box/testcases.py +54 -0
  53. rbx/box/ui/__init__.py +0 -0
  54. rbx/box/ui/captured_log.py +372 -0
  55. rbx/box/ui/css/app.tcss +48 -0
  56. rbx/box/ui/main.py +38 -0
  57. rbx/box/ui/run.py +209 -0
  58. rbx/box/validators.py +245 -0
  59. rbx/box/validators_test.py +15 -0
  60. rbx/checker.py +128 -0
  61. rbx/clone.py +197 -0
  62. rbx/config.py +271 -0
  63. rbx/conftest.py +38 -0
  64. rbx/console.py +27 -0
  65. rbx/create.py +37 -0
  66. rbx/edit.py +24 -0
  67. rbx/grading/__init__.py +0 -0
  68. rbx/grading/caching.py +356 -0
  69. rbx/grading/conftest.py +33 -0
  70. rbx/grading/judge/__init__.py +0 -0
  71. rbx/grading/judge/cacher.py +503 -0
  72. rbx/grading/judge/digester.py +35 -0
  73. rbx/grading/judge/sandbox.py +748 -0
  74. rbx/grading/judge/sandboxes/__init__.py +0 -0
  75. rbx/grading/judge/sandboxes/isolate.py +683 -0
  76. rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
  77. rbx/grading/judge/sandboxes/timeit.py +217 -0
  78. rbx/grading/judge/storage.py +284 -0
  79. rbx/grading/judge/test.py +38 -0
  80. rbx/grading/judge/testiso.py +54 -0
  81. rbx/grading/steps.py +522 -0
  82. rbx/grading/steps_with_caching.py +59 -0
  83. rbx/grading/steps_with_caching_run_test.py +429 -0
  84. rbx/grading_utils.py +148 -0
  85. rbx/hydration.py +101 -0
  86. rbx/main.py +122 -0
  87. rbx/metadata.py +105 -0
  88. rbx/providers/__init__.py +43 -0
  89. rbx/providers/codeforces.py +73 -0
  90. rbx/providers/provider.py +26 -0
  91. rbx/resources/checkers/boilerplate.cpp +20 -0
  92. rbx/resources/default_config.json +48 -0
  93. rbx/resources/envs/default.rbx.yml +37 -0
  94. rbx/resources/envs/isolate.rbx.yml +37 -0
  95. rbx/resources/packagers/boca/checker.sh +43 -0
  96. rbx/resources/packagers/boca/compare +53 -0
  97. rbx/resources/packagers/boca/compile/c +172 -0
  98. rbx/resources/packagers/boca/compile/cc +173 -0
  99. rbx/resources/packagers/boca/compile/cpp +172 -0
  100. rbx/resources/packagers/boca/compile/java +194 -0
  101. rbx/resources/packagers/boca/compile/kt +155 -0
  102. rbx/resources/packagers/boca/compile/pas +172 -0
  103. rbx/resources/packagers/boca/compile/py2 +173 -0
  104. rbx/resources/packagers/boca/compile/py3 +173 -0
  105. rbx/resources/packagers/boca/run/c +128 -0
  106. rbx/resources/packagers/boca/run/cc +128 -0
  107. rbx/resources/packagers/boca/run/cpp +128 -0
  108. rbx/resources/packagers/boca/run/java +194 -0
  109. rbx/resources/packagers/boca/run/kt +159 -0
  110. rbx/resources/packagers/boca/run/py2 +166 -0
  111. rbx/resources/packagers/boca/run/py3 +166 -0
  112. rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
  113. rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
  114. rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
  115. rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
  116. rbx/resources/presets/default/preset.rbx.yml +12 -0
  117. rbx/resources/presets/default/problem/.gitignore +6 -0
  118. rbx/resources/presets/default/problem/gen.cpp +9 -0
  119. rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
  120. rbx/resources/presets/default/problem/random.py +3 -0
  121. rbx/resources/presets/default/problem/random.txt +2 -0
  122. rbx/resources/presets/default/problem/sols/main.cpp +9 -0
  123. rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
  124. rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
  125. rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
  126. rbx/resources/presets/default/problem/statement/projecao.png +0 -0
  127. rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
  128. rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
  129. rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
  130. rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
  131. rbx/resources/presets/default/problem/validator.cpp +16 -0
  132. rbx/resources/presets/default/problem/wcmp.cpp +34 -0
  133. rbx/resources/templates/template.cpp +19 -0
  134. rbx/run.py +45 -0
  135. rbx/schema.py +64 -0
  136. rbx/submit.py +61 -0
  137. rbx/submitors/__init__.py +18 -0
  138. rbx/submitors/codeforces.py +120 -0
  139. rbx/submitors/submitor.py +25 -0
  140. rbx/test.py +347 -0
  141. rbx/testcase.py +70 -0
  142. rbx/testcase_rendering.py +79 -0
  143. rbx/testdata/box1/gen1.cpp +7 -0
  144. rbx/testdata/box1/gen2.cpp +9 -0
  145. rbx/testdata/box1/genScript.py +2 -0
  146. rbx/testdata/box1/hard-tle.sol.cpp +26 -0
  147. rbx/testdata/box1/ole.cpp +17 -0
  148. rbx/testdata/box1/problem.rbx.yml +39 -0
  149. rbx/testdata/box1/re.sol.cpp +23 -0
  150. rbx/testdata/box1/sol.cpp +22 -0
  151. rbx/testdata/box1/tests/1.in +1 -0
  152. rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
  153. rbx/testdata/box1/tle.sol.cpp +35 -0
  154. rbx/testdata/box1/validator.cpp +11 -0
  155. rbx/testdata/box1/wa.sol.cpp +22 -0
  156. rbx/testdata/caching/executable.py +1 -0
  157. rbx/testdata/compatible +0 -0
  158. rbx/testing_utils.py +65 -0
  159. rbx/utils.py +162 -0
  160. rbx_cp-0.5.0.dist-info/LICENSE +201 -0
  161. rbx_cp-0.5.0.dist-info/METADATA +89 -0
  162. rbx_cp-0.5.0.dist-info/RECORD +164 -0
  163. rbx_cp-0.5.0.dist-info/WHEEL +4 -0
  164. 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()